Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/opportunities/scope_service.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/opportunities/scope_service.go |
| Original SHA-256 | 4ee3edf43725c02080e75816ef7ddc3f492e8fc5b79d7889c9d0faa432daa8b0 |
| Git blob SHA | 27066497d97808141586ba38e36f8732f7ba0a43 |
| Reading-copy SHA-256 | 4ee3edf43725c02080e75816ef7ddc3f492e8fc5b79d7889c9d0faa432daa8b0 |
Recovered source transcript
Download complete source text ↓273 lines · 10.3 KB
package opportunities
import (
"context"
"encoding/json"
"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"
)
// The scope review queue, directive Section 6 screen 4. A reviewer sees what extraction proposed, with the
// citation that grounds it, and accepts, edits, rejects, or adds an item manually. Nothing here can reach
// the scoring engine: a scope item becomes a requirement only through a requirement draft and an approval by
// a named human who holds requirement.approve.
// ScopeItem is the app facing view of one reviewable scope item.
type ScopeItem struct {
ID string `json:"id"`
OpportunityID string `json:"opportunity_id"`
ScopeType string `json:"scope_type"`
Title string `json:"title"`
Description string `json:"description"`
SourceDocumentID string `json:"source_document_id"`
SourcePageNumber int `json:"source_page_number"`
SourceSectionLabel string `json:"source_section_label"`
SourceQuoteHash string `json:"source_quote_hash"`
ExtractionMethod string `json:"extraction_method"`
Confidence float64 `json:"extraction_confidence"`
ReviewStatus string `json:"review_status"`
DisplayOrder int `json:"display_order"`
CreatedAt string `json:"created_at"`
}
// ScopeReview is a reviewer's decision on one item. Title and Description are optional edits applied with
// the decision, which is what "edit and accept" means in one call.
type ScopeReview struct {
ReviewStatus string `json:"review_status"`
Title *string `json:"title"`
Description *string `json:"description"`
ScopeType *string `json:"scope_type"`
}
// ManualScopeInput adds an item a human authored. It needs no citation, because the human IS the source;
// the schema permits exactly that for review_status manual and for nothing else.
type ManualScopeInput struct {
ScopeType string `json:"scope_type"`
Title string `json:"title"`
Description string `json:"description"`
}
const scopeCols = `id::text, opportunity_id::text, scope_type, title, description,
COALESCE(source_document_id::text,''), COALESCE(source_page_number,0), source_section_label,
source_quote_hash, extraction_method, extraction_confidence, review_status, display_order, created_at`
// The review status vocabulary. 'split' was added by migration 0084 with the split and merge operations;
// values are EXTENDED here and never renamed or removed, because a status already written to a row is a
// historical fact and renaming it rewrites the record.
var validScopeReviewStatuses = map[string]bool{
"extracted": true, "accepted": true, "edited": true, "rejected": true, "merged": true, "manual": true,
"split": true,
}
// ListScope returns one opportunity's scope items in review order, optionally filtered by status.
func (s *Service) ListScope(ctx context.Context, opportunityID, statusFilter string) ([]ScopeItem, error) {
if !s.db.Ready() {
return nil, ErrNoDatabase
}
oid, err := uuid.Parse(strings.TrimSpace(opportunityID))
if err != nil {
return nil, fmt.Errorf("%w: opportunity id must be a uuid", ErrInvalidInput)
}
out := make([]ScopeItem, 0)
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var rows pgx.Rows
var qErr error
if sf := strings.TrimSpace(statusFilter); sf != "" {
rows, qErr = tx.Query(ctx, `SELECT `+scopeCols+` FROM opportunity_scope_items
WHERE opportunity_id = $1 AND review_status = $2 ORDER BY display_order, created_at`, oid, sf)
} else {
rows, qErr = tx.Query(ctx, `SELECT `+scopeCols+` FROM opportunity_scope_items
WHERE opportunity_id = $1 ORDER BY display_order, created_at`, oid)
}
if qErr != nil {
return qErr
}
defer rows.Close()
for rows.Next() {
it, sErr := scanScope(rows)
if sErr != nil {
return sErr
}
out = append(out, it)
}
return rows.Err()
})
if err != nil {
return nil, err
}
return out, nil
}
// ReviewScopeItem records a reviewer's decision, optionally with an edit applied in the same write. An edit
// that changes the text moves the status to edited rather than accepted, so a later reader can tell what the
// machine said from what a human rewrote.
func (s *Service) ReviewScopeItem(ctx context.Context, itemID string, r ScopeReview) (ScopeItem, error) {
if !s.db.Ready() {
return ScopeItem{}, ErrNoDatabase
}
iid, err := uuid.Parse(strings.TrimSpace(itemID))
if err != nil {
return ScopeItem{}, fmt.Errorf("%w: item id must be a uuid", ErrInvalidInput)
}
status := strings.TrimSpace(r.ReviewStatus)
if !validScopeReviewStatuses[status] {
return ScopeItem{}, fmt.Errorf("%w: review_status %q is not valid", ErrInvalidInput, status)
}
if r.ScopeType != nil && !scopeTypes[strings.TrimSpace(*r.ScopeType)] {
return ScopeItem{}, fmt.Errorf("%w: scope_type %q is not valid", ErrInvalidInput, *r.ScopeType)
}
actor := actorPtr(ctx)
if actor == nil {
return ScopeItem{}, tenant.ErrNoTenant
}
sets := []string{"review_status = $1", "reviewed_by_user_id = $2", "reviewed_at = now()", "updated_at = now()"}
args := []any{status, actor}
add := func(col string, v any) {
args = append(args, v)
sets = append(sets, fmt.Sprintf("%s = $%d", col, len(args)))
}
if r.Title != nil {
add("title", strings.TrimSpace(*r.Title))
}
if r.Description != nil {
add("description", *r.Description)
}
if r.ScopeType != nil {
add("scope_type", strings.TrimSpace(*r.ScopeType))
}
args = append(args, iid)
query := `UPDATE opportunity_scope_items SET ` + strings.Join(sets, ", ") +
fmt.Sprintf(" WHERE id = $%d RETURNING ", len(args)) + scopeCols
var out ScopeItem
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
row := tx.QueryRow(ctx, query, args...)
var sErr error
if out, sErr = scanScope(row); sErr != nil {
return sErr
}
nv, _ := json.Marshal(map[string]any{"scope_item_id": out.ID, "review_status": out.ReviewStatus})
return s.aud.OnTx(ctx, tx, audit.Event{
Action: "opportunity.scope.reviewed", ActorUserID: actor,
EntityType: "opportunity_scope_item", EntityID: out.ID, NewValues: nv,
})
})
if err != nil {
return ScopeItem{}, err
}
return out, nil
}
// AddManualScope adds a human authored scope item. It is written as review_status manual, the only status
// the citation CHECK permits without a source, because here the human is the source.
func (s *Service) AddManualScope(ctx context.Context, opportunityID string, in ManualScopeInput) (ScopeItem, error) {
if !s.db.Ready() {
return ScopeItem{}, ErrNoDatabase
}
oid, err := uuid.Parse(strings.TrimSpace(opportunityID))
if err != nil {
return ScopeItem{}, fmt.Errorf("%w: opportunity id must be a uuid", ErrInvalidInput)
}
if !scopeTypes[strings.TrimSpace(in.ScopeType)] {
return ScopeItem{}, fmt.Errorf("%w: scope_type %q is not valid", ErrInvalidInput, in.ScopeType)
}
if strings.TrimSpace(in.Title) == "" {
return ScopeItem{}, fmt.Errorf("%w: title is required", ErrInvalidInput)
}
actor := actorPtr(ctx)
if actor == nil {
return ScopeItem{}, tenant.ErrNoTenant
}
var out ScopeItem
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
row := tx.QueryRow(ctx, `
INSERT INTO opportunity_scope_items (
organization_id, opportunity_id, scope_type, title, description, review_status,
extraction_method, reviewed_by_user_id, reviewed_at)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid,
$1, $2, $3, $4, 'manual', 'human', $5, now())
RETURNING `+scopeCols,
oid, strings.TrimSpace(in.ScopeType), strings.TrimSpace(in.Title), in.Description, actor)
var sErr error
if out, sErr = scanScope(row); sErr != nil {
return sErr
}
nv, _ := json.Marshal(map[string]any{"scope_item_id": out.ID, "scope_type": out.ScopeType})
return s.aud.OnTx(ctx, tx, audit.Event{
Action: "opportunity.scope.manual_added", ActorUserID: actor,
EntityType: "opportunity_scope_item", EntityID: out.ID, NewValues: nv,
})
})
if err != nil {
return ScopeItem{}, err
}
return out, nil
}
// ListDrafts returns the requirement drafts for one opportunity, so the build requirements screen can show
// what is waiting on a decision and what has already been promoted.
func (s *Service) ListDrafts(ctx context.Context, opportunityID string) ([]map[string]any, error) {
if !s.db.Ready() {
return nil, ErrNoDatabase
}
oid, err := uuid.Parse(strings.TrimSpace(opportunityID))
if err != nil {
return nil, fmt.Errorf("%w: opportunity id must be a uuid", ErrInvalidInput)
}
out := make([]map[string]any, 0)
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
rows, qErr := tx.Query(ctx, `
SELECT id::text, labor_category, quantity, clearance_level, polygraph,
minimum_years_experience, work_location, remote_allowed, required_skills,
review_status, COALESCE(approved_requirement_id::text,''), derivation_method
FROM requirement_drafts WHERE opportunity_id = $1 ORDER BY created_at`, oid)
if qErr != nil {
return qErr
}
defer rows.Close()
for rows.Next() {
var id, lc, cl, poly, loc, status, reqID, method string
var qty, years int
var remote bool
var skills []string
if err := rows.Scan(&id, &lc, &qty, &cl, &poly, &years, &loc, &remote, &skills, &status, &reqID, &method); err != nil {
return err
}
out = append(out, map[string]any{
"id": id, "labor_category": lc, "quantity": qty, "clearance_level": cl,
"polygraph": poly, "minimum_years_experience": years, "work_location": loc,
"remote_allowed": remote, "required_skills": skills, "review_status": status,
"approved_requirement_id": reqID, "derivation_method": method,
})
}
return rows.Err()
})
if err != nil {
return nil, err
}
return out, nil
}
func scanScope(row pgx.Row) (ScopeItem, error) {
var it ScopeItem
var createdAt interface{ String() string }
var created any
if err := row.Scan(&it.ID, &it.OpportunityID, &it.ScopeType, &it.Title, &it.Description,
&it.SourceDocumentID, &it.SourcePageNumber, &it.SourceSectionLabel, &it.SourceQuoteHash,
&it.ExtractionMethod, &it.Confidence, &it.ReviewStatus, &it.DisplayOrder, &created); err != nil {
return ScopeItem{}, err
}
_ = createdAt
if t, ok := created.(interface{ Format(string) string }); ok {
it.CreatedAt = t.Format("2006-01-02T15:04:05Z07:00")
}
return it, nil
}