//go:build integration package integration import ( "context" "sync" "testing" "github.com/google/uuid" "github.com/missionworkforce/missionmatch/internal/modules/recruiting" ) // WARNING. These tests create organizations, users, candidates, job orders and placements. Point // MM_APP_DSN and MM_MIGRATOR_DSN at a scratch or staging database, never at production. // newAssignment returns a service, a context, and n applications already on a requisition with `seats`. func newAssignment(t *testing.T, seats, n int) (*recruiting.Service, context.Context, uuid.UUID, []uuid.UUID) { t.Helper() svc, ctx, req, cands := newPipeline(t, seats, n) apps := make([]uuid.UUID, n) for i, c := range cands { a, err := svc.Submit(ctx, req, recruiting.CreateApplicationInput{CandidateID: c.String()}) if err != nil { t.Fatalf("submit %d: %v", i, err) } apps[i] = uuid.MustParse(a.ID) } return svc, ctx, req, apps } // TestOfferToPlacementWalk is the whole revenue path: extend, accept, place, end. func TestOfferToPlacementWalk(t *testing.T) { svc, ctx, _, apps := newAssignment(t, 1, 1) app := apps[0] iv, err := svc.ScheduleInterview(ctx, app, recruiting.CreateInterviewInput{ Round: 1, Kind: "client", ScheduledAt: "2026-08-01T14:00:00Z", Interviewer: "Dana Whitfield"}) if err != nil { t.Fatalf("schedule interview: %v", err) } if iv.Outcome != "scheduled" { t.Errorf("new interview outcome = %q, want scheduled", iv.Outcome) } if _, err := svc.UpdateInterview(ctx, uuid.MustParse(iv.ID), recruiting.UpdateInterviewInput{ ExpectedUpdatedAt: iv.UpdatedAt, Outcome: strp("passed"), CompletedAt: strp("2026-08-01T15:00:00Z")}); err != nil { t.Fatalf("complete interview: %v", err) } o, err := svc.ExtendOffer(ctx, app, recruiting.CreateOfferInput{ EmploymentType: "contract", BillRate: "165.0000", PayRate: "110.0000"}) if err != nil { t.Fatalf("extend offer: %v", err) } // Margin must be exact: 165 minus 110 is 55, which is 33.3333 percent of 165. if o.Margin.GrossPerHour == nil || o.Margin.GrossPerHour.String() != "55" { t.Errorf("gross margin = %v, want 55", o.Margin.GrossPerHour) } if o.Margin.Pct == nil || o.Margin.Pct.String() != "33.3333" { t.Errorf("margin pct = %v, want 33.3333", o.Margin.Pct) } if len(o.NextStatuses) == 0 { t.Error("an extended offer offers no next statuses") } o, err = svc.RespondToOffer(ctx, uuid.MustParse(o.ID), recruiting.OfferStatusInput{ ExpectedStatus: "extended", Status: "accepted"}) if err != nil { t.Fatalf("accept: %v", err) } if o.RespondedAt == "" { t.Error("an accepted offer carries no responded_at") } p, err := svc.CreatePlacement(ctx, app, recruiting.CreatePlacementInput{ OfferID: o.ID, EmploymentType: "contract", StartDate: "2026-09-01", BillRate: "165.0000", PayRate: "110.0000", GuaranteeDays: intp(90)}) if err != nil { t.Fatalf("create placement: %v", err) } if !p.Live || p.Status != "pending_start" { t.Errorf("new placement is status %q live=%v", p.Status, p.Live) } if p.Guarantee.EndsOn != "2026-11-29" { // 1 Sep plus 89 days t.Errorf("guarantee ends %s, want 2026-11-29", p.Guarantee.EndsOn) } // Creating the placement must have moved the application to placed, in the same transaction. d, err := svc.GetApplication(ctx, app) if err != nil { t.Fatalf("get application: %v", err) } if d.Stage != "placed" { t.Fatalf("after placing, the application is at %q, want placed", d.Stage) } // Ending inside the guarantee as a fall off must breach; the application leaves the pipeline. p, err = svc.EndPlacement(ctx, uuid.MustParse(p.ID), recruiting.EndPlacementInput{ Status: "fell_off", EndDate: "2026-10-01", DispositionCode: "candidate_withdrew", Reason: "Took a permanent role elsewhere"}) if err != nil { t.Fatalf("end placement: %v", err) } if p.Live { t.Error("an ended placement still reports as live") } if !p.Guarantee.Breached { t.Error("falling off inside the guarantee did not breach it") } d, _ = svc.GetApplication(ctx, app) if d.Stage != "withdrawn" { t.Errorf("after the fall off the application is at %q, want withdrawn", d.Stage) } // The high water mark must still record that this person was placed. if d.FurthestStage != "placed" { t.Errorf("furthest stage = %q, want placed", d.FurthestStage) } } func strp(s string) *string { return &s } func intp(i int) *int { return &i } // TestPlacementRequiresAndHoldsTheStage. A live placement and stage placed are one fact. Neither may // exist without the other, and the database refuses both directions. func TestPlacementRequiresAndHoldsTheStage(t *testing.T) { svc, ctx, _, apps := newAssignment(t, 1, 1) app := apps[0] p, err := svc.CreatePlacement(ctx, app, recruiting.CreatePlacementInput{ EmploymentType: "contract", StartDate: "2026-09-01", BillRate: "150", PayRate: "100"}) if err != nil { t.Fatalf("create placement: %v", err) } // Moving the application off placed while the assignment is live must be refused, naming the fix. _, err = svc.MoveStage(ctx, app, recruiting.MoveStageInput{ ExpectedStage: "placed", ToStage: "rejected", DispositionCode: "client_declined"}) if err == nil { t.Fatal("the application left stage placed while a live placement stood") } // Once ended, the move is allowed. if _, err := svc.EndPlacement(ctx, uuid.MustParse(p.ID), recruiting.EndPlacementInput{ Status: "completed", EndDate: "2027-01-01"}); err != nil { t.Fatalf("end placement: %v", err) } if _, err := svc.MoveStage(ctx, app, recruiting.MoveStageInput{ ExpectedStage: "placed", ToStage: "rejected", DispositionCode: "client_declined"}); err != nil { t.Errorf("after ending the assignment the move was still refused: %v", err) } } // TestPlacementRespectsTheSeatCap. Creating a placement moves the stage, so it inherits the seat cap and // a full requisition refuses the placement before any money is recorded. func TestPlacementRespectsTheSeatCap(t *testing.T) { svc, ctx, _, apps := newAssignment(t, 1, 2) if _, err := svc.CreatePlacement(ctx, apps[0], recruiting.CreatePlacementInput{ EmploymentType: "contract", StartDate: "2026-09-01", BillRate: "150", PayRate: "100"}); err != nil { t.Fatalf("first placement: %v", err) } if _, err := svc.CreatePlacement(ctx, apps[1], recruiting.CreatePlacementInput{ EmploymentType: "contract", StartDate: "2026-09-01", BillRate: "150", PayRate: "100"}); err == nil { t.Fatal("a second placement was created against a requisition with one opening") } } // TestOnlyOneAcceptedOfferPerApplication. Two accepted offers means two contradictory sets of agreed // terms with nothing to say which one is the revenue. func TestOnlyOneAcceptedOfferPerApplication(t *testing.T) { svc, ctx, _, apps := newAssignment(t, 1, 1) app := apps[0] o1, err := svc.ExtendOffer(ctx, app, recruiting.CreateOfferInput{BillRate: "150", PayRate: "100"}) if err != nil { t.Fatalf("first offer: %v", err) } // A second LIVE offer is refused outright. if _, err := svc.ExtendOffer(ctx, app, recruiting.CreateOfferInput{BillRate: "160", PayRate: "100"}); err == nil { t.Fatal("a second live offer was extended") } if _, err := svc.RespondToOffer(ctx, uuid.MustParse(o1.ID), recruiting.OfferStatusInput{ ExpectedStatus: "extended", Status: "accepted"}); err != nil { t.Fatalf("accept: %v", err) } // Now that the first is accepted a new one may be extended, but it cannot also be accepted. o2, err := svc.ExtendOffer(ctx, app, recruiting.CreateOfferInput{BillRate: "170", PayRate: "100"}) if err != nil { t.Fatalf("second offer after acceptance: %v", err) } if _, err := svc.RespondToOffer(ctx, uuid.MustParse(o2.ID), recruiting.OfferStatusInput{ ExpectedStatus: "extended", Status: "accepted"}); err == nil { t.Fatal("a second offer was accepted while another acceptance stood") } } // TestOfferLifecycleIsEnforced. A declined offer cannot later be accepted; re offering is a new offer so // the history keeps both. func TestOfferLifecycleIsEnforced(t *testing.T) { svc, ctx, _, apps := newAssignment(t, 1, 1) o, err := svc.ExtendOffer(ctx, apps[0], recruiting.CreateOfferInput{BillRate: "150", PayRate: "100"}) if err != nil { t.Fatalf("extend: %v", err) } id := uuid.MustParse(o.ID) if _, err := svc.RespondToOffer(ctx, id, recruiting.OfferStatusInput{ ExpectedStatus: "extended", Status: "declined", DeclineReason: "rate too low"}); err != nil { t.Fatalf("decline: %v", err) } if _, err := svc.RespondToOffer(ctx, id, recruiting.OfferStatusInput{Status: "accepted"}); err == nil { t.Fatal("a declined offer was accepted after the fact") } // An accepted offer may still be rescinded, because a client pulling a role after acceptance is real. o2, _ := svc.ExtendOffer(ctx, apps[0], recruiting.CreateOfferInput{BillRate: "160", PayRate: "100"}) id2 := uuid.MustParse(o2.ID) if _, err := svc.RespondToOffer(ctx, id2, recruiting.OfferStatusInput{ ExpectedStatus: "extended", Status: "accepted"}); err != nil { t.Fatalf("accept: %v", err) } if _, err := svc.RespondToOffer(ctx, id2, recruiting.OfferStatusInput{ ExpectedStatus: "accepted", Status: "rescinded"}); err != nil { t.Errorf("an accepted offer could not be rescinded: %v", err) } } // TestPlacementCannotBorrowAnotherCandidatesOffer. Reproduced against 0048 before the fix: a placement // carrying an offer extended to a different person, so the rates and the money came from someone else. func TestPlacementCannotBorrowAnotherCandidatesOffer(t *testing.T) { svc, ctx, _, apps := newAssignment(t, 2, 2) other, err := svc.ExtendOffer(ctx, apps[1], recruiting.CreateOfferInput{BillRate: "200", PayRate: "120"}) if err != nil { t.Fatalf("offer to the second candidate: %v", err) } if _, err := svc.CreatePlacement(ctx, apps[0], recruiting.CreatePlacementInput{ OfferID: other.ID, EmploymentType: "contract", StartDate: "2026-09-01", BillRate: "150", PayRate: "100"}); err == nil { t.Fatal("a placement was created carrying another candidate's offer") } // Control: the candidate's own offer is accepted. own, _ := svc.ExtendOffer(ctx, apps[0], recruiting.CreateOfferInput{BillRate: "150", PayRate: "100"}) if _, err := svc.CreatePlacement(ctx, apps[0], recruiting.CreatePlacementInput{ OfferID: own.ID, EmploymentType: "contract", StartDate: "2026-09-01", BillRate: "150", PayRate: "100"}); err != nil { t.Errorf("a placement with the candidate's own offer was refused: %v", err) } } // TestConcurrentPlacementsIntoOneSeat. Eight recruiters placing eight different candidates into a single // seat at the same instant: exactly one may win, and no money is recorded for the losers. func TestConcurrentPlacementsIntoOneSeat(t *testing.T) { svc, ctx, _, apps := newAssignment(t, 1, 8) var wg sync.WaitGroup errs := make([]error, len(apps)) start := make(chan struct{}) for i, app := range apps { wg.Add(1) go func(i int, app uuid.UUID) { defer wg.Done() <-start _, errs[i] = svc.CreatePlacement(ctx, app, recruiting.CreatePlacementInput{ EmploymentType: "contract", StartDate: "2026-09-01", BillRate: "150", PayRate: "100"}) }(i, app) } close(start) wg.Wait() won := 0 for _, e := range errs { if e == nil { won++ } } if won != 1 { t.Fatalf("%d of %d concurrent placements into one seat succeeded, want exactly 1", won, len(apps)) } list, err := svc.ListPlacements(ctx, "live", 100, 0) if err != nil { t.Fatalf("list: %v", err) } if list.Total != 1 { t.Errorf("%d live placements recorded, want 1", list.Total) } } // TestDirectHireCarriesAFeeNotASpread. The two revenue shapes must never be blended. func TestDirectHireCarriesAFeeNotASpread(t *testing.T) { svc, ctx, _, apps := newAssignment(t, 1, 1) p, err := svc.CreatePlacement(ctx, apps[0], recruiting.CreatePlacementInput{ EmploymentType: "direct_hire", StartDate: "2026-09-01", Salary: "150000.00", FeeAmount: "30000.00"}) if err != nil { t.Fatalf("direct hire placement: %v", err) } if p.Margin.Fee == nil || p.Margin.Fee.String() != "30000" { t.Errorf("fee = %v, want 30000", p.Margin.Fee) } if p.Margin.GrossPerHour != nil || p.Margin.Pct != nil { t.Error("a direct hire reported an hourly margin") } // A contract placement with no rates is refused rather than recorded as earning nothing. if _, err := svc.CreatePlacement(ctx, apps[0], recruiting.CreatePlacementInput{ EmploymentType: "contract", StartDate: "2026-09-01"}); err == nil { t.Error("a contract placement with no rates was accepted") } } // TestAssignmentReadsEverythingAtOnce. func TestAssignmentReadsEverythingAtOnce(t *testing.T) { svc, ctx, _, apps := newAssignment(t, 1, 1) app := apps[0] if _, err := svc.ScheduleInterview(ctx, app, recruiting.CreateInterviewInput{Round: 1}); err != nil { t.Fatalf("interview: %v", err) } if _, err := svc.ScheduleInterview(ctx, app, recruiting.CreateInterviewInput{Round: 2, Kind: "panel"}); err != nil { t.Fatalf("interview 2: %v", err) } // The same round twice is refused, so two recruiters cannot both book "round 2". if _, err := svc.ScheduleInterview(ctx, app, recruiting.CreateInterviewInput{Round: 2}); err == nil { t.Error("the same interview round was booked twice") } if _, err := svc.ExtendOffer(ctx, app, recruiting.CreateOfferInput{BillRate: "150", PayRate: "100"}); err != nil { t.Fatalf("offer: %v", err) } if _, err := svc.CreatePlacement(ctx, app, recruiting.CreatePlacementInput{ EmploymentType: "contract", StartDate: "2026-09-01", BillRate: "150", PayRate: "100"}); err != nil { t.Fatalf("placement: %v", err) } a, err := svc.GetAssignment(ctx, app) if err != nil { t.Fatalf("assignment: %v", err) } if len(a.Interviews) != 2 || len(a.Offers) != 1 || len(a.Placements) != 1 { t.Errorf("assignment has %d interviews, %d offers, %d placements, want 2, 1, 1", len(a.Interviews), len(a.Offers), len(a.Placements)) } if a.Stage != "placed" { t.Errorf("stage = %q, want placed", a.Stage) } } // TestAssignmentTenantIsolation, with a control. func TestAssignmentTenantIsolation(t *testing.T) { svcA, ctxA, _, appsA := newAssignment(t, 1, 1) o, err := svcA.ExtendOffer(ctxA, appsA[0], recruiting.CreateOfferInput{BillRate: "150", PayRate: "100"}) if err != nil { t.Fatalf("offer in A: %v", err) } _, ctxB, _, _ := newAssignment(t, 1, 1) if _, err := svcA.GetAssignment(ctxB, appsA[0]); err == nil { t.Fatal("tenant B read tenant A's assignment") } if _, err := svcA.RespondToOffer(ctxB, uuid.MustParse(o.ID), recruiting.OfferStatusInput{ ExpectedStatus: "extended", Status: "accepted"}); err == nil { t.Fatal("tenant B accepted tenant A's offer") } listB, err := svcA.ListPlacements(ctxB, "", 100, 0) if err != nil { t.Fatalf("list in B: %v", err) } if listB.Total != 0 { t.Errorf("tenant B sees %d placements from another tenant", listB.Total) } // Control: A still sees its own offer. a, err := svcA.GetAssignment(ctxA, appsA[0]) if err != nil || len(a.Offers) != 1 { t.Fatalf("tenant A sees %d of its own offers, want 1 (err=%v)", len(a.Offers), err) } }