feat(programs): ProgramInstance tier + backfill (restructure phase 1)#953
Open
jee7s wants to merge 1 commit into
Open
feat(programs): ProgramInstance tier + backfill (restructure phase 1)#953jee7s wants to merge 1 commit into
jee7s wants to merge 1 commit into
Conversation
…e 1) Phase 1 of PROGRAM_INSTANCE_RESTRUCTURE.md: additive/expand-only introduction of the ProgramInstance offering tier between Program (definition) and Event. - New ProgramInstance model mirroring Program's offering columns + nullable narrowing overrides; nullable Event.instanceId FK; Program.instances / Person.instancesLed back-relations. Sensitivity-annotated; classifications regenerated. - Migration 20260708040000: generated additive DDL + hand-written idempotent 1:1 backfill (id-aliased instance per program, Event.instanceId=programId, sequence bump), wrapped in BEGIN/COMMIT. - ProgramInstance added to OPT_OUT_PENDING_ROUTE (no read consumers until the P3 claim swap). - Re-validated against #930 (single-pool capacity + hold ledger), NOT the unmerged #929: instance mirrors the real Shopify columns (single variant + legacy pair), no shopifyMemberDiscountId. See the doc's re-validation section. No FK repoint, read/security cutover, UI, or column drops — those are P2-P5, deferred per the expand-contract migration policy. Integration test covers the backfill (id-alias, column copy, event linkage, sequence bump, idempotency) against a populated schema. 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
Phase 1 of the Program → ProgramInstance → Event 3-tier restructure
(
docs/designs/PROGRAM_INSTANCE_RESTRUCTURE.md). TodayProgramconflates acatalog definition ("Woodworking 101") with a concrete offering ("Fall 2026
Woodworking"); the fix is a middle
ProgramInstancetier. This PR ships thedoc's P1 only — the additive schema + a 1:1 backfill — and nothing that
reads it yet. It is expand/contract-safe: purely additive, safe for old code
during the deploy drain window (
docs/DEPLOY_MIGRATION_ORDER_OF_OPERATIONS.md).See the new Re-validation section at the top of the design doc for the full
scope + cut line.
Decisions
ProgramInstancetable (alloffering columns mirrored from
Program+ nullable narrowing overrides),nullable
Event.instanceIdFK,Program.instances/Person.instancesLedback-relations. No FK repoint, no read/security-claim cutover, no
write/UI, no column drops — those are P2–P5 and are RENAME/DROP (contract)
steps deliberately deferred to later releases.
SHOPIFY_MEMBER_SEGMENT_PRICING.md(docs(design): propose Shopify member-segment pricing #929) landed first (one variant + a storedshopifyMemberDiscountId). What merged is Shopify single-pool capacity + scholarship lifecycle inventory (supersedes two-pool mirror) #930 (single-pool capacity +scholarship hold ledger,
checkin-app/docs/PROGRAM_CAPACITY_AND_SCHOLARSHIPS.md).So the instance mirrors the Shopify columns that actually exist —
shopifyProductId,shopifyVariantId, and the legacyshopifyOrgMemberVariantId/shopifyNonOrgMemberVariantIdpair — and carriesno
shopifyMemberDiscountId. Capacity (maxParticipants, Shopify-sourced)and the hold-ledger fields (
inventoryHeldAt/paymentPlanDeniedAtonProgramParticipant) are untouched in P1 and consistent with Shopify single-pool capacity + scholarship lifecycle inventory (supersedes two-pool mirror) #930; the doc'sre-validation section flags where P2/P3 must re-target
adjustProgramInventoryand
withdrawAndReleaseHoldto the instance.ProgramInstanceperProgramwith
id = Program.id, so the later FK repoint (P2) is a pure RENAME with novalue rewrite;
Event.instanceId = programId;setvalbumps the sequence pastthe aliased ids. Hand-written SQL (not a Prisma DROP+ADD), wrapped in one
BEGIN/COMMIT, idempotent (skip programs that already have an instance, onlyfill still-null
instanceId,setvalderived fromMAX(id)so it neverregresses).
ProgramInstanceis sensitive+scopable (viaprogramId) but hasno read consumers in P1, so it goes in
OPT_OUT_PENDING_ROUTE; its realtheir_program_participantsbinding lands with the atomic claim swap in P3.read consumers by design, and no live write path creates instances yet, so a
read-only page or a seed that mints an instance would misrepresent the running
app. The backfill is proven by the integration test.
Testing
src/app/__tests__/programInstanceBackfill.integration.test.ts— seeds realProgram/Event rows (legacy two-variant + single-pool shapes, a program-bound
and a program-less event), runs the migration's own backfill SQL (extracted
verbatim between markers), and asserts the id-alias, full column copy, event
linkage, sequence bump (a new insert doesn't collide), and idempotency (second
run is a no-op). Passing under
INTEGRATION_DB=1.binding-equivalence tests confirm the
OPT_OUT_PENDING_ROUTEaddition.npx tsc --noEmitclean;eslint --max-warnings 0clean on touched files.prisma migrate diff(matchesschema.prismaexactly → drift check passes);
migration-safety.ymlre-runs the backfill onthe seeded
Woodworking 101program on a populated DB.Deferred (later phases, per the design doc §7)
P2 FK repoint (RENAME), P3 read cutover + atomic
programsLed→instancesLedclaim swap, P4 write/UI flip + public
/programsschedule, P5Programcolumndrops. Each is called out in the doc's Phase-1 cut line.
🤖 Generated with Claude Code