Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/opportunities/promotion_service.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/opportunities/promotion_service.go |
| Original SHA-256 | 1bfed2110ba9f28ea674fdc5126177fa5bd574a7ab8cf1769df1cad68dce5d33 |
| Git blob SHA | e7e9a70967d09f3bfc8c3a0f6517b61a99890096 |
| Reading-copy SHA-256 | 1bfed2110ba9f28ea674fdc5126177fa5bd574a7ab8cf1769df1cad68dce5d33 |
Recovered source transcript
Download complete source text ↓207 lines · 8.6 KB
package opportunities
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/missionworkforce/missionmatch/internal/platform/audit"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// ErrDraftRejected is returned when a promotion is attempted on a draft a reviewer already refused. A
// rejected draft is a decision, not a queue item, so it is never silently revived by a later approval.
var ErrDraftRejected = errors.New("opportunities: this requirement draft was rejected and cannot be promoted")
// PromotedRequirement is what an approval produced: the live requirement id, and whether THIS call is the
// one that created it.
type PromotedRequirement struct {
RequirementID string `json:"requirement_id"`
DraftID string `json:"draft_id"`
AlreadyDone bool `json:"already_promoted"`
}
// ApproveDraft is the ONE path by which anything the machine derived becomes visible to the scoring engine.
//
// It is the most safety critical write in the opportunity pipeline, so it is deliberately narrow:
//
// 1. It takes FOR UPDATE on the draft row FIRST. Eight racing approvals therefore serialize on that lock
// and exactly one creates the requirement; the other seven read the winner's result and return it with
// AlreadyDone set. This is the same lock discipline as the nine existing recruiting locks, and it is why
// a double click or two reviewers pressing approve at once cannot produce two live requirements.
// 2. It records the NAMED HUMAN who approved. The schema refuses an approved row without a reviewer, so an
// automated caller cannot satisfy this write at all; the AEDT human in the loop rule is structural.
// 3. It writes the requirement and flips the draft in the SAME transaction, so the draft can never claim an
// approval whose requirement was rolled back, nor a requirement exist that no draft accounts for.
// 4. It resolves skills against the EXISTING ontology only. A solicitation routinely names a skill the
// ontology does not carry; those are left unlinked rather than inserted, because writing the global
// ontology is superuser curation (curation.curate), never a side effect of approving one requirement.
func (s *Service) ApproveDraft(ctx context.Context, draftID string) (PromotedRequirement, error) {
if !s.db.Ready() {
return PromotedRequirement{}, ErrNoDatabase
}
did, err := uuid.Parse(strings.TrimSpace(draftID))
if err != nil {
return PromotedRequirement{}, fmt.Errorf("%w: draft id must be a uuid", ErrInvalidInput)
}
actor := actorPtr(ctx)
if actor == nil {
return PromotedRequirement{}, tenant.ErrNoTenant
}
var out PromotedRequirement
out.DraftID = did.String()
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
// THE LOCK. Every racing approval of this draft queues here.
var status string
var existing *string
var laborCat, clearance, poly, workLoc string
var years int
var remote bool
var skills []string
var mandatory []bool
var oppTitle string
err := tx.QueryRow(ctx, `
SELECT d.review_status, d.approved_requirement_id::text, d.labor_category, d.clearance_level,
d.polygraph, d.work_location, d.minimum_years_experience, d.remote_allowed,
d.required_skills, d.mandatory_skill_flags, COALESCE(t.title,'')
FROM requirement_drafts d
JOIN tracked_opportunities t ON t.organization_id = d.organization_id AND t.id = d.opportunity_id
WHERE d.id = $1
FOR UPDATE OF d`, did).
Scan(&status, &existing, &laborCat, &clearance, &poly, &workLoc, &years, &remote, &skills, &mandatory, &oppTitle)
if err != nil {
return err
}
// The losers of the race land here: the winner already promoted, so return its result rather than
// creating a second requirement. Idempotent by construction, not by retry.
if status == "approved" && existing != nil {
out.RequirementID = *existing
out.AlreadyDone = true
return nil
}
if status == "rejected" {
return ErrDraftRejected
}
title := strings.TrimSpace(laborCat)
if title == "" {
title = "Requirement from " + oppTitle
}
var reqID uuid.UUID
if err := tx.QueryRow(ctx, `
INSERT INTO requirements (organization_id, title, required_level, required_poly, required_years,
work_location, remote_allowed)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid, $1, $2, $3, $4, $5, $6)
RETURNING id`,
title, clearance, poly, years, workLoc, remote).Scan(&reqID); err != nil {
return fmt.Errorf("create requirement: %w", err)
}
// Link only the skills the ontology already knows. Unknown skill names stay on the draft as the record
// of what the solicitation asked for; they are a curation input, never an automatic ontology write.
if len(skills) > 0 {
// unnest pairs each skill with its flag positionally, so the mandatory bit lands on the skill it
// belongs to. The schema already guarantees the arrays are either equal length or the flags are
// absent; padFlags makes the absent case explicit rather than relying on that guarantee holding at
// the SQL layer, because a length mismatch here would mark the WRONG skill mandatory and a mandatory
// skill drives a band cap in the frozen scoring engine.
if _, err := tx.Exec(ctx, `
INSERT INTO required_skills (organization_id, requirement_id, skill_id, mandatory)
SELECT NULLIF(current_setting('app.current_org_id', true), '')::uuid, $1, sk.id, s.mand
FROM unnest($2::text[], $3::boolean[]) AS s(nm, mand)
JOIN skills sk ON lower(sk.name) = s.nm
ON CONFLICT DO NOTHING`,
reqID, lowerAll(skills), padFlags(mandatory, len(skills))); err != nil {
return fmt.Errorf("link required skills: %w", err)
}
}
if _, err := tx.Exec(ctx, `
UPDATE requirement_drafts
SET review_status = 'approved', approved_requirement_id = $1,
reviewed_by_user_id = $2, reviewed_at = now(), updated_at = now()
WHERE id = $3`, reqID, actor, did); err != nil {
return fmt.Errorf("mark draft approved: %w", err)
}
out.RequirementID = reqID.String()
nv, _ := json.Marshal(map[string]any{
"draft_id": did.String(), "requirement_id": reqID.String(), "labor_category": laborCat,
"clearance_level": clearance, "skills_named": len(skills),
})
return s.aud.OnTx(ctx, tx, audit.Event{
Action: "requirement.promoted", ActorUserID: actor,
EntityType: "requirement", EntityID: reqID.String(), NewValues: nv,
})
})
if err != nil {
return PromotedRequirement{}, err
}
return out, nil
}
// RejectDraft records a reviewer's refusal. A rejected draft keeps its contents as the record of what was
// refused and by whom, so a later reader can see the decision rather than an absence.
func (s *Service) RejectDraft(ctx context.Context, draftID string) error {
if !s.db.Ready() {
return ErrNoDatabase
}
did, err := uuid.Parse(strings.TrimSpace(draftID))
if err != nil {
return fmt.Errorf("%w: draft id must be a uuid", ErrInvalidInput)
}
actor := actorPtr(ctx)
if actor == nil {
return tenant.ErrNoTenant
}
return s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var status string
if err := tx.QueryRow(ctx, `SELECT review_status FROM requirement_drafts WHERE id = $1 FOR UPDATE`, did).Scan(&status); err != nil {
return err
}
if status == "approved" {
// An approved draft already produced a live requirement. Rejecting it here would leave the
// requirement in place with no draft accounting for it, so the refusal is explicit.
return fmt.Errorf("%w: this draft was already approved and promoted", ErrInvalidInput)
}
if _, err := tx.Exec(ctx, `
UPDATE requirement_drafts
SET review_status = 'rejected', reviewed_by_user_id = $1, reviewed_at = now(), updated_at = now()
WHERE id = $2`, actor, did); err != nil {
return err
}
nv, _ := json.Marshal(map[string]any{"draft_id": did.String()})
return s.aud.OnTx(ctx, tx, audit.Event{
Action: "requirement.draft.rejected", ActorUserID: actor,
EntityType: "requirement_draft", EntityID: did.String(), NewValues: nv,
})
})
}
func lowerAll(in []string) []string {
out := make([]string, len(in))
for i, v := range in {
out[i] = strings.ToLower(strings.TrimSpace(v))
}
return out
}
// padFlags returns a mandatory flag per skill. A draft may legitimately carry no flags at all (nothing was
// marked mandatory), and unnest over two arrays of different lengths would misalign the pairs, so the short
// case is padded with false rather than left to chance.
func padFlags(flags []bool, n int) []bool {
if len(flags) == n {
return flags
}
out := make([]bool, n)
copy(out, flags)
return out
}