Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/recruiting/exceptions.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/recruiting/exceptions.go |
| Original SHA-256 | 8373e4ce30e20716a1f75f3497e99ed0b101bac8d48278aa42afb0f760655848 |
| Git blob SHA | 03ff504afabd01d677cbde189877cadf2b743913 |
| Reading-copy SHA-256 | 8373e4ce30e20716a1f75f3497e99ed0b101bac8d48278aa42afb0f760655848 |
Recovered source transcript
Download complete source text ↓373 lines · 13.8 KB
package recruiting
import (
"context"
"strconv"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
// THE MANAGER EXCEPTION QUEUE.
//
// Alerts are organization wide and ordered by when they happened. That is the right shape for a recruiter
// working their own desk and the wrong shape for a manager, who does not need to know that something
// happened, they need to know WHICH OF THEIR PEOPLE has a problem they are not going to solve on their own.
//
// A manager reading an org wide feed has to hold the team roster in their head and filter by eye, which is
// exactly the work software should be doing. So this asks three questions the alert feed cannot:
//
// an order nobody has touched the job order is open and no activity has been logged against it
// a desk that has gone quiet the recruiter holds open orders and has logged nothing at all
// a redeployment nobody picked up somebody is rolling off and the alert is still sitting unread
//
// EVERY ONE OF THESE IS A READ OVER DATA ALREADY HELD. No new table, no new column, no migration. The
// facts were all present and simply had nobody asking the manager's version of the question.
const (
// agingOrderDays is how long an open job order may go untouched before it is an exception.
//
// Two weeks, because a requisition is a commitment to a client with a fill date attached, and the
// common failure is not a recruiter refusing to work an order, it is an order quietly falling behind
// the noisier ones. Two weeks is long enough that a busy fortnight does not raise noise and short
// enough that the order is still recoverable when it surfaces.
agingOrderDays = 14
// silentDeskDays is how long a recruiter may hold open orders while logging nothing.
//
// Deliberately SHORTER than the order threshold. An order going quiet is a prioritisation problem. A
// whole desk going quiet is a person problem: illness, leave nobody recorded, a resignation in
// progress, or somebody stuck. Those want a conversation this week, not in a fortnight.
silentDeskDays = 7
// exceptionListLimit bounds the queue, for the same reason the alerts feed and the key personnel list
// are bounded. A manager acts on the worst handful; nobody scrolls three thousand exceptions.
exceptionListLimit = 200
)
// Exception is one thing a manager should look at, phrased as a sentence rather than a row of codes.
type Exception struct {
Kind string `json:"kind"`
Severity string `json:"severity"`
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Title string `json:"title"`
Detail string `json:"detail"`
OwnerUserID string `json:"owner_user_id,omitempty"`
OwnerName string `json:"owner_name,omitempty"`
// Days is what the queue sorts on: the longest neglected thing is the most urgent one.
Days int `json:"days"`
}
// ExceptionQueue is the manager's list, already scoped to the people they asked about.
type ExceptionQueue struct {
Scope string `json:"scope"`
ScopeLabel string `json:"scope_label"`
Exceptions []Exception `json:"exceptions"`
// Counts is per kind, so the interface can show three headline numbers without counting the list and
// without a second request.
Counts map[string]int `json:"counts"`
// Truncated says plainly that the list was cut. A queue that silently stops at two hundred reads as
// "you have two hundred problems" when the real number is larger, and a manager who acts on that
// number is acting on a number the product made up.
Truncated bool `json:"truncated"`
Total int `json:"total"`
}
// Exception kinds, named once so the interface and the service cannot drift.
const (
KindAgingOrder = "aging_order"
KindSilentDesk = "silent_desk"
KindUnclaimedRedeployment = "unclaimed_redeployment"
)
// Exceptions builds the queue for one scope.
//
// Scope resolution is REUSED from the manager view rather than reimplemented, so the two screens cannot
// disagree about who is on a desk. That matters more than it looks: a manager comparing a capacity figure
// on one screen with an exception count on another must be able to trust that both mean the same team,
// including the empty team case that would otherwise silently widen to the whole organization.
func (s *Service) Exceptions(ctx context.Context, teamID, owner string) (ExceptionQueue, error) {
if s.db == nil {
return ExceptionQueue{}, ErrNoDatabase
}
out := ExceptionQueue{Exceptions: []Exception{}, Counts: map[string]int{}}
err := s.db.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error {
ids, scope, label, err := s.resolveScope(ctx, tx, teamID, owner)
if err != nil {
return err
}
out.Scope, out.ScopeLabel = scope, label
// A nil slice means the whole organization, which the queries read as "no owner filter". An empty
// team arrives here as a slice holding the nil uuid, which matches nobody, which is correct.
var filter any
if ids != nil {
filter = ids
}
for _, read := range []func(context.Context, pgx.Tx, any) ([]Exception, error){
readAgingOrders, readSilentDesks, readUnclaimedRedeployments,
} {
found, err := read(ctx, tx, filter)
if err != nil {
return err
}
out.Exceptions = append(out.Exceptions, found...)
}
return nil
})
if err != nil {
return ExceptionQueue{}, err
}
for _, e := range out.Exceptions {
out.Counts[e.Kind]++
}
out.Total = len(out.Exceptions)
// Longest neglected first, across all three kinds together. A manager wants the worst thing on their
// team at the top, not the worst thing within each category, because categories are the product's way
// of organizing the problem and not the manager's.
sortByDaysDesc(out.Exceptions)
if len(out.Exceptions) > exceptionListLimit {
out.Exceptions = out.Exceptions[:exceptionListLimit]
out.Truncated = true
}
return out, nil
}
// sortByDaysDesc is an insertion sort, which is the right choice here and not laziness: the list is
// bounded by what one team can neglect, it is nearly sorted already because each reader returns ordered
// rows, and insertion sort is linear on nearly sorted input.
func sortByDaysDesc(xs []Exception) {
for i := 1; i < len(xs); i++ {
v := xs[i]
j := i - 1
for j >= 0 && xs[j].Days < v.Days {
xs[j+1] = xs[j]
j--
}
xs[j+1] = v
}
}
// readAgingOrders finds open job orders nobody has logged activity against.
//
// The measurement is from the LAST ACTIVITY, falling back to when the order opened. An order opened
// yesterday with no activity is not neglected, it is new, and an exception queue that cannot tell those
// apart trains managers to ignore it.
func readAgingOrders(ctx context.Context, tx pgx.Tx, filter any) ([]Exception, error) {
q := `
WITH last_touch AS (
SELECT requisition_id, max(occurred_at) AS at
FROM recruiter_activities
WHERE requisition_id IS NOT NULL
GROUP BY requisition_id)
SELECT r.id,
r.title,
r.owner_user_id,
coalesce(u.full_name, ''),
(extract(epoch FROM now() - coalesce(lt.at, r.opened_at, r.created_at)) / 86400)::int AS days,
lt.at IS NULL AS never_touched
FROM requisitions r
LEFT JOIN last_touch lt ON lt.requisition_id = r.id
LEFT JOIN users u ON u.id = r.owner_user_id
WHERE r.deleted_at IS NULL
AND r.status = 'open'
AND ($1::uuid[] IS NULL OR r.owner_user_id = ANY($1))
AND coalesce(lt.at, r.opened_at, r.created_at) < now() - make_interval(days => $2)
ORDER BY days DESC
LIMIT ` + strconv.Itoa(exceptionListLimit)
rows, err := tx.Query(ctx, q, filter, agingOrderDays)
if err != nil {
return nil, translatePG(err)
}
defer rows.Close()
var out []Exception
for rows.Next() {
var (
id uuid.UUID
title string
ownerID *uuid.UUID
ownerName string
days int
neverTouched bool
)
if err := rows.Scan(&id, &title, &ownerID, &ownerName, &days, &neverTouched); err != nil {
return nil, err
}
e := Exception{
Kind: KindAgingOrder, SubjectType: "requisition", SubjectID: id.String(),
Title: title, OwnerName: ownerName, Days: days,
Severity: severityFor(days, agingOrderDays),
}
if neverTouched {
e.Detail = "Open " + plural(days, "day") + " and nothing has ever been logged against it"
} else {
e.Detail = "No activity logged for " + plural(days, "day")
}
if ownerID != nil {
e.OwnerUserID = ownerID.String()
} else {
e.Detail += ", and nobody holds it"
}
out = append(out, e)
}
return out, translatePG(rows.Err())
}
// readSilentDesks finds recruiters holding open orders who have logged nothing at all.
//
// The condition is deliberately about the PERSON, not about any one record. A recruiter can have every
// individual order look recently touched and still have gone quiet, and the reverse is also true, so this
// is a genuinely different question from the one above rather than a rollup of it.
func readSilentDesks(ctx context.Context, tx pgx.Tx, filter any) ([]Exception, error) {
q := `
WITH holders AS (
SELECT owner_user_id AS uid, count(*)::int AS open_orders
FROM requisitions
WHERE deleted_at IS NULL AND status = 'open' AND owner_user_id IS NOT NULL
AND ($1::uuid[] IS NULL OR owner_user_id = ANY($1))
GROUP BY owner_user_id),
last_activity AS (
SELECT actor_user_id AS uid, max(occurred_at) AS at
FROM recruiter_activities
GROUP BY actor_user_id)
SELECT h.uid,
coalesce(u.full_name, ''),
h.open_orders,
CASE WHEN la.at IS NULL THEN NULL
ELSE (extract(epoch FROM now() - la.at) / 86400)::int END AS days
FROM holders h
LEFT JOIN last_activity la ON la.uid = h.uid
LEFT JOIN users u ON u.id = h.uid
WHERE la.at IS NULL OR la.at < now() - make_interval(days => $2)
ORDER BY days DESC NULLS FIRST
LIMIT ` + strconv.Itoa(exceptionListLimit)
rows, err := tx.Query(ctx, q, filter, silentDeskDays)
if err != nil {
return nil, translatePG(err)
}
defer rows.Close()
var out []Exception
for rows.Next() {
var (
uid uuid.UUID
name string
orders int
days *int
)
if err := rows.Scan(&uid, &name, &orders, &days); err != nil {
return nil, err
}
e := Exception{
Kind: KindSilentDesk, SubjectType: "user", SubjectID: uid.String(),
Title: name, OwnerUserID: uid.String(), OwnerName: name,
}
if days == nil {
// Never logged anything at all. Sorted to the top by a deliberately large number rather than
// by a real measurement, because there is no elapsed time to measure and reporting zero would
// bury the most serious case at the bottom of the queue.
e.Days = 9999
e.Severity = "act"
e.Detail = "Holds " + plural(orders, "open order") + " and has never logged any activity"
} else {
e.Days = *days
e.Severity = severityFor(*days, silentDeskDays)
e.Detail = "Holds " + plural(orders, "open order") + " and has logged nothing for " + plural(*days, "day")
}
out = append(out, e)
}
return out, translatePG(rows.Err())
}
// readUnclaimedRedeployments finds people rolling off whose alert is still sitting unread.
//
// This closes the loop opened in Tier 1. The redeployment alert fires when a contractor is finishing and
// has no next assignment booked. Nobody was asking whether anyone had actually PICKED IT UP, so the alert
// could sit unread until the person was already out of access and the whole point of the early warning
// was gone.
func readUnclaimedRedeployments(ctx context.Context, tx pgx.Tx, filter any) ([]Exception, error) {
// Alerts have no owner column, so they are scoped through the placement's owning recruiter. An alert
// whose subject cannot be traced to an owner is still shown when the scope is the whole organization,
// because an unowned redeployment is precisely the kind nobody is going to pick up.
q := `
SELECT a.id,
a.title,
a.detail,
(extract(epoch FROM now() - a.created_at) / 86400)::int AS days,
r.owner_user_id,
coalesce(u.full_name, '')
FROM alerts a
LEFT JOIN placements p ON p.id = a.subject_id AND a.subject_type = 'placement'
LEFT JOIN requisitions r ON r.id = p.requisition_id
LEFT JOIN users u ON u.id = r.owner_user_id
WHERE a.alert_type = 'redeployment_due'
AND a.status = 'unread'
AND ($1::uuid[] IS NULL OR r.owner_user_id = ANY($1))
ORDER BY days DESC
LIMIT ` + strconv.Itoa(exceptionListLimit)
rows, err := tx.Query(ctx, q, filter)
if err != nil {
return nil, translatePG(err)
}
defer rows.Close()
var out []Exception
for rows.Next() {
var (
id uuid.UUID
title string
detail string
days int
ownerID *uuid.UUID
ownerName string
)
if err := rows.Scan(&id, &title, &detail, &days, &ownerID, &ownerName); err != nil {
return nil, err
}
e := Exception{
Kind: KindUnclaimedRedeployment, SubjectType: "alert", SubjectID: id.String(),
Title: title, Detail: detail, OwnerName: ownerName, Days: days,
Severity: severityFor(days, 3),
}
if e.Detail == "" {
e.Detail = "Unread for " + plural(days, "day")
}
if ownerID != nil {
e.OwnerUserID = ownerID.String()
}
out = append(out, e)
}
return out, translatePG(rows.Err())
}
// severityFor turns elapsed time into one of two words.
//
// Two, not five. A manager scanning a queue makes a binary decision, look now or look later, and a five
// level scale asks them to do arithmetic on a colour before they can make it. Double the threshold is the
// line: once something has been neglected for twice as long as the rule allows, it stopped being a busy
// week and became a thing nobody is going to fix without being asked.
func severityFor(days, threshold int) string {
if days >= threshold*2 {
return "act"
}
return "watch"
}
// plural writes a count and its noun as a person would say it, so the interface never renders "1 days".
func plural(n int, noun string) string {
if n == 1 {
return "1 " + noun
}
return strconv.Itoa(n) + " " + noun + "s"
}