Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/matching/access_handler.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/matching/access_handler.go |
| Original SHA-256 | fb57e9077bd72fe4e21f164863fa6293cb639bbef726b698d13e8c2724d22fde |
| Git blob SHA | 5da22f195ae0ab10006deb455d5b8b2d323f003e |
| Reading-copy SHA-256 | fb57e9077bd72fe4e21f164863fa6293cb639bbef726b698d13e8c2724d22fde |
Recovered source transcript
Download complete source text ↓146 lines · 5.1 KB
package matching
import (
"context"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/missionworkforce/missionmatch/internal/modules/matching/intelligence"
"github.com/missionworkforce/missionmatch/internal/platform/audit"
)
// THE ACCESS HISTORY WRITE, which is the half without which the whole feature is decoration.
//
// The posture is derived from three dates, and if there is no way to enter them then every candidate reports
// "not known" forever and the product has added a column that says nothing. This is the route a recruiter
// uses after a phone call, which is where these dates actually come from: nobody imports them.
// AccessInput is the write shape. Every field is a pointer to a string so a caller can distinguish leaving a
// value alone from clearing it, and dates cross the wire as YYYY-MM-DD rather than as a timestamp, because
// what is being recorded is a day somebody remembers rather than an instant.
type AccessInput struct {
CVEnrolled *bool `json:"cv_enrolled"`
CVEnrolledOn *string `json:"cv_enrolled_on"`
AccessEndedOn *string `json:"access_ended_on"`
LastAdjudicatedOn *string `json:"last_adjudicated_on"`
}
func (h *Handler) patchCandidateAccess(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(chi.URLParam(r, "candidateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid candidate id")
return
}
var in AccessInput
if !decodeJSON(w, r, &in) {
return
}
intel, err := h.svc.SetCandidateAccess(r.Context(), id, in)
if err != nil {
writeServiceError(w, err)
return
}
// The DERIVED posture comes straight back, so the screen renders the consequence of what was just entered
// rather than refetching and hoping it agrees.
writeJSON(w, http.StatusOK, intel)
}
// parseDay accepts YYYY-MM-DD or an empty string meaning clear.
func parseDay(v *string, field string) (*time.Time, bool, error) {
if v == nil {
return nil, false, nil // absent: leave alone
}
if *v == "" {
return nil, true, nil // present and empty: clear
}
t, err := time.Parse("2006-01-02", *v)
if err != nil {
return nil, false, fmt.Errorf("%s must be a date in the form YYYY-MM-DD", field)
}
return &t, true, nil
}
// SetCandidateAccess records the access history and returns the posture it implies.
func (s *Service) SetCandidateAccess(ctx context.Context, id uuid.UUID, in AccessInput) (intelligence.AccessIntel, error) {
var out intelligence.AccessIntel
if s.db == nil {
return out, ErrNoDatabase
}
ended, endedSet, err := parseDay(in.AccessEndedOn, "access_ended_on")
if err != nil {
return out, fmt.Errorf("%w: %s", ErrBadRequest, err.Error())
}
adj, adjSet, err := parseDay(in.LastAdjudicatedOn, "last_adjudicated_on")
if err != nil {
return out, fmt.Errorf("%w: %s", ErrBadRequest, err.Error())
}
enrolledOn, enrolledOnSet, err := parseDay(in.CVEnrolledOn, "cv_enrolled_on")
if err != nil {
return out, fmt.Errorf("%w: %s", ErrBadRequest, err.Error())
}
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
// COALESCE on the unset fields so a partial write leaves the rest alone. The database CHECK refuses
// the one combination that cannot be true, somebody enrolled AND separated, so an inconsistent pair
// fails loudly here rather than producing a posture nobody can explain.
if _, e := tx.Exec(ctx, `
UPDATE candidates SET
cv_enrolled = CASE WHEN $2::boolean IS NOT NULL OR $3 THEN $2 ELSE cv_enrolled END,
cv_enrolled_on = CASE WHEN $4 THEN $5::date ELSE cv_enrolled_on END,
access_ended_on = CASE WHEN $6 THEN $7::date ELSE access_ended_on END,
last_adjudicated_on = CASE WHEN $8 THEN $9::date ELSE last_adjudicated_on END,
updated_at = now()
WHERE id = $1 AND deleted_at IS NULL`,
id, in.CVEnrolled, in.CVEnrolled != nil,
enrolledOnSet, enrolledOn,
endedSet, ended,
adjSet, adj); e != nil {
return e
}
var c struct {
enrolled *bool
ended pgtype.Date
adj pgtype.Date
level string
status string
}
if e := tx.QueryRow(ctx,
`SELECT cv_enrolled, access_ended_on, last_adjudicated_on, clearance_level, clearance_status
FROM candidates WHERE id = $1`, id).
Scan(&c.enrolled, &c.ended, &c.adj, &c.level, &c.status); e != nil {
if e == pgx.ErrNoRows {
return ErrNotFound
}
return e
}
f := intelligence.AccessFacts{CVEnrolled: c.enrolled, ClearanceLevel: c.level, ClearanceStatus: c.status}
if c.ended.Valid {
t := c.ended.Time
f.AccessEndedOn = &t
}
if c.adj.Valid {
t := c.adj.Time
f.LastAdjudicated = &t
}
out = intelligence.Access(f, s.clock().UTC())
// AUDITED, because a change here changes whether a person is presented as placeable, which is a
// staffing decision input. No PII in the payload beyond the candidate id the event already names.
if s.aud != nil {
return s.aud.OnTx(ctx, tx, audit.Event{
Action: "candidate.access.updated", EntityType: "candidate", EntityID: id.String(),
ActorUserID: actorID(ctx),
})
}
return nil
})
return out, err
}