feat(programs): archive / un-archive programs (soft-archive)#954
Open
jee7s wants to merge 1 commit into
Open
Conversation
Add Program.archivedAt (soft-archive, @sensitivity:internal), orthogonal to ProgramPhase: FINISHED is a lifecycle fact, archived is a board visibility/ activity decision. Board/sysadmin archive & un-archive on the program-ops program page (rides PATCH /api/programs/[id] via an `archived` flag). Archived programs are hidden from default lists/pickers/dashboards (public catalog, program-ops list with an "Include archived" toggle, session/trends pickers, nav todo-counts), and frozen for new activity — enrollment, payment-plan/scholarship requests, and volunteer signup return 409 before any Shopify inventory hold/decrement (no inventory leak), and a maxParticipants edit no longer pushes to Shopify. Crons skip archived programs (pending-participants, scholarship-grace-expiry, post-event). Existing roster/ history stays readable; lead read-scope is unchanged. No Shopify-side archive in this PR (sibling PR; integration point noted in the design doc). Shared filter/guard in src/lib/program/archive.ts. See docs/designs/PROGRAM_ARCHIVE.md. 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
Adds a reversible soft-archive for programs so the board can retire a
mistaken / cancelled / duplicate / long-dead program from the active surfaces
without deleting it (which would destroy the participant history the ops surfaces
still need). Design:
checkin-app/docs/designs/PROGRAM_ARCHIVE.md.New field
Program.archivedAt DateTime?(@sensitivity:internal). NULL = live;non-null = archived.
Decisions (from the product interview)
phase.phase/FINISHEDis a lifecycle fact (did it run?);archived is a visibility/activity decision (should we still surface it?). A
program in any phase can be archived. A separate nullable timestamp keeps both
facts independently true and makes un-archive a trivial
NULLset that restoresthe prior phase.
Archive / Un-archive button. It rides the existing
PATCH /api/programs/[id]viaan
archivedboolean gated toisSysadmin || isBoardMember(same gate theShopify-identifier fields already use) — no new route, no new route-auth surface.
list with an "Include archived" toggle, session & trends pickers, nav
todo-counts) via a shared
NOT_ARCHIVEDwhere-fragment.includeArchived=trueisboard-gated;
archivedAtis tierinternalso it is stripped from theGET /api/programs/[id]response for everyone but board/sysadmin./api/programs/mine)— that's enrollment history, not a catalog.
payment-plan/scholarship request, and volunteer signup. The guard fires before
any inventory hold/decrement, so a blocked action can never leak Shopify inventory
(respects the Shopify single-pool capacity + scholarship lifecycle inventory (supersedes two-pool mirror) #930 single-pool capacity model). A
maxParticipantsedit still savesto the DB but is not pushed to Shopify while archived. Enrollment blocks even a board
force-enroll override (archived is a hard freeze, not an over-ridable soft limit).
activity), metadata/phase edits. Lead-mentor read scope is unchanged
(
buildCallerContextuntouched) — history stays readable exactly as before.Changed:
pending-participants,scholarship-grace-expiry,post-event/nightlypost-event leg. Untouched: nightly facility-close leg, membership-renewals,
person-bg-annual, trusted-adult-expiry.
archive its Shopify listing / zero inventory — that's a sibling PR. Integration
point (the
archivedtransition inPATCH /api/programs/[id]) is noted in the doc.Data model / migration
20260708020000_program_archive: one additive, nullable column, no backfill, noindex. NULL = "not archived", so every existing program keeps its current behavior.
Expand-only.
Testing
programArchive.integration.test.ts(8 cases): board archive stampsarchivedAt→ hidden from default list (board can opt in; member can't) → blockedself-enroll (409, no row) → blocked board force-enroll override → blocked
payment-plan with no
-1Shopify inventory side effect (dev-mock log assertedabsent,
inventoryHeldAtstays null) → blocked volunteer signup → un-archiverestores list visibility + enrollment → cron-skip: pending-participants sweep
leaves an archived program's stale row but kicks a live one.
tsc --noEmitandeslint --max-warnings 0.🤖 Generated with Claude Code