Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/sourcing/service.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 9bde0f9a74b89d01980fa658e5253acda7528d0c |
| Branch | per-tenant-routing |
| Path or archive member | internal/modules/sourcing/service.go |
| Original SHA-256 | c4ddf1e620bfcb997017688498316c3c29ae297e1ee41331db89e49e7a30572c |
| Git blob SHA | d0c82d619d3648fc80ad68f22bfe617ad8c5de96 |
| Reading-copy SHA-256 | c4ddf1e620bfcb997017688498316c3c29ae297e1ee41331db89e49e7a30572c |
Every recorded occurrence
These 3 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/sourcing/service.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / internal/modules/sourcing/service.go | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / internal/modules/sourcing/service.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
Recovered source transcript
Download complete source text ↓186 lines · 6.3 KB
package sourcing
import (
"context"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/missionworkforce/missionmatch/internal/platform/audit"
"github.com/missionworkforce/missionmatch/internal/platform/database/dbgen"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// ErrNoDatabase is returned when no pool is configured. ErrNotFound when a delete matches nothing.
var (
ErrNoDatabase = errors.New("sourcing: no database configured")
ErrNotFound = errors.New("sourcing: external candidate not found")
)
const listLimit = 200
// Service reads externally sourced candidates and the compliance log, and deletes sourced records.
type Service struct {
db *tenant.DB
aud *audit.Writer
}
// NewService builds the service. db may be nil (then calls return ErrNoDatabase). The audit writer
// records the PII view and erasure events on the same tenant transaction as the work.
func NewService(db *tenant.DB, aud *audit.Writer) *Service { return &Service{db: db, aud: aud} }
// ListExternal returns the org's externally sourced candidates.
func (s *Service) ListExternal(ctx context.Context) ([]ExternalCandidate, error) {
if s.db == nil {
return nil, ErrNoDatabase
}
var out []ExternalCandidate
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
rows, err := dbgen.New(tx).ListExternalCandidates(ctx, listLimit)
if err != nil {
return err
}
out = make([]ExternalCandidate, 0, len(rows))
for _, r := range rows {
out = append(out, ExternalCandidate{
ID: r.ID.String(),
FullName: r.FullName,
Email: deref(r.Email),
Headline: deref(r.Headline),
Location: deref(r.Location),
YearsExperience: int(r.YearsExperience),
Available: r.Available,
ExternalID: deref(r.ExternalID),
Source: "External",
Clearance: "Unknown",
CreatedAt: r.CreatedAt.Format(time.RFC3339),
})
}
// Viewing externally sourced candidates surfaces contact PII, so record the access.
return s.audit(ctx, tx, audit.Event{Action: "candidate.pii.view", EntityType: "sourcing", NewValues: countJSON(len(out))})
})
return out, err
}
func countJSON(n int) []byte {
return []byte(fmt.Sprintf(`{"count":%d}`, n))
}
// Log returns the org's sourcing compliance log.
func (s *Service) Log(ctx context.Context) ([]SourcingLogEntry, error) {
if s.db == nil {
return nil, ErrNoDatabase
}
var out []SourcingLogEntry
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
rows, err := dbgen.New(tx).ListSourcingLog(ctx, listLimit)
if err != nil {
return err
}
out = make([]SourcingLogEntry, 0, len(rows))
for _, r := range rows {
cid := ""
if r.CandidateID != nil {
cid = r.CandidateID.String()
}
out = append(out, SourcingLogEntry{
ID: r.ID.String(),
CandidateID: cid,
ExternalID: r.ExternalID,
Source: r.Source,
Provider: r.Provider,
Query: r.Query,
LegalBasis: r.LegalBasis,
CollectedAt: r.CollectedAt.Format(time.RFC3339),
})
}
return nil
})
return out, err
}
// Delete hard deletes a sourced candidate (removing the personal data) and detaches its compliance row
// so the audit that a record was collected and then deleted is retained without the personal data.
func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
if s.db == nil {
return ErrNoDatabase
}
return s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
q := dbgen.New(tx)
// Detach the compliance log so the audit that a record was collected survives without the
// candidate link.
if err := q.DetachSourcingLog(ctx, &id); err != nil {
return err
}
// Anonymize in place rather than hard delete: the PII is wiped but the de identified scored
// outcomes (slate_members) and the demographic bucket are KEPT for the adverse impact record, so
// erasure does not destroy the AEDT decision history. Once the name, email, contact, and resume
// vector are gone the person is no longer identifiable.
rows, err := q.AnonymizeCandidate(ctx, id)
if err != nil {
return err
}
if rows == 0 {
// Distinguish an already anonymized candidate (idempotent success) from one that does not
// exist in this org (not found).
if _, gerr := q.GetCandidate(ctx, id); errors.Is(gerr, pgx.ErrNoRows) {
return ErrNotFound
}
return nil
}
// Purge the qualification detail too (skills, certifications, customer agency experience) so no
// child PII survives the erasure. The frozen scored outcomes in slate_members are untouched.
if err := purgeCandidateChildData(ctx, tx, id); err != nil {
return err
}
// Wipe any AI memory about this candidate so erasure leaves no semantic trace in the AI layer
// (the same intent as nulling the resume vector). ai_memory is RLS scoped, so this is org bounded.
if _, err := tx.Exec(ctx, "DELETE FROM ai_memory WHERE candidate_id = $1", id); err != nil {
return err
}
// Remove the candidate's W3 staffing recommendations too. Anonymize keeps the candidate row and its
// frozen evaluations, so the FK cascade does not fire; this is the explicit cleanup, like ai_memory.
if err := q.DeleteRecommendationsForCandidate(ctx, &id); err != nil {
return err
}
return s.audit(ctx, tx, audit.Event{Action: "privacy.candidate.anonymized", EntityType: "candidate", EntityID: id.String()})
})
}
// purgeCandidateChildData removes a candidate's qualification rows. RLS scopes each delete to the org.
func purgeCandidateChildData(ctx context.Context, tx pgx.Tx, id uuid.UUID) error {
for _, stmt := range []string{
`DELETE FROM candidate_skills WHERE candidate_id = $1`,
`DELETE FROM candidate_certifications WHERE candidate_id = $1`,
`DELETE FROM candidate_customer_experience WHERE candidate_id = $1`,
} {
if _, err := tx.Exec(ctx, stmt, id); err != nil {
return err
}
}
return nil
}
// audit records an event on the tenant tx with the actor taken from the validated session.
func (s *Service) audit(ctx context.Context, tx pgx.Tx, e audit.Event) error {
if s.aud == nil {
return nil
}
if p, ok := tenant.UserFromContext(ctx); ok {
if id, err := uuid.Parse(p.UserID); err == nil {
e.ActorUserID = &id
}
}
return s.aud.OnTx(ctx, tx, e)
}
func deref(s *string) string {
if s == nil {
return ""
}
return *s
}