Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/recruiting/requisition_status.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/recruiting/requisition_status.go |
| Original SHA-256 | 42ced5d4aa2bdf1a7cc0de4aaf7ef127ca5b832b6050a1addef0ad6d477b5e45 |
| Git blob SHA | 4753c6c105911959e3a04a45e11c4744a104025c |
| Reading-copy SHA-256 | 42ced5d4aa2bdf1a7cc0de4aaf7ef127ca5b832b6050a1addef0ad6d477b5e45 |
Every recorded occurrence
These 2 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/recruiting/requisition_status.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/recruiting/requisition_status.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓145 lines · 6.8 KB
package recruiting
import (
"time"
"github.com/shopspring/decimal"
)
// The requisition lifecycle, expressed as data rather than as a chain of if statements, so that every
// one of the thirty six ordered pairs of statuses is decided in one place and provable in one test.
//
// A job order is not a free form field with a status label on it. It is opened, it may be paused while
// the client reorganizes, it ends exactly once, and the time it spent paused is subtracted from how long
// it took to fill. Every metric this product will report about recruiting speed is a function of those
// three facts, so the transitions and the clocks they move are defined together here and nowhere else.
const (
reqDraft = "draft"
reqOpen = "open"
reqOnHold = "on_hold"
reqFilled = "filled"
reqClosed = "closed"
reqCancelled = "cancelled"
)
// reqStatuses is the vocabulary. It is pinned against the CHECK constraint in migration 0047 by a test,
// because a value this code accepts and the database refuses is a 500 rather than a clean rejection.
var reqStatuses = map[string]bool{
reqDraft: true, reqOpen: true, reqOnHold: true,
reqFilled: true, reqClosed: true, reqCancelled: true,
}
// reqTerminal are the statuses that end an order and therefore must carry closed_at. Kept as its own
// set rather than inlined so the closed_at rule and the CHECK constraint in 0047 cannot drift apart.
var reqTerminal = map[string]bool{reqFilled: true, reqClosed: true, reqCancelled: true}
// reqTransitions is the legal graph.
//
// The shape encodes real recruiting rather than a tidy diagram. A draft has not been released to anyone,
// so it cannot be held or filled, only opened or abandoned. An order that ended can be reopened, because
// a placement falling through in week two is ordinary and forcing a new requisition would break the
// history of the role. Cancelled is reopenable for the same reason a delete needs an undo: it is the one
// transition that is usually a misclick.
var reqTransitions = map[string]map[string]bool{
reqDraft: {reqOpen: true, reqCancelled: true},
reqOpen: {reqOnHold: true, reqFilled: true, reqClosed: true, reqCancelled: true},
reqOnHold: {reqOpen: true, reqFilled: true, reqClosed: true, reqCancelled: true},
reqFilled: {reqOpen: true, reqClosed: true},
reqClosed: {reqOpen: true},
reqCancelled: {reqOpen: true},
}
// statusEffect is what a transition does to the three clocks. Separating the decision from the write
// means the arithmetic is testable without a database, and the SQL becomes a direct transcription of a
// value rather than a place where the rule is re derived.
type statusEffect struct {
// NoOp is a transition to the same status. It is accepted and changes nothing, which makes a repeated
// request idempotent rather than an error, and records no event so the history is not padded with
// entries where nothing happened.
NoOp bool
SetClosedAt bool
ClearClosedAt bool
StartHold bool
SettleHold bool
}
// transitionFor decides a status change.
//
// The four clock rules are stated as biconditionals against the target status rather than enumerated per
// pair. That is what makes the two database invariants hold for every legal transition by construction:
// after applying the effect, closed_at is non null exactly when the target is terminal, and held_since is
// non null exactly when the target is on hold. TestTransitionPreservesInvariants proves it over the whole
// graph rather than trusting the argument.
func transitionFor(from, to string) (statusEffect, error) {
if !reqStatuses[to] {
return statusEffect{}, &ValidationError{Field: "status", Message: "is not a permitted status"}
}
if from == to {
return statusEffect{NoOp: true}, nil
}
if !reqTransitions[from][to] {
return statusEffect{}, &ValidationError{
Field: "status",
Message: "a " + from + " requisition cannot move to " + to,
}
}
return statusEffect{
SetClosedAt: reqTerminal[to] && !reqTerminal[from],
ClearClosedAt: !reqTerminal[to] && reqTerminal[from],
StartHold: to == reqOnHold,
// Leaving a hold settles it even when the destination is terminal. Without this, an order closed
// while paused keeps an open hold bracket forever and its accrued hold time grows without bound
// against a dead row, which would make its adjusted age fall below zero and keep falling.
SettleHold: from == reqOnHold,
}, nil
}
// reqAge computes how long an order has been alive and how long it was actually being worked.
//
// Both numbers are legitimate and the industry does not agree on which to report, so both are returned
// and the caller labels them. Elapsed is the wall clock. Adjusted subtracts every period the order spent
// on hold, including the hold currently in progress, which is the number that answers "how long did this
// take us" without punishing a recruiter for a client's hiring freeze.
//
// Arithmetic is exact decimal on a nanosecond integer, never float64. Seconds rather than calendar days
// because a day is not always 86400 seconds across a daylight saving boundary, and a duration expressed
// in days would silently gain or lose an hour twice a year.
func reqAge(openedAt time.Time, closedAt *time.Time, heldSeconds decimal.Decimal, heldSince *time.Time, now time.Time) (elapsed, adjusted decimal.Decimal) {
end := now
if closedAt != nil {
end = *closedAt
}
elapsed = secondsBetween(openedAt, end)
hold := heldSeconds
if heldSince != nil {
// A hold in progress accrues up to the same end point used for elapsed, so the two measurements
// are taken from one instant and cannot disagree.
hold = hold.Add(secondsBetween(*heldSince, end))
}
adjusted = elapsed.Sub(hold)
if adjusted.IsNegative() {
// Unreachable given the invariants: hold periods are disjoint subsets of the interval from
// opened_at to end, because status holds one value at a time and every bracket is settled when it
// closes, so their sum cannot exceed the whole. Clamped rather than returned negative because a
// negative age is never a real measurement, and TestAdjustedAgeIsNeverNegative asserts that this
// branch does not fire for any legal sequence of transitions.
return elapsed, decimal.Zero
}
return elapsed, adjusted
}
// secondsBetween is an exact elapsed second count. Sub returns an integer nanosecond count, so dividing
// by 1e9 in decimal is lossless, whereas Seconds() would round through a float64.
func secondsBetween(from, to time.Time) decimal.Decimal {
return decimal.NewFromInt(to.Sub(from).Nanoseconds()).Div(decimal.NewFromInt(1e9))
}
// days renders a second count as days to three places, which is the unit a recruiter reads. Rounding is
// applied only here, at the display boundary, so no stored or accumulated value is ever rounded.
func days(seconds decimal.Decimal) float64 {
d, _ := seconds.Div(decimal.NewFromInt(86400)).Round(3).Float64()
return d
}