Skip to content

feat(membership): escalating background-check nudges for 18+ students#961

Open
jee7s wants to merge 2 commits into
mainfrom
feat/bg-student-nudges
Open

feat(membership): escalating background-check nudges for 18+ students#961
jee7s wants to merge 2 commits into
mainfrom
feat/bg-student-nudges

Conversation

@jee7s

@jee7s jee7s commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What & why

cron/person-bg-annual already OPENS a PERSON_BG obligation for every
program-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)

  • Nudges + consent link, NO blocking. Each email carries
    backgroundCheckProvider.getConsentDeepLink() (Averity) and points at the
    self-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).
  • Schedule: open, +14d, +30d, then monthly (dueThresholdDay, a pure
    function). Two escalations in the first month while a reminder is still timely,
    then a steady monthly poke for the long tail.
  • Recipients mirror the existing trusted-adult notifications
    (emailHouseholdLeads → household leads) plus the student themselves when
    they have an email on file (deduped).
  • Dedup ledger: a new additive PersonBgNudge table with
    @@unique([processId, thresholdDay]). The sweep inserts before it emails, so a
    re-run at the same stage is a no-op.
  • Standalone off main, not dependent on the open feat: auto-notifications as things go stale #958 sibling PR. The design
    doc spells out the fold-in: drop this table into feat: auto-notifications as things go stale #958's NotificationLedger and
    register a PERSON_BG_NUDGE type — a registration + table merge, not a rewrite.

Where it runs

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. Adding the schedule is an infra follow-up (noted in the doc).

Clearing stops nudges naturally

No explicit stop signal: a submitted (bgConsentAt set), cleared
(lastBackgroundCheck stamped → ACTIVE), under-18 (MINOR), or already-fresh
subject drops out of the open-and-still-NEEDED set. The eligibility recheck
(personBgVerdict) mirrors person-bg-annual exactly.

Testing evidence

  • Unit (personBgNudge.test.ts): the threshold schedule (0/14/30/monthly) +
    monotonicity; withCron auth gate (missing/wrong secret → 401, envelope on 200).
  • Integration (personBgNudge.integration.test.ts): a full sweep — obligation
    open → 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.
  • Full pre-commit suite green (1716 tests), tsc --noEmit clean,
    eslint --max-warnings 0 clean. Migration applies cleanly under the
    integration harness's migrate deploy.

🤖 Generated with Claude Code

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
@jee7s jee7s added the post-first-release Deferred until after the first production release label Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

post-first-release Deferred until after the first production release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant