Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/matching/handler.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | b7d742f8f032984be3817efffa3eb8b13b651c1f |
| Branch | ruthless-debug-sweep |
| Path or archive member | internal/modules/matching/handler.go |
| Original SHA-256 | 6aeaa7ddc8c0e922d2d8f5aca46dc6256899453d52bfc8f504a843ba726718d0 |
| Git blob SHA | a43d990343d05749acc4ae72d5e593fd4c8e0ba9 |
| Reading-copy SHA-256 | 6aeaa7ddc8c0e922d2d8f5aca46dc6256899453d52bfc8f504a843ba726718d0 |
Every recorded occurrence
These 3 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/matching/handler.go | ruthless-debug-sweep | b7d742f8f032984be3817efffa3eb8b13b651c1f | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | workstream-a-shared-core | e0de03ac3a1ec826bd15d0e048e3b02baa23eeff | Open original |
| Janady13/missionmatch / internal/modules/matching/handler.go | ruthless-debug-sweep | b7d742f8f032984be3817efffa3eb8b13b651c1f | Open original |
Recovered source transcript
Download complete source text ↓463 lines · 15.2 KB
package matching
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/missionworkforce/missionmatch/internal/platform/database/dbgen"
"github.com/missionworkforce/missionmatch/internal/platform/security"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// Handler exposes the matching/slate API. Routes are mounted under a tenant-scoped group.
type Handler struct{ svc *Service }
// NewHandler builds the HTTP handler.
func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} }
// Routes registers the API endpoints on r (already wrapped by the session resolver). Every person
// scoring surface is an AEDT output and carries a capability check, so authorization is enforced at
// the API from the validated session, never hidden in the UI. Scoring and ranking need search.run;
// the slate write and the human decision need slate.write; reads need candidate.read.
func (h *Handler) Routes(r chi.Router) {
r.With(security.RequireCapability(security.CapSearchRun)).Post("/match", h.postMatch)
r.With(security.RequireCapability(security.CapSearchRun)).Post("/screen", h.postScreen)
r.With(security.RequireCapability(security.CapSlateEdit)).Post("/slates/generate", h.postGenerate)
r.With(security.RequireCapability(security.CapSlateEdit)).Patch("/slates/{slateID}/members/{candidateID}/decision", h.postDecision)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/slates/{slateID}/members/{candidateID}/trace", h.getTrace)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/candidates", h.getCandidates)
r.With(security.RequireCapability(security.CapSearchRun)).Get("/candidates/intelligence", h.getCandidateIntelligence)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/candidates/{candidateID}", h.getCandidate)
r.With(security.RequireCapability(security.CapSearchRun)).Get("/candidates/{candidateID}/opportunities", h.getOpportunities)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/requirements", h.getRequirements)
r.With(security.RequireCapability(security.CapCandidateRead)).Get("/requirements/{requirementID}", h.getRequirement)
r.With(security.RequireCapability(security.CapSearchRun)).Get("/requirements/{requirementID}/candidates", h.getRankedCandidates)
// Mission Check (W1): scoring an in memory resume is a search.run AEDT output; the optional Save
// Candidate is a recruiter write. Both ride the same session auth, RBAC, and audit as every surface.
r.With(security.RequireCapability(security.CapSearchRun)).Post("/mission-check/opportunities", h.postMissionCheckOpportunities)
r.With(security.RequireCapability(security.CapSearchRun)).Post("/mission-check/screen", h.postMissionCheckJD)
r.With(security.RequireCapability(security.CapCandidateWrite)).Post("/candidates", h.postCreateCandidate)
r.With(security.RequireCapability(security.CapCandidateWrite)).Patch("/candidates/{candidateID}/clearance-provenance", h.patchClearanceProvenance)
}
type matchRequest struct {
CandidateID uuid.UUID `json:"candidate_id"`
RequirementID uuid.UUID `json:"requirement_id"`
}
func (h *Handler) postMatch(w http.ResponseWriter, r *http.Request) {
var req matchRequest
if !decodeJSON(w, r, &req) {
return
}
if req.CandidateID == uuid.Nil || req.RequirementID == uuid.Nil {
writeError(w, http.StatusBadRequest, "candidate_id and requirement_id are required")
return
}
res, err := h.svc.ScreenCandidate(r.Context(), req.CandidateID, req.RequirementID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, matchResultView(res))
}
func (h *Handler) postScreen(w http.ResponseWriter, r *http.Request) {
var req matchRequest
if !decodeJSON(w, r, &req) {
return
}
if req.CandidateID == uuid.Nil || req.RequirementID == uuid.Nil {
writeError(w, http.StatusBadRequest, "candidate_id and requirement_id are required")
return
}
view, err := h.svc.Screen(r.Context(), req.CandidateID, req.RequirementID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, view)
}
func (h *Handler) getRankedCandidates(w http.ResponseWriter, r *http.Request) {
reqID, err := uuid.Parse(chi.URLParam(r, "requirementID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid requirement id")
return
}
out, err := h.svc.RankCandidates(r.Context(), reqID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getOpportunities(w http.ResponseWriter, r *http.Request) {
candidateID, err := uuid.Parse(chi.URLParam(r, "candidateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid candidate id")
return
}
limit := 10
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 100 {
limit = v
}
fits, err := h.svc.OpportunitiesForCandidate(r.Context(), candidateID, limit)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, fits)
}
type generateRequest struct {
RequirementID uuid.UUID `json:"requirement_id"`
Name string `json:"name"`
MaxCandidates int32 `json:"max_candidates"`
}
func (h *Handler) postGenerate(w http.ResponseWriter, r *http.Request) {
var req generateRequest
if !decodeJSON(w, r, &req) {
return
}
if req.RequirementID == uuid.Nil {
writeError(w, http.StatusBadRequest, "requirement_id is required")
return
}
out, err := h.svc.GenerateSlate(r.Context(), GenerateSlateInput{
RequirementID: req.RequirementID, Name: req.Name, MaxCandidates: req.MaxCandidates,
CreatedBy: principalID(r),
})
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, out)
}
type decisionRequest struct {
Decision string `json:"decision"` // approved or rejected
}
func (h *Handler) postDecision(w http.ResponseWriter, r *http.Request) {
slateID, err := uuid.Parse(chi.URLParam(r, "slateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid slate id")
return
}
candidateID, err := uuid.Parse(chi.URLParam(r, "candidateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid candidate id")
return
}
var req decisionRequest
if !decodeJSON(w, r, &req) {
return
}
if err := h.svc.DecideSlateMember(r.Context(), slateID, candidateID, req.Decision); err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": req.Decision})
}
func (h *Handler) getTrace(w http.ResponseWriter, r *http.Request) {
slateID, err := uuid.Parse(chi.URLParam(r, "slateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid slate id")
return
}
candidateID, err := uuid.Parse(chi.URLParam(r, "candidateID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid candidate id")
return
}
m, err := h.svc.SlateMemberTrace(r.Context(), slateID, candidateID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, slateMemberTraceView(m))
}
// principalID returns the validated session user id for the actor on a decision or generation.
func principalID(r *http.Request) *uuid.UUID {
if p, ok := tenant.UserFromContext(r.Context()); ok {
if id, err := uuid.Parse(p.UserID); err == nil {
return &id
}
}
return nil
}
func (h *Handler) getCandidateIntelligence(w http.ResponseWriter, r *http.Request) {
limit := 0
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil {
limit = v
}
out, err := h.svc.CandidateIntelligence(r.Context(), limit)
if err != nil {
writeServiceError(w, err)
return
}
if out == nil {
out = []CandidateIntelligenceView{}
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getCandidates(w http.ResponseWriter, r *http.Request) {
limit, offset := pageParams(r)
rows, err := h.svc.ListCandidates(r.Context(), limit, offset)
if err != nil {
writeServiceError(w, err)
return
}
out := make([]CandidateListView, len(rows))
for i, c := range rows {
out[i] = candidateListView(c)
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getRequirements(w http.ResponseWriter, r *http.Request) {
limit, offset := pageParams(r)
rows, err := h.svc.ListRequirements(r.Context(), limit, offset)
if err != nil {
writeServiceError(w, err)
return
}
out := make([]RequirementListView, len(rows))
for i, rq := range rows {
out[i] = requirementListView(rq)
}
writeJSON(w, http.StatusOK, out)
}
func (h *Handler) getCandidate(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
}
c, err := h.svc.GetCandidate(r.Context(), id)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, candidateListView(c))
}
func (h *Handler) getRequirement(w http.ResponseWriter, r *http.Request) {
id, err := uuid.Parse(chi.URLParam(r, "requirementID"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid requirement id")
return
}
rq, err := h.svc.GetRequirement(r.Context(), id)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, requirementListView(rq))
}
// ---------- Mission Check (W1) ----------
type missionCheckOpportunitiesRequest struct {
ResumeText string `json:"resume_text"`
Limit int `json:"limit"`
}
func (h *Handler) postMissionCheckOpportunities(w http.ResponseWriter, r *http.Request) {
var req missionCheckOpportunitiesRequest
if !decodeJSON(w, r, &req) {
return
}
if strings.TrimSpace(req.ResumeText) == "" {
writeError(w, http.StatusBadRequest, "resume_text is required")
return
}
out, err := h.svc.MissionCheckOpportunities(r.Context(), req.ResumeText, req.Limit)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}
type missionCheckJDRequest struct {
ResumeText string `json:"resume_text"`
JobDescriptionText string `json:"job_description_text"`
}
func (h *Handler) postMissionCheckJD(w http.ResponseWriter, r *http.Request) {
var req missionCheckJDRequest
if !decodeJSON(w, r, &req) {
return
}
if strings.TrimSpace(req.ResumeText) == "" || strings.TrimSpace(req.JobDescriptionText) == "" {
writeError(w, http.StatusBadRequest, "resume_text and job_description_text are required")
return
}
out, err := h.svc.MissionCheckJD(r.Context(), req.ResumeText, req.JobDescriptionText)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}
type createCandidateRequest struct {
FullName string `json:"full_name"`
ClearanceLevel string `json:"clearance_level"`
ClearancePoly string `json:"clearance_poly"`
ClearanceStatus string `json:"clearance_status"`
YearsExperience int `json:"years_experience"`
Location string `json:"location"`
Remote bool `json:"remote"`
Relocatable bool `json:"relocatable"`
Available bool `json:"available"`
Skills []string `json:"skills"`
Certifications []string `json:"certifications"`
CustomerExperience []string `json:"customer_experience"`
}
func (h *Handler) postCreateCandidate(w http.ResponseWriter, r *http.Request) {
var req createCandidateRequest
if !decodeJSON(w, r, &req) {
return
}
if strings.TrimSpace(req.FullName) == "" {
writeError(w, http.StatusBadRequest, "full_name is required")
return
}
id, err := h.svc.SaveCandidate(r.Context(), SaveCandidateInput(req))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, map[string]string{"id": id.String()})
}
type setProvenanceRequest struct {
Provenance string `json:"provenance"` // self_reported | sponsor_verified | document | continuous_vetting
VerifiedAt *string `json:"verified_at,omitempty"` // optional YYYY-MM-DD, the FSO verification date
}
func (h *Handler) patchClearanceProvenance(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 req setProvenanceRequest
if !decodeJSON(w, r, &req) {
return
}
var va *time.Time
if req.VerifiedAt != nil && *req.VerifiedAt != "" {
t, perr := time.Parse("2006-01-02", *req.VerifiedAt)
if perr != nil {
writeError(w, http.StatusBadRequest, "verified_at must be YYYY-MM-DD")
return
}
va = &t
}
if err := h.svc.SetClearanceProvenance(r.Context(), id, req.Provenance, va); err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}
// ---------- helpers ----------
func pageParams(r *http.Request) (limit, offset int32) {
limit, offset = 50, 0
if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 500 {
limit = int32(v)
}
if v, err := strconv.Atoi(r.URL.Query().Get("offset")); err == nil && v >= 0 {
offset = int32(v)
}
return limit, offset
}
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body: "+err.Error())
return false
}
return true
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
// writeServiceError maps service/tenant errors to HTTP status without leaking internals.
func writeServiceError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, tenant.ErrNoTenant):
writeError(w, http.StatusForbidden, "no validated tenant")
case errors.Is(err, ErrNotFound):
writeError(w, http.StatusNotFound, "not found")
case errors.Is(err, ErrBadRequest):
writeError(w, http.StatusBadRequest, "bad request")
case errors.Is(err, ErrNoDatabase):
writeError(w, http.StatusServiceUnavailable, "database not configured")
case errors.Is(err, ErrNoExtractor):
writeError(w, http.StatusServiceUnavailable, "mission check extraction not configured")
default:
writeError(w, http.StatusInternalServerError, "internal error")
}
}
// SlateMemberTraceView is the audit and candidate facing explanation of a frozen decision: the per
// component trace, the human rationale, the gap summary, the decision, and the model version that
// produced it. The trace and gap summary are passed through as the stored JSON.
type SlateMemberTraceView struct {
CandidateID string `json:"candidate_id"`
TotalScore string `json:"total_score"`
Band string `json:"band"`
Explanation string `json:"explanation"`
Trace json.RawMessage `json:"trace"`
GapSummary json.RawMessage `json:"gap_summary"`
Decision string `json:"decision"`
ModelVersion string `json:"model_version"`
}
func slateMemberTraceView(m dbgen.SlateMember) SlateMemberTraceView {
v := SlateMemberTraceView{
CandidateID: m.CandidateID.String(),
TotalScore: m.TotalScore.StringFixed(2),
Band: m.Band,
Explanation: m.Explanation,
Trace: json.RawMessage(m.Trace),
GapSummary: json.RawMessage(m.GapSummary),
Decision: m.Decision,
}
if m.ModelVersion != nil {
v.ModelVersion = *m.ModelVersion
}
return v
}