Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/recruiting/pipeline_service.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/recruiting/pipeline_service.go |
| Original SHA-256 | af59dcc15f0fb62ecd544210d59c5768f36028783ac4f0397037df7597401b61 |
| Git blob SHA | 6ee3c77052a385227a410440b45df41dcc05476b |
| Reading-copy SHA-256 | af59dcc15f0fb62ecd544210d59c5768f36028783ac4f0397037df7597401b61 |
Every recorded occurrence
These 2 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/recruiting/pipeline_service.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/recruiting/pipeline_service.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓502 lines · 18.8 KB
package recruiting
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/shopspring/decimal"
"github.com/missionworkforce/missionmatch/internal/platform/audit"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
const appSelect = `
a.id::text, a.requisition_id::text, r.title, cl.name,
a.candidate_id::text, c.full_name, c.clearance_level, c.available,
a.stage, a.furthest_stage_ord, a.disposition_code, a.disposition_reason,
a.owner_user_id::text, u.full_name,
a.source_first_touch, a.source_last_touch,
a.stage_changed_at, a.created_at, a.updated_at`
const appFrom = `
FROM applications a
JOIN candidates c ON c.organization_id = a.organization_id AND c.id = a.candidate_id
JOIN requisitions r ON r.organization_id = a.organization_id AND r.id = a.requisition_id
LEFT JOIN clients cl ON cl.organization_id = r.organization_id AND cl.id = r.client_id
LEFT JOIN users u ON u.organization_id = a.organization_id AND u.id = a.owner_user_id`
func scanApplication(row scannable, now time.Time) (Application, error) {
var a Application
var reqTitle, clientName, ownerID, ownerName, dispCode, dispReason *string
var furthest int
var changed, created, updated time.Time
if err := row.Scan(
&a.ID, &a.RequisitionID, &reqTitle, &clientName,
&a.CandidateID, &a.CandidateName, &a.ClearanceLevel, &a.Available,
&a.Stage, &furthest, &dispCode, &dispReason,
&ownerID, &ownerName,
&a.SourceFirstTouch, &a.SourceLastTouch,
&changed, &created, &updated,
); err != nil {
return Application{}, err
}
a.RequisitionTitle, a.ClientName = str(reqTitle), str(clientName)
a.OwnerUserID, a.OwnerName = str(ownerID), str(ownerName)
a.DispositionCode, a.DispositionReason = str(dispCode), str(dispReason)
a.StageLabel = stageLabels[a.Stage]
if furthest >= 1 && furthest <= len(ladder) {
a.FurthestStage = ladder[furthest-1]
a.FurthestStageLabel = stageLabels[a.FurthestStage]
}
a.StageChangedAt, a.CreatedAt, a.UpdatedAt = ts(changed), ts(created), ts(updated)
a.DaysInStage = days(stageAge(changed, now))
return a, nil
}
// Submit adds a candidate to a job order's pipeline.
//
// The unique constraint on (organization_id, requisition_id, candidate_id) is the authority on
// duplicates, not a pre check. Two recruiters submitting the same person to the same role at the same
// moment is exactly the case a SELECT then INSERT would get wrong, and the constraint cannot.
func (s *Service) Submit(ctx context.Context, reqID uuid.UUID, in CreateApplicationInput) (Application, error) {
if s.db == nil {
return Application{}, ErrNoDatabase
}
candID, err := uuid.Parse(strings.TrimSpace(in.CandidateID))
if err != nil {
return Application{}, &ValidationError{Field: "candidate_id", Message: "must be a candidate id"}
}
stage := strings.TrimSpace(in.Stage)
if stage == "" {
stage = stageSourced
}
// An application cannot BEGIN in a terminal state. A rejection with no preceding consideration is not
// a pipeline record, and it would enter the adverse impact denominator without ever being a real
// candidacy.
if !isStage(stage) || isTerminalStage(stage) {
return Application{}, &ValidationError{Field: "stage", Message: "a submission starts on the ladder, not at an outcome"}
}
reason, err := clean("reason", in.Reason, maxReqNote)
if err != nil {
return Application{}, err
}
touch, err := clean("source_touch", in.SourceTouch, 120)
if err != nil {
return Application{}, err
}
actor := actorPtr(ctx)
if actor == nil {
return Application{}, tenant.ErrNoTenant
}
// Scanned as a uuid.UUID rather than as text. The id comes from the database, so parsing it back
// could only ever fail on something impossible, but uuid.MustParse turns that impossible case into a
// panic in a request path, which CLAUDE.md forbids. Two of those panics sat AFTER the commit, where a
// panic would destroy the response for work that is already durably written and cannot be retried
// safely. Scanning the native type removes the parse, and with it the question.
var newID uuid.UUID
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
// The job order must be live. Submitting into a cancelled or closed requisition records work
// against a role that no longer exists, and it would inflate the funnel of a dead order.
var status string
if e := tx.QueryRow(ctx,
`SELECT status FROM requisitions WHERE id = $1 AND deleted_at IS NULL`, reqID).Scan(&status); e != nil {
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
return translatePG(e)
}
if status == reqClosed || status == reqCancelled {
return &ValidationError{Field: "requisition_id",
Message: "this job order is " + status + ", so candidates cannot be submitted to it"}
}
// Insert at the default stage, then move it with an event if a later start was asked for. The
// stage column is writable only through the event log, so this is the only correct order.
e := tx.QueryRow(ctx, `
INSERT INTO applications
(organization_id, requisition_id, candidate_id, owner_user_id, created_by,
source_first_touch, source_last_touch)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid, $1, $2, $3, $3, $4, $4)
RETURNING id`, reqID, candID, actor, touch).Scan(&newID)
if e != nil {
return translatePG(e)
}
if err := insertStageEvent(ctx, tx, newID, stageSourced, stageSourced, "", reason, actor, true); err != nil {
return err
}
aid := newID
if stage != stageSourced {
if err := insertStageEvent(ctx, tx, newID, stageSourced, stage, "", reason, actor, false); err != nil {
return err
}
}
// Captured, not logged. The flag is raised inside captureActivity and cleared immediately, and
// this is unreachable from HTTP, so a human cannot produce a row that claims to be captured.
if e := captureActivity(ctx, tx, activityRow{
actor: actor, kind: "submission", direction: "internal", outcome: "logged",
candidateID: &candID, applicationID: &aid, requisitionID: &reqID,
body: "Added to the pipeline at " + stage,
}); e != nil {
return e
}
nv, _ := json.Marshal(map[string]string{"application_id": newID.String(), "candidate_id": candID.String(), "stage": stage})
return s.audit.OnTx(ctx, tx, audit.Event{
Action: "application.created", ActorUserID: actor,
EntityType: "application", EntityID: newID.String(), NewValues: nv,
})
})
if err != nil {
return Application{}, err
}
return s.applicationRow(ctx, newID)
}
// MoveStage advances, reverses or disposes an application.
//
// Almost every rule here is enforced by the database, deliberately. from_stage must match reality, the
// event cannot predate the current stage, the high water mark cannot fall, a terminal stage needs a
// coded reason, and placements cannot exceed the openings. Each of those is a race if it is checked in
// application code and a fact if it is checked under the row lock the trigger holds, so this method
// validates only what is purely a matter of vocabulary and lets the database decide the rest.
func (s *Service) MoveStage(ctx context.Context, id uuid.UUID, in MoveStageInput) (Application, error) {
if s.db == nil {
return Application{}, ErrNoDatabase
}
to := strings.TrimSpace(in.ToStage)
if !isStage(to) {
return Application{}, &ValidationError{Field: "to_stage", Message: "is not a permitted stage"}
}
expected := strings.TrimSpace(in.ExpectedStage)
// Required, not optional. It is the concurrency token: the database refuses the write unless the
// application is still where the caller last saw it, which is what stops two recruiters moving the
// same person from different screens.
if !isStage(expected) {
return Application{}, &ValidationError{Field: "expected_stage", Message: "is required and must be the stage you last saw"}
}
if expected == to {
return Application{}, &ValidationError{Field: "to_stage", Message: "the application is already at that stage"}
}
reason, err := clean("reason", in.Reason, maxReqNote)
if err != nil {
return Application{}, err
}
code := strings.TrimSpace(in.DispositionCode)
if isTerminalStage(to) {
if code == "" {
return Application{}, &ValidationError{Field: "disposition_code",
Message: "say why this candidate left the process"}
}
if err := validDisposition(code, to); err != nil {
return Application{}, err
}
} else if code != "" {
return Application{}, &ValidationError{Field: "disposition_code",
Message: "a reason code belongs only to a rejection or a withdrawal"}
}
actor := actorPtr(ctx)
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
if err := insertStageEvent(ctx, tx, id, expected, to, code, reason, actor, false); err != nil {
return err
}
nv, _ := json.Marshal(map[string]string{"to": to, "code": code, "reason": reason})
return s.audit.OnTx(ctx, tx, audit.Event{
Action: "application.stage_changed", ActorUserID: actor,
EntityType: "application", EntityID: id.String(), NewValues: nv,
})
})
if err != nil {
return Application{}, err
}
return s.applicationRow(ctx, id)
}
// insertStageEvent writes the event. The trigger validates it, moves the row and measures the duration,
// so this inserts and interprets the failure rather than duplicating any of that logic.
//
// first marks the opening event, whose from_stage is the same as its to_stage. The table's own CHECK
// forbids from_stage = to_stage, so the opening event is written with a NULL predecessor and the trigger
// fills it in.
func insertStageEvent(ctx context.Context, tx pgx.Tx, id uuid.UUID, from, to, code, reason string, actor *uuid.UUID, first bool) error {
var fromArg any
if from != "" && !first {
fromArg = from
}
var codeArg any
if code != "" {
codeArg = code
}
_, err := tx.Exec(ctx, `
INSERT INTO application_stage_events
(organization_id, application_id, from_stage, to_stage, disposition_code, reason,
actor_user_id, actor_name)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid, $1, $2, $3, $4, $5, $6,
COALESCE((SELECT full_name FROM users WHERE id = $6), ''))`,
id, fromArg, to, codeArg, reason, actor)
if err != nil {
return translateStageErr(err)
}
return nil
}
// translateStageErr maps the trigger's raised conditions onto typed domain errors. The trigger is the
// only place these rules can be checked without a race, so its messages are load bearing and this
// mapping is what turns them into a status the caller can act on.
func translateStageErr(err error) error {
var pg *pgconn.PgError
if !errors.As(err, &pg) {
return translatePG(err)
}
switch {
case pg.Code == "40001": // the trigger's stage conflict
return ErrConflict
case pg.Code == "22007": // an out of order event
return &ValidationError{Field: "occurred_at", Message: "that change is older than the current stage"}
// The live placement guard from 0055 fires on a stage move, so its message has to be understood
// here as well as in translateAssignmentErr. Without this case the trigger's instruction, which
// tells the user exactly what to do, was replaced by "failed a database constraint".
case pg.Code == "23514" && strings.Contains(pg.Message, "live placement"):
return &ValidationError{Field: "to_stage", Message: pg.Message}
case pg.Code == "23514" && strings.Contains(pg.Message, "opening"):
return &ValidationError{Field: "to_stage", Message: pg.Message}
case pg.Code == "23514" && strings.Contains(pg.Message, "reason code"):
return &ValidationError{Field: "disposition_code", Message: "say why this candidate left the process"}
case pg.Code == "23505":
return &DuplicateError{Field: "candidate_id"}
case pg.Code == "42501":
// The stage guard fired, which means something tried to write stage outside the event log. That
// is a defect in this service, not bad input.
return err
}
return translatePG(err)
}
// Board assembles one requisition's pipeline.
//
// The funnel is computed from high water marks in Go rather than by a GROUP BY on the current stage,
// because only the high water mark yields a monotone series. A GROUP BY on stage answers "who is here
// now", which is the board's columns, and those two questions have different answers on purpose.
func (s *Service) Board(ctx context.Context, reqID uuid.UUID) (PipelineBoard, error) {
if s.db == nil {
return PipelineBoard{}, ErrNoDatabase
}
board := PipelineBoard{
Columns: []StageColumn{}, Funnel: []FunnelStage{},
Conversion: []Conversion{}, Dispositions: []DispositionBreakdown{},
}
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
if e := tx.QueryRow(ctx,
`SELECT id::text, title, openings FROM requisitions WHERE id = $1 AND deleted_at IS NULL`, reqID).
Scan(&board.RequisitionID, &board.RequisitionTitle, &board.Openings); e != nil {
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
return translatePG(e)
}
now := time.Now().UTC()
rows, e := tx.Query(ctx, `SELECT`+appSelect+appFrom+`
WHERE a.requisition_id = $1
ORDER BY a.furthest_stage_ord DESC, a.stage_changed_at ASC, a.id ASC`, reqID)
if e != nil {
return translatePG(e)
}
defer rows.Close()
byStage := map[string][]Application{}
var marks []int
dispCount := map[string]DispositionBreakdown{}
for rows.Next() {
a, e := scanApplication(rows, now)
if e != nil {
return translatePG(e)
}
board.Total++
if !isTerminalStage(a.Stage) {
board.Active++
}
if a.Stage == stagePlaced {
board.Placed++
}
marks = append(marks, stageOrd[a.FurthestStage])
byStage[a.Stage] = append(byStage[a.Stage], a)
if a.DispositionCode != "" {
d := dispCount[a.DispositionCode]
d.Code, d.Stage, d.Count = a.DispositionCode, a.Stage, d.Count+1
dispCount[a.DispositionCode] = d
}
}
if e := rows.Err(); e != nil {
return e
}
// Every stage gets a column, including the empty ones, so the board does not change shape as
// candidates move and a recruiter can always drop someone into the next stage.
for _, st := range append(append([]string{}, ladder...), stageRejected, stageWithdrawn) {
apps := byStage[st]
if apps == nil {
apps = []Application{}
}
board.Columns = append(board.Columns, StageColumn{
Stage: st, Label: stageLabels[st], Count: len(apps), Applications: apps,
})
}
board.Funnel = funnelFrom(marks, board.Total)
board.Conversion = conversionFrom(board.Funnel)
for _, d := range dispCount {
d.Pct = pctOf(d.Count, board.Total)
board.Dispositions = append(board.Dispositions, d)
}
board.SeatsLeft = board.Openings - board.Placed
if board.SeatsLeft < 0 {
// Unreachable: the trigger refuses a placement past the last seat. Clamped rather than shown
// negative because a negative vacancy is never a real quantity.
board.SeatsLeft = 0
}
return nil
})
if err != nil {
return PipelineBoard{}, err
}
return board, nil
}
// GetApplication returns one application with its full history.
func (s *Service) GetApplication(ctx context.Context, id uuid.UUID) (ApplicationDetail, error) {
if s.db == nil {
return ApplicationDetail{}, ErrNoDatabase
}
out := ApplicationDetail{Events: []StageEvent{}}
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
now := time.Now().UTC()
a, e := scanApplication(tx.QueryRow(ctx, `SELECT`+appSelect+appFrom+` WHERE a.id = $1`, id), now)
if e != nil {
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
return translatePG(e)
}
out.Application = a
rows, e := tx.Query(ctx, `
SELECT e.id::text, e.from_stage, e.to_stage, e.seconds_in_from::text, e.reason,
e.disposition_code, e.actor_user_id::text, NULLIF(e.actor_name, ''), e.occurred_at
FROM application_stage_events e
WHERE e.application_id = $1
ORDER BY e.occurred_at ASC, e.id ASC`, id)
if e != nil {
return translatePG(e)
}
defer rows.Close()
for rows.Next() {
var ev StageEvent
var from, secs, code, actor, actorName *string
var at time.Time
if e := rows.Scan(&ev.ID, &from, &ev.ToStage, &secs, &ev.Reason, &code, &actor, &actorName, &at); e != nil {
return translatePG(e)
}
ev.FromStage, ev.DispositionCode = str(from), str(code)
ev.ActorUserID, ev.ActorName = str(actor), str(actorName)
ev.FromStageLabel, ev.ToStageLabel = stageLabels[ev.FromStage], stageLabels[ev.ToStage]
ev.OccurredAt = ts(at)
if secs != nil {
if d, err := decimal.NewFromString(*secs); err == nil {
ev.DaysInFrom = days(d)
}
}
out.Events = append(out.Events, ev)
}
return rows.Err()
})
if err != nil {
return ApplicationDetail{}, err
}
return out, nil
}
// ListApplications returns a candidate's applications across every job order, or a stage filtered list.
func (s *Service) ListApplications(ctx context.Context, candidateID, stage string, limit, offset int) (ApplicationList, error) {
if s.db == nil {
return ApplicationList{}, ErrNoDatabase
}
if limit <= 0 || limit > 200 {
limit = 50
}
if offset < 0 {
offset = 0
}
where := []string{"true"}
var args []any
if candidateID != "" {
id, err := uuid.Parse(candidateID)
if err != nil {
return ApplicationList{}, &ValidationError{Field: "candidate_id", Message: "must be a candidate id"}
}
args = append(args, id)
where = append(where, "a.candidate_id = $"+itoa(len(args)))
}
switch stage {
case "":
case "active":
where = append(where, "a.stage NOT IN ('rejected','withdrawn')")
default:
if !isStage(stage) {
return ApplicationList{}, &ValidationError{Field: "stage", Message: "is not a permitted stage"}
}
args = append(args, stage)
where = append(where, "a.stage = $"+itoa(len(args)))
}
cond := strings.Join(where, " AND ")
out := ApplicationList{Applications: []Application{}, Limit: limit, Offset: offset}
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
if e := tx.QueryRow(ctx, `SELECT count(*)`+appFrom+` WHERE `+cond, args...).Scan(&out.Total); e != nil {
return translatePG(e)
}
rows, e := tx.Query(ctx, `SELECT`+appSelect+appFrom+` WHERE `+cond+`
ORDER BY a.stage_changed_at DESC, a.id ASC
LIMIT $`+itoa(len(args)+1)+` OFFSET $`+itoa(len(args)+2), append(args, limit, offset)...)
if e != nil {
return translatePG(e)
}
defer rows.Close()
now := time.Now().UTC()
for rows.Next() {
a, e := scanApplication(rows, now)
if e != nil {
return translatePG(e)
}
out.Applications = append(out.Applications, a)
}
return rows.Err()
})
if err != nil {
return ApplicationList{}, err
}
return out, nil
}
func (s *Service) applicationRow(ctx context.Context, id uuid.UUID) (Application, error) {
var out Application
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
a, e := scanApplication(tx.QueryRow(ctx, `SELECT`+appSelect+appFrom+` WHERE a.id = $1`, id), time.Now().UTC())
if e != nil {
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
return translatePG(e)
}
out = a
return nil
})
return out, err
}