Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/opportunities/amendment_view.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/opportunities/amendment_view.go |
| Original SHA-256 | 7e32040ee12b4c9bfe6e91f1e1e77085ab5cc1ce046aaf403e1f4bdb43b2632d |
| Git blob SHA | cf54f8ca4a710f27ae9a48875e17ed715e06c722 |
| Reading-copy SHA-256 | 7e32040ee12b4c9bfe6e91f1e1e77085ab5cc1ce046aaf403e1f4bdb43b2632d |
Recovered source transcript
Download complete source text ↓156 lines · 5.6 KB
package opportunities
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
// THE AMENDMENT VIEW, the read side of directive Section 4 item 11 and the screen Gate C names. Everything
// needed to answer one question a capture team asks under time pressure: the source changed, so what does that
// do to the requirements we already approved?
//
// IT IS DELIBERATELY THREE THINGS IN ONE RESPONSE. A version list alone says a change happened. An alert list
// alone says somebody was told. Neither says whether an APPROVED requirement is affected, which is the only
// part with money attached. Splitting these across three round trips invites a screen that renders two of them
// and quietly omits the third.
//
// NOTHING HERE MUTATES. The impact is reported as a FLAG for a named human to act on, never applied. That is
// acceptance item 11 and it is the line this whole phase is built around.
type OpportunityVersion struct {
VersionNumber int `json:"version_number"`
AmendmentNumber string `json:"amendment_number"`
SourceVersionLabel string `json:"source_version_label"`
SourcePayloadHash string `json:"source_payload_hash"`
FetchedAt string `json:"fetched_at"`
IsCurrent bool `json:"is_current"`
}
type AmendmentAlert struct {
ID string `json:"id"`
AlertType string `json:"alert_type"`
Severity string `json:"severity"`
Title string `json:"title"`
Detail string `json:"detail"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
// AffectedRequirement is an approved requirement whose lineage runs back through this opportunity's scope. It
// carries the draft and scope item it came from so a reviewer can walk from a flag to the cited span.
type AffectedRequirement struct {
RequirementID string `json:"requirement_id"`
DraftID string `json:"draft_id"`
LaborCategory string `json:"labor_category"`
ClearanceLevel string `json:"clearance_level"`
Quantity int `json:"quantity"`
SourceScopeItemID string `json:"source_scope_item_id"`
SourceScopeTitle string `json:"source_scope_title"`
}
type AmendmentView struct {
Versions []OpportunityVersion `json:"versions"`
Alerts []AmendmentAlert `json:"alerts"`
AffectedRequirements []AffectedRequirement `json:"affected_requirements"`
}
// AmendmentViewFor assembles the whole amendment picture for one opportunity.
func (s *Service) AmendmentViewFor(ctx context.Context, opportunityID string) (AmendmentView, error) {
out := AmendmentView{
Versions: []OpportunityVersion{},
Alerts: []AmendmentAlert{},
AffectedRequirements: []AffectedRequirement{},
}
if !s.db.Ready() {
return out, ErrNoDatabase
}
oid, err := uuid.Parse(strings.TrimSpace(opportunityID))
if err != nil {
return out, fmt.Errorf("%w: opportunity id must be a uuid", ErrInvalidInput)
}
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
vRows, vErr := tx.Query(ctx, `
SELECT version_number, amendment_number, source_version_label,
COALESCE(source_payload_hash,''), fetched_at::text, is_current
FROM opportunity_versions
WHERE opportunity_id = $1
ORDER BY version_number`, oid)
if vErr != nil {
return vErr
}
for vRows.Next() {
var v OpportunityVersion
if sErr := vRows.Scan(&v.VersionNumber, &v.AmendmentNumber, &v.SourceVersionLabel,
&v.SourcePayloadHash, &v.FetchedAt, &v.IsCurrent); sErr != nil {
vRows.Close()
return sErr
}
out.Versions = append(out.Versions, v)
}
vRows.Close()
if rErr := vRows.Err(); rErr != nil {
return rErr
}
// The amendment and deadline alert kinds, which are the two migration 0075 added to the vocabulary.
aRows, aErr := tx.Query(ctx, `
SELECT id::text, alert_type, severity, title, detail, status, created_at::text
FROM alerts
WHERE subject_type = 'opportunity' AND subject_id = $1
AND alert_type IN ('opportunity_amendment','opportunity_deadline')
ORDER BY created_at DESC`, oid)
if aErr != nil {
return aErr
}
for aRows.Next() {
var a AmendmentAlert
if sErr := aRows.Scan(&a.ID, &a.AlertType, &a.Severity, &a.Title, &a.Detail,
&a.Status, &a.CreatedAt); sErr != nil {
aRows.Close()
return sErr
}
out.Alerts = append(out.Alerts, a)
}
aRows.Close()
if rErr := aRows.Err(); rErr != nil {
return rErr
}
// THE PART THAT MATTERS. An approved requirement is reachable from this opportunity only through a
// promoted draft, and the draft records the scope item it was derived from. That chain is the lineage
// acceptance item 11 depends on, so it is followed rather than guessed at from titles.
rRows, rErr := tx.Query(ctx, `
SELECT d.approved_requirement_id::text, d.id::text, d.labor_category, d.clearance_level, d.quantity,
COALESCE(d.source_scope_item_id::text,''), COALESCE(si.title,'')
FROM requirement_drafts d
LEFT JOIN opportunity_scope_items si ON si.id = d.source_scope_item_id
WHERE d.opportunity_id = $1
AND d.review_status = 'approved'
AND d.approved_requirement_id IS NOT NULL
ORDER BY d.labor_category`, oid)
if rErr != nil {
return rErr
}
for rRows.Next() {
var a AffectedRequirement
if sErr := rRows.Scan(&a.RequirementID, &a.DraftID, &a.LaborCategory, &a.ClearanceLevel,
&a.Quantity, &a.SourceScopeItemID, &a.SourceScopeTitle); sErr != nil {
rRows.Close()
return sErr
}
out.AffectedRequirements = append(out.AffectedRequirements, a)
}
rRows.Close()
return rRows.Err()
})
if err != nil {
return AmendmentView{}, err
}
return out, nil
}