feat: sync program mailing lists into Google Groups#960
Open
jee7s wants to merge 1 commit into
Open
Conversation
Keep a program's Google Group membership in step with its active participants. Config is a board-set Program.googleGroupEmail (validated as an email); membership resolves to the participant plus their household leads, mirroring how lib/notifications resolves recipients today. - lib/googleGroups.ts: Directory API client — JWT-bearer OAuth2 signed with node crypto (no new dep), token cache, timeouts, typed errors, idempotent add(409)/remove(404)/list. fetch seam mirrors lib/shopify.ts. - lib/program/groupSync.ts: the shared service. Event pushes on activation (orders/paid webhook, payment-plan approval, free/override enroll) and on removal (inside withdrawAndReleaseHold, the one shared exit path) are best-effort — a Google failure never fails the triggering user action. - Nightly reconcile cron + board-only "Sync now" button do the full diff (add missing, remove MEMBER-role extras; OWNER/MANAGER kept). - Cleanly OFF when GOOGLE_SA_KEY_JSON / GOOGLE_SA_SUBJECT are unset: config UI works, sync paths log-and-skip, config-health reports it unconfigured (mirrors the Zoho/Shopify idiom). Real creds + cron schedule are an infra follow-up — see docs/designs/PROGRAM_GOOGLE_GROUP_SYNC.md. Migration 20260708050000: additive nullable Program.googleGroupEmail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Programs want a single mailing list their active participants can reach. This
wires program → Google Group membership sync so the app keeps a group in
step with who is actively enrolled — add on activation, remove on withdrawal,
plus a nightly reconcile and a manual "Sync now" that self-heal drift.
Design:
checkin-app/docs/designs/PROGRAM_GOOGLE_GROUP_SYNC.md.Decisions (from the product interview)
Program.googleGroupEmail(@sensitivity:internal),validated as an email on the program-ops general tab. Null = no group.
mirroring exactly how
lib/notifications.ts › sendCheckinNotificationsresolvesrecipients today (documented in the design doc). A dependent with no email is
represented by their household lead(s).
three post-Shopify single-pool capacity + scholarship lifecycle inventory (supersedes two-pool mirror) #930 activation points (orders/paid webhook, payment-plan approval,
free/board-override enroll) and from the one shared exit path
(
withdrawAndReleaseHold). A Google failure never fails the triggering useraction — it logs + reports to System Status › Link Status; the reconcile heals.
missing, remove MEMBER-role extras (OWNER/MANAGER are never removed).
domain-wide delegation. OAuth2 is a JWT-bearer assertion signed with node
crypto— no new npm dependency (avoidsgoogleapisfor three endpoints).GOOGLE_SA_KEY_JSON/GOOGLE_SA_SUBJECTare absent the integration is off: config UI still works,sync paths log-and-skip, and
lib/configHealth.tsreports it unconfigured(mirrors the Zoho/Shopify idiom; a gap only in prod).
Infra follow-up (NOT in this PR)
Provisioning the service account (DWD for
admin.directory.group.member), setting the two env vars, and scheduling thereconcile cron are an explicit infra follow-up — see the design doc's
"Provisioning" section. The cron route exists but nothing invokes it yet.
Data model
Migration
20260708050000_program_google_group— additive, nullableProgram.googleGroupEmail TEXT.npx prisma generateregeneratedclassifications.ts(committed).Testing
lib/googleGroups.test.ts(11): JWT assertion shape, token caching,idempotent add(409)/remove(404), list pagination, failure + unconfigured paths.
lib/program/__tests__/groupSync.test.ts(11): recipient rule, removal safetynet (shared lead stays), reconcile diff, best-effort never-throws contract.
configHealth.test.tsupdated for the new check.fetch) —programGoogleGroupSync.integration.test.ts(5): config save (persist/normalizevalid, reject malformed, clear on empty) + a reconcile sync run.
authzRoleRejection.integration.test.tsgains the 401/403 deny-path for the newrole-gated route (registered in the authz drift guard).
test:ci),tsc --noEmit, andeslint --max-warnings 0on all touched files: green. Route auth drift-guard + config-health route tests: green.
🤖 Generated with Claude Code