// Package bidoutcomes is the M7 bid outcomes ledger: the table, the recording path, and the one advisory // base rate read live behind it (the advisory read itself is the bid_win_rate_by_segment intent in the AI // Registry). It records what a bid was for (an opportunity, a requirement, a slate, each optional), when the // proposal was submitted, the win or loss result once the award lands, who won on a loss, and debrief notes. // It is the single most reused unlock on the AI roadmap (win probability, confidence calibration, and win // pattern learning all depend on it), so it ships DORMANT and flag gated (MM_BID_OUTCOMES_ENABLED, default // off) NOW: real win and loss labels start accruing the day it lands while no learned model consumes it yet. // Nothing here is learned, nothing here is a candidate score; the frozen matching engine stays the only // scorer. package bidoutcomes // ResultPending, ResultWin, ResultLoss, and ResultNoBid are the four allowed values of a bid outcome // result, mirroring the CHECK constraint on bid_outcomes.result. A recorded outcome defaults to pending // until the award decision lands. const ( ResultPending = "pending" ResultWin = "win" ResultLoss = "loss" ResultNoBid = "no_bid" ) // RecordInput is the request to record (or upsert) a bid outcome. Every link is optional because not every // bid links all three; the result defaults to pending when empty. The notes carry debrief text and are // never copied into the audit event (no PII or sensitive debrief detail in the audit trail). type RecordInput struct { ID string `json:"id,omitempty"` // when set, upsert this existing outcome rather than insert a new one OpportunityID string `json:"opportunity_id,omitempty"` // the opportunity the bid was for; empty when not linked RequirementID string `json:"requirement_id,omitempty"` // the requirement the bid was for; empty when not linked SlateID string `json:"slate_id,omitempty"` // the slate the bid was staffed from; empty when not linked SubmittedAt string `json:"submitted_at,omitempty"` // RFC3339 timestamp the proposal was submitted; empty until submitted Result string `json:"result,omitempty"` // win, loss, no_bid, or pending (the default) AwardDate string `json:"award_date,omitempty"` // RFC3339 date the award decision landed; empty until known AwardeeUEI string `json:"awardee_uei,omitempty"` // who won, recorded on a loss Notes string `json:"notes,omitempty"` // debrief notes; NEVER copied into the audit event } // UpdateInput is the request to update the result and award fields of a recorded outcome once they are // known. The id comes from the route path, not the body. type UpdateInput struct { Result string `json:"result,omitempty"` // win, loss, no_bid, or pending SubmittedAt string `json:"submitted_at,omitempty"` // RFC3339 timestamp; empty leaves it unchanged AwardDate string `json:"award_date,omitempty"` // RFC3339 date; empty leaves it unchanged AwardeeUEI string `json:"awardee_uei,omitempty"` // who won, recorded on a loss Notes string `json:"notes,omitempty"` // debrief notes; NEVER copied into the audit event } // BidOutcome is the recorded ledger row returned to the caller. type BidOutcome struct { ID string `json:"id"` OpportunityID string `json:"opportunity_id,omitempty"` RequirementID string `json:"requirement_id,omitempty"` SlateID string `json:"slate_id,omitempty"` SubmittedAt string `json:"submitted_at,omitempty"` Result string `json:"result"` AwardDate string `json:"award_date,omitempty"` AwardeeUEI string `json:"awardee_uei,omitempty"` Notes string `json:"notes,omitempty"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // validResults is the closed set of allowed result values, the same set the DB CHECK enforces. The service // validates against it before the write so a bad value is a clean 422, never a raw constraint violation. var validResults = map[string]bool{ ResultPending: true, ResultWin: true, ResultLoss: true, ResultNoBid: true, } // IsValidResult reports whether r is one of the four allowed result values. func IsValidResult(r string) bool { return validResults[r] }