Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/opportunities/teaming.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/opportunities/teaming.go |
| Original SHA-256 | a4bdafebf1d51d63069bf006e7d2d2ccd079f9e08042e1b259329e89e43c1428 |
| Git blob SHA | 0bc2059fdb1b9781993d71b285ea9d56d84360ad |
| Reading-copy SHA-256 | a4bdafebf1d51d63069bf006e7d2d2ccd079f9e08042e1b259329e89e43c1428 |
Recovered source transcript
Download complete source text ↓467 lines · 19.8 KB
package opportunities
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/missionworkforce/missionmatch/internal/platform/audit"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// TEAMING PARTNERS AND KEY PERSONNEL COMMITMENTS.
//
// It lives in the capture module because that is where the relationship is formed. A teaming agreement is
// negotiated while the pursuit is being shaped, months before a proposal row exists, which is precisely the
// window the rest of the market does not cover: the ERPs handle partners after award and the capture CRMs
// drop them at proposal time.
var (
// ErrPartnerExists is the unique constraint on one company per pursuit, surfaced as a clean conflict
// rather than a raw constraint violation, because adding the same partner twice is an ordinary mistake.
ErrPartnerExists = errors.New("opportunities: that company is already on this pursuit")
// ErrCommitmentNeedsPerson is the CHECK that a commitment names exactly one person.
ErrCommitmentNeedsPerson = errors.New(
"a commitment must name either one of your candidates or a partner plus the person's name")
// ErrSignedNeedsDate is the CHECK that a signed thing carries the date it was signed.
ErrSignedNeedsDate = errors.New("a signed agreement or commitment must carry the date it was signed")
// ErrRequirementMissing is a commitment against a requirement that does not exist or was deleted.
ErrRequirementMissing = errors.New("that position does not exist")
// ErrNotFound is an update that matched nothing in the caller's org, which under RLS is the same answer
// as a row belonging to somebody else: not found, never "forbidden", because the difference would
// confirm the row exists.
ErrNotFound = errors.New("opportunities: not found")
)
// TeamingPartner is one company on one pursuit.
type TeamingPartner struct {
ID string `json:"id"`
OpportunityID string `json:"opportunity_id"`
CompanyName string `json:"company_name"`
PartnerRole string `json:"partner_role"`
Status string `json:"status"`
UEI string `json:"uei,omitempty"`
CageCode string `json:"cage_code,omitempty"`
SetAsides []string `json:"set_asides"`
ScopeSummary string `json:"scope_summary,omitempty"`
AgreementSignedOn string `json:"agreement_signed_on,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
// Commitments is how many key personnel this partner is providing, so a bid review can see at a glance
// which partners carry evaluated people and which carry only scope.
Commitments int `json:"commitments"`
}
// PartnerInput is the write shape.
type PartnerInput struct {
CompanyName string `json:"company_name"`
PartnerRole string `json:"partner_role"`
Status string `json:"status"`
UEI string `json:"uei"`
CageCode string `json:"cage_code"`
SetAsides []string `json:"set_asides"`
ScopeSummary string `json:"scope_summary"`
AgreementSignedOn string `json:"agreement_signed_on"`
}
// KeyPersonnelCommitment is one named person held to one evaluated position.
type KeyPersonnelCommitment struct {
ID string `json:"id"`
RequirementID string `json:"requirement_id"`
RequirementTitle string `json:"requirement_title,omitempty"`
CandidateID string `json:"candidate_id,omitempty"`
CandidateName string `json:"candidate_name,omitempty"`
TeamingPartnerID string `json:"teaming_partner_id,omitempty"`
PartnerCompany string `json:"partner_company,omitempty"`
PartnerPersonName string `json:"partner_person_name,omitempty"`
RoleTitle string `json:"role_title,omitempty"`
CommitmentType string `json:"commitment_type"`
Status string `json:"status"`
SignedOn string `json:"signed_on,omitempty"`
ExpiresOn string `json:"expires_on,omitempty"`
Notes string `json:"notes,omitempty"`
// DaysUntilExpiry is negative once lapsed. Absent when no expiry was recorded, which is a different and
// honest state from an expiry far away.
DaysUntilExpiry *int `json:"days_until_expiry,omitempty"`
// Lapsed says the commitment has passed its expiry, which is the fact a bid review most needs and the
// one nobody notices: a letter of intent signed for a bid that slipped six months is not a commitment.
Lapsed bool `json:"lapsed,omitempty"`
}
// CommitmentInput is the write shape.
type CommitmentInput struct {
RequirementID string `json:"requirement_id"`
CandidateID string `json:"candidate_id"`
TeamingPartnerID string `json:"teaming_partner_id"`
PartnerPersonName string `json:"partner_person_name"`
RoleTitle string `json:"role_title"`
CommitmentType string `json:"commitment_type"`
Status string `json:"status"`
SignedOn string `json:"signed_on"`
ExpiresOn string `json:"expires_on"`
Notes string `json:"notes"`
}
// ListPartners returns the partners on one pursuit, with how many evaluated people each is providing.
func (s *Service) ListPartners(ctx context.Context, oppID uuid.UUID) ([]TeamingPartner, error) {
out := make([]TeamingPartner, 0, 16)
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
rows, e := tx.Query(ctx, `
SELECT p.id::text, p.opportunity_id::text, p.company_name, p.partner_role, p.status,
p.uei, p.cage_code, p.set_asides, p.scope_summary,
p.agreement_signed_on, p.created_at, p.updated_at,
(SELECT count(*)::int FROM key_personnel_commitments k
WHERE k.teaming_partner_id = p.id AND k.status <> 'withdrawn')
FROM teaming_partners p
WHERE p.opportunity_id = $1
ORDER BY CASE p.partner_role WHEN 'prime' THEN 0 WHEN 'joint_venture' THEN 1 ELSE 2 END,
p.company_name ASC`, oppID)
if e != nil {
return e
}
defer rows.Close()
for rows.Next() {
var p TeamingPartner
var signed, created, updated *time.Time
if e := rows.Scan(&p.ID, &p.OpportunityID, &p.CompanyName, &p.PartnerRole, &p.Status,
&p.UEI, &p.CageCode, &p.SetAsides, &p.ScopeSummary,
&signed, &created, &updated, &p.Commitments); e != nil {
return e
}
p.AgreementSignedOn, p.CreatedAt, p.UpdatedAt = dayStr(signed), tsStr(created), tsStr(updated)
out = append(out, p)
}
return rows.Err()
})
return out, err
}
// AddPartner records a company on a pursuit.
func (s *Service) AddPartner(ctx context.Context, oppID uuid.UUID, in PartnerInput) (string, error) {
in.CompanyName = strings.TrimSpace(in.CompanyName)
if in.CompanyName == "" {
return "", fmt.Errorf("%w: a partner needs a company name", ErrInvalidInput)
}
signed, err := optDay(in.AgreementSignedOn)
if err != nil {
return "", fmt.Errorf("%w: agreement_signed_on must be YYYY-MM-DD", ErrInvalidInput)
}
if in.SetAsides == nil {
in.SetAsides = []string{}
}
var id string
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
e := tx.QueryRow(ctx, `
INSERT INTO teaming_partners (organization_id, opportunity_id, company_name, partner_role, status,
uei, cage_code, set_asides, scope_summary, agreement_signed_on, created_by)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid, $1, $2,
coalesce(nullif($3,''),'subcontractor'), coalesce(nullif($4,''),'identified'),
$5, $6, $7, $8, $9, $10)
RETURNING id::text`,
oppID, in.CompanyName, in.PartnerRole, in.Status, in.UEI, in.CageCode, in.SetAsides,
in.ScopeSummary, signed, actorPtr(ctx)).Scan(&id)
if e != nil {
return translateTeaming(e)
}
return s.auditTeaming(ctx, tx, "capture.partner.added", id)
})
return id, err
}
// UpdatePartner changes status, terms or identifiers on an existing partner.
func (s *Service) UpdatePartner(ctx context.Context, id uuid.UUID, in PartnerInput) error {
signed, err := optDay(in.AgreementSignedOn)
if err != nil {
return fmt.Errorf("%w: agreement_signed_on must be YYYY-MM-DD", ErrInvalidInput)
}
return s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
tag, e := tx.Exec(ctx, `
UPDATE teaming_partners SET
partner_role = coalesce(nullif($2,''), partner_role),
status = coalesce(nullif($3,''), status),
uei = coalesce(nullif($4,''), uei),
cage_code = coalesce(nullif($5,''), cage_code),
scope_summary = coalesce(nullif($6,''), scope_summary),
agreement_signed_on = coalesce($7, agreement_signed_on),
updated_at = now()
WHERE id = $1`, id, in.PartnerRole, in.Status, in.UEI, in.CageCode, in.ScopeSummary, signed)
if e != nil {
return translateTeaming(e)
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return s.auditTeaming(ctx, tx, "capture.partner.updated", id.String())
})
}
// commitmentListLimit bounds both commitment lists.
//
// IT WAS UNBOUNDED AND THAT IS A DEFECT, NOT A MISSING NICETY. A stress run put twenty thousand commitments
// on one position and the position view returned a NINE MEGABYTE response in 1.4 seconds, to a panel that
// shows a handful of names. It is the same failure the alerts feed had at 7.9 megabytes, found the same way,
// which is the argument for bounding every list at the moment it is written rather than when somebody
// notices. A key personnel panel showing more than a few hundred names is already telling a story nobody can
// read.
const commitmentListLimit = 500
// ListCommitments returns the key personnel commitments on one pursuit, across every requirement it carries.
func (s *Service) ListCommitments(ctx context.Context, oppID uuid.UUID) ([]KeyPersonnelCommitment, error) {
out := make([]KeyPersonnelCommitment, 0, 16)
now := time.Now().UTC().Truncate(24 * time.Hour)
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
rows, e := tx.Query(ctx, `
SELECT k.id::text, k.requirement_id::text, coalesce(r.title,''),
coalesce(k.candidate_id::text,''), coalesce(c.full_name,''),
coalesce(k.teaming_partner_id::text,''), coalesce(p.company_name,''),
k.partner_person_name, k.role_title, k.commitment_type, k.status,
k.signed_on, k.expires_on, k.notes
FROM key_personnel_commitments k
JOIN requirements r ON r.organization_id = k.organization_id AND r.id = k.requirement_id
LEFT JOIN candidates c ON c.organization_id = k.organization_id AND c.id = k.candidate_id
LEFT JOIN teaming_partners p ON p.organization_id = k.organization_id AND p.id = k.teaming_partner_id
WHERE EXISTS (
SELECT 1
FROM proposal_staffing_links psl
JOIN proposals pr ON pr.organization_id = psl.organization_id AND pr.id = psl.proposal_id
WHERE psl.organization_id = k.organization_id
AND psl.requirement_id = k.requirement_id
AND pr.opportunity_id = $1)
ORDER BY r.title ASC, k.created_at ASC
LIMIT `+strconv.Itoa(commitmentListLimit), oppID)
if e != nil {
return e
}
defer rows.Close()
for rows.Next() {
var k KeyPersonnelCommitment
var signed, expires *time.Time
if e := rows.Scan(&k.ID, &k.RequirementID, &k.RequirementTitle, &k.CandidateID, &k.CandidateName,
&k.TeamingPartnerID, &k.PartnerCompany, &k.PartnerPersonName, &k.RoleTitle,
&k.CommitmentType, &k.Status, &signed, &expires, &k.Notes); e != nil {
return e
}
k.SignedOn, k.ExpiresOn = dayStr(signed), dayStr(expires)
if expires != nil {
d := int(expires.UTC().Truncate(24*time.Hour).Sub(now).Hours() / 24)
k.DaysUntilExpiry = &d
// LAPSED IS COMPUTED, NEVER STORED. A stored flag would be correct the day it was written and
// wrong every day after, and the whole value of this field is that it changes on its own.
k.Lapsed = d < 0 && (k.Status == "proposed" || k.Status == "committed")
}
out = append(out, k)
}
return rows.Err()
})
return out, err
}
// ListCommitmentsForRequirement returns the commitments on ONE position.
//
// IT EXISTS SO A COMMITMENT IS ALWAYS VISIBLE SOMEWHERE. The pursuit view joins through the proposal and its
// staffing links, which is the correct relationship and is also empty until a proposal exists and the
// position is staffed onto it. Without this route a commitment recorded early in capture would be written,
// return an identifier, and appear on no screen at all, which is the failure the first version of this
// feature actually shipped with.
func (s *Service) ListCommitmentsForRequirement(ctx context.Context, reqID uuid.UUID) ([]KeyPersonnelCommitment, error) {
out := make([]KeyPersonnelCommitment, 0, 8)
now := time.Now().UTC().Truncate(24 * time.Hour)
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
rows, e := tx.Query(ctx, `
SELECT k.id::text, k.requirement_id::text, coalesce(r.title,''),
coalesce(k.candidate_id::text,''), coalesce(c.full_name,''),
coalesce(k.teaming_partner_id::text,''), coalesce(p.company_name,''),
k.partner_person_name, k.role_title, k.commitment_type, k.status,
k.signed_on, k.expires_on, k.notes
FROM key_personnel_commitments k
JOIN requirements r ON r.organization_id = k.organization_id AND r.id = k.requirement_id
LEFT JOIN candidates c ON c.organization_id = k.organization_id AND c.id = k.candidate_id
LEFT JOIN teaming_partners p ON p.organization_id = k.organization_id AND p.id = k.teaming_partner_id
WHERE k.requirement_id = $1
ORDER BY k.created_at ASC
LIMIT `+strconv.Itoa(commitmentListLimit), reqID)
if e != nil {
return e
}
defer rows.Close()
for rows.Next() {
var k KeyPersonnelCommitment
var signed, expires *time.Time
if e := rows.Scan(&k.ID, &k.RequirementID, &k.RequirementTitle, &k.CandidateID, &k.CandidateName,
&k.TeamingPartnerID, &k.PartnerCompany, &k.PartnerPersonName, &k.RoleTitle,
&k.CommitmentType, &k.Status, &signed, &expires, &k.Notes); e != nil {
return e
}
k.SignedOn, k.ExpiresOn = dayStr(signed), dayStr(expires)
if expires != nil {
d := int(expires.UTC().Truncate(24*time.Hour).Sub(now).Hours() / 24)
k.DaysUntilExpiry = &d
k.Lapsed = d < 0 && (k.Status == "proposed" || k.Status == "committed")
}
out = append(out, k)
}
return rows.Err()
})
return out, err
}
// AddCommitment records a person held to an evaluated position.
func (s *Service) AddCommitment(ctx context.Context, in CommitmentInput) (string, error) {
reqID, err := uuid.Parse(strings.TrimSpace(in.RequirementID))
if err != nil {
return "", fmt.Errorf("%w: requirement_id is required", ErrInvalidInput)
}
signed, err := optDay(in.SignedOn)
if err != nil {
return "", fmt.Errorf("%w: signed_on must be YYYY-MM-DD", ErrInvalidInput)
}
expires, err := optDay(in.ExpiresOn)
if err != nil {
return "", fmt.Errorf("%w: expires_on must be YYYY-MM-DD", ErrInvalidInput)
}
var cand, partner *uuid.UUID
if v := strings.TrimSpace(in.CandidateID); v != "" {
id, perr := uuid.Parse(v)
if perr != nil {
return "", fmt.Errorf("%w: candidate_id is not an identifier", ErrInvalidInput)
}
cand = &id
}
if v := strings.TrimSpace(in.TeamingPartnerID); v != "" {
id, perr := uuid.Parse(v)
if perr != nil {
return "", fmt.Errorf("%w: teaming_partner_id is not an identifier", ErrInvalidInput)
}
partner = &id
}
var id string
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
// THE REQUIREMENT MUST EXIST, and that is all this can check.
//
// The first version of this guard tested requirements.opportunity_id, on the assumption that it
// pointed at the tracked opportunity the partners hang off. It does not. There are two opportunity
// tables: requirements reference `opportunities`, from migration 0002, while proposals and teaming
// partners reference `tracked_opportunities`, the capture spine from 0070. The bridge between a
// pursuit and a requirement runs through the proposal and its staffing links, which is why the list
// below joins that way. Guarding on the wrong column refused every legitimate commitment.
var exists bool
if e := tx.QueryRow(ctx,
`SELECT true FROM requirements WHERE id = $1 AND deleted_at IS NULL`, reqID).Scan(&exists); e != nil {
if errors.Is(e, pgx.ErrNoRows) {
return ErrRequirementMissing
}
return e
}
e := tx.QueryRow(ctx, `
INSERT INTO key_personnel_commitments (organization_id, requirement_id, candidate_id,
teaming_partner_id, partner_person_name, role_title, commitment_type, status,
signed_on, expires_on, notes, created_by)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid, $1, $2, $3, $4, $5,
coalesce(nullif($6,''),'letter_of_intent'), coalesce(nullif($7,''),'proposed'),
$8, $9, $10, $11)
RETURNING id::text`,
reqID, cand, partner, strings.TrimSpace(in.PartnerPersonName), in.RoleTitle,
in.CommitmentType, in.Status, signed, expires, in.Notes, actorPtr(ctx)).Scan(&id)
if e != nil {
return translateTeaming(e)
}
return s.auditTeaming(ctx, tx, "capture.commitment.added", id)
})
return id, err
}
// UpdateCommitment changes the status or the dates on an existing commitment.
func (s *Service) UpdateCommitment(ctx context.Context, id uuid.UUID, in CommitmentInput) error {
signed, err := optDay(in.SignedOn)
if err != nil {
return fmt.Errorf("%w: signed_on must be YYYY-MM-DD", ErrInvalidInput)
}
expires, err := optDay(in.ExpiresOn)
if err != nil {
return fmt.Errorf("%w: expires_on must be YYYY-MM-DD", ErrInvalidInput)
}
return s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
tag, e := tx.Exec(ctx, `
UPDATE key_personnel_commitments SET
commitment_type = coalesce(nullif($2,''), commitment_type),
status = coalesce(nullif($3,''), status),
role_title = coalesce(nullif($4,''), role_title),
signed_on = coalesce($5, signed_on),
expires_on = coalesce($6, expires_on),
notes = coalesce(nullif($7,''), notes),
updated_at = now()
WHERE id = $1`, id, in.CommitmentType, in.Status, in.RoleTitle, signed, expires, in.Notes)
if e != nil {
return translateTeaming(e)
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return s.auditTeaming(ctx, tx, "capture.commitment.updated", id.String())
})
}
// translateTeaming turns the database's own refusals into sentences a capture manager can act on. The CHECK
// constraints encode real rules, and surfacing them as raw constraint violations would waste the fact that
// the rule was written down.
func translateTeaming(e error) error {
msg := e.Error()
switch {
case strings.Contains(msg, "uq_teaming_partner_per_pursuit"):
return ErrPartnerExists
case strings.Contains(msg, "ck_kpc_names_somebody"):
return ErrCommitmentNeedsPerson
case strings.Contains(msg, "ck_teaming_signed_dated"), strings.Contains(msg, "ck_kpc_committed_dated"):
return ErrSignedNeedsDate
case strings.Contains(msg, "ck_kpc_expiry_after_signing"):
return fmt.Errorf("%w: a commitment cannot expire before it was signed", ErrInvalidInput)
}
return e
}
func (s *Service) auditTeaming(ctx context.Context, tx pgx.Tx, action, entityID string) error {
if s.aud == nil {
return nil
}
ev := audit.Event{Action: action, EntityType: "teaming", EntityID: entityID}
if p, ok := tenant.UserFromContext(ctx); ok {
if uid, err := uuid.Parse(p.UserID); err == nil {
ev.ActorUserID = &uid
}
}
return s.aud.OnTx(ctx, tx, ev)
}
func optDay(v string) (*time.Time, error) {
v = strings.TrimSpace(v)
if v == "" {
return nil, nil
}
t, err := time.Parse("2006-01-02", v)
if err != nil {
return nil, err
}
return &t, nil
}
func dayStr(t *time.Time) string {
if t == nil {
return ""
}
return t.UTC().Format("2006-01-02")
}
func tsStr(t *time.Time) string {
if t == nil {
return ""
}
return t.UTC().Format(time.RFC3339)
}