Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/recruiting/errors.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/recruiting/errors.go |
| Original SHA-256 | d307174b50e4e4de6e57c804bb1e37217576b511f925e64896b0086dc0b73e03 |
| Git blob SHA | a34cc4df993280a7449379ff820e3a856a134041 |
| Reading-copy SHA-256 | d307174b50e4e4de6e57c804bb1e37217576b511f925e64896b0086dc0b73e03 |
Recovered source transcript
Download complete source text ↓122 lines · 6.6 KB
package recruiting
import (
"errors"
"fmt"
"strings"
"github.com/jackc/pgx/v5/pgconn"
)
var (
ErrNoDatabase = errors.New("recruiting: no database configured")
ErrNotFound = errors.New("recruiting: not found")
// ErrConflict is an optimistic concurrency failure: the row moved after the caller read it.
ErrConflict = errors.New("recruiting: the record was changed by someone else")
ErrInvalid = errors.New("recruiting: invalid input")
)
// DuplicateError names the field that collided so the caller can point at the right input rather than
// showing a generic failure.
type DuplicateError struct{ Field string }
func (e *DuplicateError) Error() string { return "recruiting: duplicate " + e.Field }
// ValidationError carries a per field message. Returning the field lets the UI mark the offending input
// instead of dropping one sentence at the top of a form.
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string { return e.Field + ": " + e.Message }
func (e *ValidationError) Unwrap() error { return ErrInvalid }
// translatePG converts a Postgres integrity failure into a typed domain error.
//
// The database is the only place uniqueness can be checked without a race, so the service inserts and
// interprets the result rather than pre checking with a SELECT and hoping nothing changes in between.
// Doing it the other way round produces a 500 under concurrency, which is exactly the case that matters.
//
// The constraint NAMES are matched deliberately. They are declared in migrations 0047 and 0048 and
// changing one without changing this mapping would silently turn a clean 409 back into a 500, so the
// names are load bearing and the tests pin them.
func translatePG(err error) error {
var pg *pgconn.PgError
if !errors.As(err, &pg) {
return err
}
switch pg.Code {
case "23505": // unique_violation
switch {
case strings.Contains(pg.ConstraintName, "uq_clients_org_name"):
return &DuplicateError{Field: "name"}
case strings.Contains(pg.ConstraintName, "uq_clients_org_uei"):
return &DuplicateError{Field: "uei"}
// The same candidate cannot be on one job order twice. Postgres names this constraint after its
// columns, and without this case the raw name reached the user as "that
// applications_organization_id_requisition_id_candidate_id_key is already in use".
case strings.Contains(pg.ConstraintName, "applications_organization_id_requisition_id_candidate"):
return &DuplicateError{Field: "candidate"}
case strings.Contains(pg.ConstraintName, "uq_client_contacts_primary"):
// Reached only if the demote and promote were not done in one transaction. The service does
// them together, so this is a guard against a future caller taking a shortcut.
return &DuplicateError{Field: "is_primary"}
default:
return &DuplicateError{Field: pg.ConstraintName}
}
case "23514": // check_violation
switch {
case strings.Contains(pg.ConstraintName, "clients_name_check"):
return &ValidationError{Field: "name", Message: "must be between 1 and 200 characters"}
// Rate ranges. Only one end of a range is often being changed, and the other end is whatever is
// already stored, so the database is the only place the pair can be compared. Naming the field
// turns an unactionable failure into one the form can mark.
case strings.Contains(pg.ConstraintName, "requisitions_bill_range_chk"):
return &ValidationError{Field: "bill_rate_min", Message: "cannot be greater than the maximum bill rate"}
case strings.Contains(pg.ConstraintName, "requisitions_pay_range_chk"):
return &ValidationError{Field: "pay_rate_min", Message: "cannot be greater than the maximum pay rate"}
case strings.Contains(pg.ConstraintName, "requisitions_salary_range_chk"):
return &ValidationError{Field: "salary_min", Message: "cannot be greater than the maximum salary"}
case strings.Contains(pg.ConstraintName, "requisitions_rates_nonneg_chk"):
return &ValidationError{Field: "bill_rate_min", Message: "rates cannot be negative"}
case strings.Contains(pg.ConstraintName, "requisitions_target_after_open_chk"):
return &ValidationError{Field: "target_fill_date", Message: "cannot be before the date the order was opened"}
case strings.Contains(pg.ConstraintName, "requisitions_title_check"):
return &ValidationError{Field: "title", Message: "must be between 1 and 300 characters"}
case strings.Contains(pg.ConstraintName, "requisitions_openings_check"):
return &ValidationError{Field: "openings", Message: "must be at least 1"}
// The two clock invariants from 0047 and 0052. Reaching either means the transition logic moved a
// status without moving the clock that status implies, which is a defect in this service rather
// than bad input, so it is reported as an internal failure and not blamed on the caller.
case strings.Contains(pg.ConstraintName, "requisitions_closed_at_chk"),
strings.Contains(pg.ConstraintName, "requisitions_held_since_chk"):
return fmt.Errorf("recruiting: requisition clock invariant %s violated: %w", pg.ConstraintName, err)
case strings.Contains(pg.ConstraintName, "status"):
return &ValidationError{Field: "status", Message: "is not a permitted value"}
// The VMS intake rules from migration 0102. They are named here rather than left to the generic
// fallback because both are ordinary data entry mistakes with an obvious fix, and "failed a database
// constraint, field ck_requisitions_vms_named" tells a recruiter nothing they can act on.
case strings.Contains(pg.ConstraintName, "ck_requisitions_vms_named"):
return &ValidationError{Field: "vms_name",
Message: "is required when the requisition came through a vendor management system, " +
"because a submission cannot be reconciled without knowing which portal it came from"}
case strings.Contains(pg.ConstraintName, "ck_requisitions_vms_counts"):
return &ValidationError{Field: "competing_vendors",
Message: "and max_submissions cannot be negative"}
default:
return &ValidationError{Field: pg.ConstraintName, Message: "failed a database constraint"}
}
case "23503": // foreign_key_violation
// The composite key added in 0052 carries client_id, so this fires when a contact exists but works
// at a different client. "Not found" would be misleading, because the contact is right there.
if strings.Contains(pg.ConstraintName, "requisitions_hiring_manager_fkey") {
return &ValidationError{Field: "hiring_manager_id", Message: "is not a contact at this client"}
}
return ErrNotFound
case "23502": // not_null_violation
return &ValidationError{Field: pg.ColumnName, Message: "is required"}
}
return fmt.Errorf("recruiting: database error: %w", err)
}