feat(membership): escalating background-check nudges for 18+ students#961
Open
jee7s wants to merge 2 commits into
Open
feat(membership): escalating background-check nudges for 18+ students#961jee7s wants to merge 2 commits into
jee7s wants to merge 2 commits into
Conversation
cron/person-bg-annual already OPENS PERSON_BG obligations for program-attached people >=18 with no fresh check, and the compliance dashboard shows the board who needs one -- but nothing reaches the students. This adds the missing half: escalating email nudges to the student's household carrying the Averity consent deep link, with a dedup ledger so repeated daily cron runs never re-send the same threshold. - New daily cron GET /api/cron/person-bg-nudge (withCron + CRON_SECRET), kept separate from person-bg-annual so that open-obligations job stays single-purpose. Schedule is an infra follow-up. - Escalating schedule: open, +14d, +30d, then monthly (pure dueThresholdDay). - Recipients mirror the trusted-adult notifications (household leads) plus the student themselves when they have an email on file. - Dedup via a new additive PersonBgNudge table (unique(processId, thresholdDay)); record-before-send guarantees no re-send at the same threshold. - Warn-only: NO check-in / enrollment blocking (rejected alternative documented). - Clearing needs no signal: a submitted / cleared / under-18 / already-fresh subject drops out of the open-and-still-NEEDED set (eligibility mirrors person-bg-annual exactly), so nudges stop naturally. Design: checkin-app/docs/designs/BG_STUDENT_NUDGES.md (includes the fold-in plan for #958's registry + NotificationLedger -- a type registration + ledger merge, not a rewrite). Tests: unit for the threshold schedule + the withCron auth gate; integration for a full sweep (email captured with consent link, ledger blocks re-send, escalation fires, cleared/submitted/under-18 excluded, policy-off no-op). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RsD1zGYyqQFZaHT2wfkSB7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
cron/person-bg-annualalready OPENS aPERSON_BGobligation for everyprogram-attached person ≥18 with no fresh check, and the compliance dashboard
shows the board who needs one — but nothing reaches the students. This PR
adds the missing half: escalating email nudges to the student's household that
carry the Averity consent deep link, with dedup plumbing so repeated daily cron
runs never re-send the same threshold.
Design doc:
checkin-app/docs/designs/BG_STUDENT_NUDGES.md.Decisions (from the product interview)
backgroundCheckProvider.getConsentDeepLink()(Averity) and points at theself-attest path (
/membership, Applicant copy overpromises: BG consent is board-gated, so 'pay right away' stalls on a human #875) for after they've submitted. Warn-only —no check-in or enrollment blocking (rejected alternative documented in the
design doc).
dueThresholdDay, a purefunction). Two escalations in the first month while a reminder is still timely,
then a steady monthly poke for the long tail.
(
emailHouseholdLeads→ household leads) plus the student themselves whenthey have an email on file (deduped).
PersonBgNudgetable with@@unique([processId, thresholdDay]). The sweep inserts before it emails, so are-run at the same stage is a no-op.
main, not dependent on the open feat: auto-notifications as things go stale #958 sibling PR. The designdoc spells out the fold-in: drop this table into feat: auto-notifications as things go stale #958's
NotificationLedgerandregister a
PERSON_BG_NUDGEtype — a registration + table merge, not a rewrite.Where it runs
New daily cron
GET /api/cron/person-bg-nudge(withCron+CRON_SECRET), keptseparate from
person-bg-annualso that open-obligations job stayssingle-purpose. Adding the schedule is an infra follow-up (noted in the doc).
Clearing stops nudges naturally
No explicit stop signal: a submitted (
bgConsentAtset), cleared(
lastBackgroundCheckstamped →ACTIVE), under-18 (MINOR), or already-freshsubject drops out of the open-and-still-
NEEDEDset. The eligibility recheck(
personBgVerdict) mirrorsperson-bg-annualexactly.Testing evidence
personBgNudge.test.ts): the threshold schedule (0/14/30/monthly) +monotonicity; withCron auth gate (missing/wrong secret → 401, envelope on 200).
personBgNudge.integration.test.ts): a full sweep — obligationopen → email captured with the consent + self-attest links, sent to lead and
student; ledger blocks a same-stage re-send; day-14 escalation fires a second
nudge; cleared / submitted / under-18 subjects get nothing; policy-off no-op.
tsc --noEmitclean,eslint --max-warnings 0clean. Migration applies cleanly under theintegration harness's
migrate deploy.🤖 Generated with Claude Code