Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/careers/manage.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/careers/manage.go |
| Original SHA-256 | 398d43fe606083de8928aef85a3e0bb73ec47005b2751ab467e8f5d8847b9c52 |
| Git blob SHA | 06d17f346ba09a86bd6ef2f536482bb1d814304c |
| Reading-copy SHA-256 | 398d43fe606083de8928aef85a3e0bb73ec47005b2751ab467e8f5d8847b9c52 |
Recovered source transcript
Download complete source text ↓327 lines · 12.3 KB
package careers
// THE TENANT SIDE: publishing a posting and reviewing what arrives.
//
// PUBLISHING IS AN ACT OF WRITING, not of flipping a flag on a requisition. The public copy is its own text,
// so an agency's bill rate, its margin and its customer's name cannot reach the open internet through a join
// somebody added later. That costs a recruiter one screen of typing and removes a whole class of disclosure.
import (
"context"
"encoding/json"
"errors"
"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"
)
type PostingInput struct {
RequisitionID string `json:"requisition_id"`
Title string `json:"title"`
Summary string `json:"summary"`
Responsibilities string `json:"responsibilities"`
Qualifications string `json:"qualifications"`
Location string `json:"location_text"`
RemoteAllowed bool `json:"remote_allowed"`
EmploymentType string `json:"employment_type"`
Clearance string `json:"clearance_text"`
PayRange string `json:"pay_range_text"`
ClosesAt *time.Time `json:"closes_at"`
}
type ManagedPosting struct {
ID string `json:"id"`
Token string `json:"public_token"`
RequisitionID string `json:"requisition_id"`
Title string `json:"title"`
Status string `json:"status"`
PublishedAt *time.Time `json:"published_at,omitempty"`
ClosesAt *time.Time `json:"closes_at,omitempty"`
Applications int `json:"applications"`
CreatedAt time.Time `json:"created_at"`
}
type InboundApplication struct {
ID string `json:"id"`
PostingID string `json:"posting_id"`
PostingTitle string `json:"posting_title"`
FullName string `json:"full_name"`
Email string `json:"email"`
Phone string `json:"phone,omitempty"`
Location string `json:"location_text,omitempty"`
CoverNote string `json:"cover_note,omitempty"`
Clearance string `json:"clearance_self_reported,omitempty"`
HasResume bool `json:"has_resume"`
Status string `json:"status"`
StatusReason string `json:"status_reason,omitempty"`
CandidateID string `json:"candidate_id,omitempty"`
Source string `json:"source"`
ConsentedAt *time.Time `json:"consented_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
func (s *Service) CreatePosting(ctx context.Context, in PostingInput) (string, string, error) {
if s.db == nil {
return "", "", ErrNoDatabase
}
reqID, err := uuid.Parse(strings.TrimSpace(in.RequisitionID))
if err != nil {
return "", "", errors.New("careers: a requisition identifier is required")
}
in.Title = strings.TrimSpace(in.Title)
if in.Title == "" || len(in.Title) > 300 {
return "", "", errors.New("careers: a posting needs a public title of up to 300 characters")
}
token, err := NewPostingToken()
if err != nil {
return "", "", err
}
actor := actorPtr(ctx)
var id string
err = s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
return tx.QueryRow(ctx, `
INSERT INTO job_postings (organization_id, requisition_id, public_token, title, summary,
responsibilities, qualifications, location_text, remote_allowed, employment_type,
clearance_text, pay_range_text, closes_at, created_by)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid,
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
RETURNING id::text`,
reqID, token, in.Title, clip(in.Summary, 8000), clip(in.Responsibilities, 8000),
clip(in.Qualifications, 8000), clip(in.Location, 200), in.RemoteAllowed,
clip(in.EmploymentType, 60), clip(in.Clearance, 200), clip(in.PayRange, 200),
in.ClosesAt, actor).Scan(&id)
})
return id, token, err
}
// SetPostingStatus publishes or closes. Publishing stamps published_at, which the schema requires, so a live
// advertisement always records when it went live: the first question in any hiring audit.
func (s *Service) SetPostingStatus(ctx context.Context, id uuid.UUID, status, ip, ua string) error {
if s.db == nil {
return ErrNoDatabase
}
if status != "draft" && status != "published" && status != "closed" {
return errors.New("careers: a posting is draft, published or closed")
}
actor := actorPtr(ctx)
return s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
tag, e := tx.Exec(ctx, `
UPDATE job_postings
SET status = $2,
published_at = CASE WHEN $2 = 'published' AND published_at IS NULL THEN now() ELSE published_at END,
updated_at = now()
WHERE id = $1`, id, status)
if e != nil {
return e
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
if s.aud == nil {
return nil
}
body, _ := json.Marshal(map[string]any{"posting_id": id.String(), "status": status})
return s.aud.OnTx(ctx, tx, audit.Event{
Action: "careers.posting." + status, ActorUserID: actor,
EntityType: "job_posting", EntityID: id.String(), NewValues: body,
IPAddress: ip, UserAgent: ua,
})
})
}
func (s *Service) ListPostings(ctx context.Context) ([]ManagedPosting, error) {
if s.db == nil {
return nil, ErrNoDatabase
}
out := make([]ManagedPosting, 0, 32)
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
rows, e := tx.Query(ctx, `
SELECT p.id::text, p.public_token, p.requisition_id::text, p.title, p.status,
p.published_at, p.closes_at,
(SELECT count(*)::int FROM job_applications a WHERE a.posting_id = p.id),
p.created_at
FROM job_postings p ORDER BY p.created_at DESC LIMIT 300`)
if e != nil {
return e
}
defer rows.Close()
for rows.Next() {
var p ManagedPosting
if e := rows.Scan(&p.ID, &p.Token, &p.RequisitionID, &p.Title, &p.Status,
&p.PublishedAt, &p.ClosesAt, &p.Applications, &p.CreatedAt); e != nil {
return e
}
out = append(out, p)
}
return rows.Err()
})
return out, err
}
func (s *Service) ListApplications(ctx context.Context, status string) ([]InboundApplication, error) {
if s.db == nil {
return nil, ErrNoDatabase
}
out := make([]InboundApplication, 0, 64)
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
rows, e := tx.Query(ctx, `
SELECT a.id::text, a.posting_id::text, p.title, a.full_name, a.email, a.phone, a.location_text,
a.cover_note, a.clearance_self_reported, (a.resume_storage_key <> ''), a.status,
a.status_reason, coalesce(a.candidate_id::text,''), a.source, a.consented_at, a.created_at
FROM job_applications a JOIN job_postings p ON p.id = a.posting_id
WHERE ($1 = '' OR a.status = $1)
ORDER BY a.created_at DESC LIMIT 500`, status)
if e != nil {
return e
}
defer rows.Close()
for rows.Next() {
var a InboundApplication
if e := rows.Scan(&a.ID, &a.PostingID, &a.PostingTitle, &a.FullName, &a.Email, &a.Phone,
&a.Location, &a.CoverNote, &a.Clearance, &a.HasResume, &a.Status, &a.StatusReason,
&a.CandidateID, &a.Source, &a.ConsentedAt, &a.CreatedAt); e != nil {
return e
}
out = append(out, a)
}
return rows.Err()
})
return out, err
}
// ReviewApplication records the CONSIDERATION decision, which under the Internet Applicant rule is a separate
// event from the submission and is the half a federal contractor is actually audited on. The reviewer and the
// time are always recorded, so no decision is anonymous.
func (s *Service) ReviewApplication(ctx context.Context, id uuid.UUID, status, reason, ip, ua string) error {
if s.db == nil {
return ErrNoDatabase
}
switch status {
case "considered", "rejected", "spam", "withdrawn":
default:
return errors.New("careers: a review records considered, rejected, spam or withdrawn")
}
actor := actorPtr(ctx)
return s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
tag, e := tx.Exec(ctx, `
UPDATE job_applications
SET status = $2, status_reason = $3, reviewed_by = $4, reviewed_at = now()
WHERE id = $1 AND status <> 'converted'`, id, status, clip(reason, 500), actor)
if e != nil {
return e
}
if tag.RowsAffected() == 0 {
// Either it does not exist or it has already become a candidate. A converted application is not
// re decidable, because the consideration already happened and rewriting it would falsify the
// applicant flow log.
return ErrNotFound
}
if s.aud == nil {
return nil
}
body, _ := json.Marshal(map[string]any{"status": status, "reason": clip(reason, 500)})
return s.aud.OnTx(ctx, tx, audit.Event{
Action: "careers.application.reviewed", ActorUserID: actor,
EntityType: "job_application", EntityID: id.String(), NewValues: body,
IPAddress: ip, UserAgent: ua,
})
})
}
// ConvertApplication turns a submission into a real candidate. This is the CONSIDERATION event made concrete,
// and it is deliberately explicit: nothing becomes a candidate automatically, so an inbound flood cannot
// silently fill a bench with people nobody looked at.
func (s *Service) ConvertApplication(ctx context.Context, id uuid.UUID, ip, ua string) (string, error) {
if s.db == nil {
return "", ErrNoDatabase
}
actor := actorPtr(ctx)
var candID string
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
var name, email, phone, loc, resumeKey, resumeName, resumeType, resumeSum string
var existing *string
e := tx.QueryRow(ctx, `
SELECT full_name, email, phone, location_text, resume_storage_key, resume_file_name,
resume_media_type, resume_sha256, candidate_id::text
FROM job_applications WHERE id = $1 AND status <> 'converted'`, id).
Scan(&name, &email, &phone, &loc, &resumeKey, &resumeName, &resumeType, &resumeSum, &existing)
if errors.Is(e, pgx.ErrNoRows) {
return ErrNotFound
}
if e != nil {
return e
}
// An address already on the bench is REUSED rather than duplicated. An inbound application from
// somebody a recruiter sourced last year is the same person, and creating a second row would split
// their history in half.
var found uuid.UUID
e = tx.QueryRow(ctx, `SELECT id FROM candidates WHERE lower(email) = lower($1) AND deleted_at IS NULL
LIMIT 1`, email).Scan(&found)
switch {
case e == nil:
candID = found.String()
case errors.Is(e, pgx.ErrNoRows):
// CLEARANCE IS NOT CARRIED ACROSS. What the applicant typed is self reported text on the
// application, and writing it into the candidate's clearance field would turn an unverified
// claim into a scored attribute. A recruiter sets it after they have checked.
e = tx.QueryRow(ctx, `
INSERT INTO candidates (organization_id, full_name, email, location, clearance_level,
clearance_status, years_experience, available)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid,
$1,$2,$3,'unknown','unknown',0,true)
RETURNING id::text`, name, email, nullIfEmpty(loc)).Scan(&candID)
if e != nil {
return e
}
default:
return e
}
// The resume follows the person, so an inbound applicant arrives with their file attached rather
// than with a note saying one exists somewhere.
if resumeKey != "" {
if _, e := tx.Exec(ctx, `
INSERT INTO candidate_documents (organization_id, candidate_id, document_type, file_name,
media_type, storage_key, sha256, uploaded_by_user_id, extraction_status)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid,
$1,'resume_original',$2,$3,$4,$5,$6,'pending')
ON CONFLICT DO NOTHING`,
candID, resumeName, resumeType, resumeKey, resumeSum, actor); e != nil {
return e
}
}
if _, e := tx.Exec(ctx, `
UPDATE job_applications SET status = 'converted', candidate_id = $2::uuid,
reviewed_by = $3, reviewed_at = now()
WHERE id = $1`, id, candID, actor); e != nil {
return e
}
if s.aud == nil {
return nil
}
body, _ := json.Marshal(map[string]any{"application_id": id.String(), "candidate_id": candID})
return s.aud.OnTx(ctx, tx, audit.Event{
Action: "careers.application.converted", ActorUserID: actor,
EntityType: "job_application", EntityID: id.String(), NewValues: body,
IPAddress: ip, UserAgent: ua,
})
})
return candID, err
}
func actorPtr(ctx context.Context) *uuid.UUID {
p, ok := tenant.UserFromContext(ctx)
if !ok {
return nil
}
id, err := uuid.Parse(p.UserID)
if err != nil {
return nil
}
return &id
}