feat(programs): archive/un-archive a program's Shopify listing#955
Open
jee7s wants to merge 1 commit into
Open
feat(programs): archive/un-archive a program's Shopify listing#955jee7s wants to merge 1 commit into
jee7s wants to merge 1 commit into
Conversation
Adds a board/sysadmin action (program-ops → Shopify section) to retire a program's Shopify listing and restore it, without touching sales history or existing participant records. On archive the Shopify product is set to `archived` status via the Admin API; on un-archive it is restored to `active`. New Program.shopifyArchivedAt (nullable, @sensitivity:internal). While set, every app-side surface that would touch a live listing treats it as absent: checkout-link building, member discount minting, capacity pushes (adjustProgramInventory — the single choke point, so one guard covers PATCH cap edits, scholarship holds/releases and the webhook's sibling mirror), and the sync-shopify repair path. The orders/paid webhook still processes late-arriving paid orders (money already moved). On a Shopify failure the checkin side is archived anyway, reported via reportShopifyFailure, with a warning for the operator to reconcile. See docs/designs/SHOPIFY_LISTING_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 board/sysadmin action to archive (retire) and un-archive a program's Shopify
listing, on the program-ops → Shopify section. Archiving takes a program's Shopify
product out of service without touching sales history or existing participant records;
un-archiving restores checkout. Design:
checkin-app/docs/designs/SHOPIFY_LISTING_ARCHIVE.md.Both sides of the linkage are acted on, in order (Shopify first, then stamp checkin):
statusis set toarchivedvia the Admin API on archive(hidden from the storefront, not purchasable, data preserved) and restored to
activeonun-archive. Product id comes from
Program.shopifyProductId, or is derived from a variant(
GET variants/{id}.json → product_id) for the manual-repair case, mirroringadjustProgramInventory.Program.shopifyArchivedAt(@sensitivity:internal).Decisions (from the interview)
drift.
archived(notdraft) is the semantic match for "retired" and round-trips toactive.archiving; the design names the integration point — a future program-archive hook should
chain into this route /
setProgramListingArchived.What "archived" gates (treat the program as having no live listing)
While
shopifyArchivedAtis set, cleanly (no stray 4xx / warnings / silent decrements):POST .../discount-code){ code: null }→ undiscounted fallback.adjustProgramInventory)sync-shopifyrepairNot gated: the
orders/paidwebhook still processes late-arriving paid orders — money hasalready moved, so the participant is still activated (only the inventory side-effects no-op).
On a Shopify failure the checkin side is archived anyway, reported via
reportShopifyFailure(IntegrationErrorLog + admin email), and the route returns a
warning(reconcile by retry orin the Shopify admin).
setProgramListingArchivednever throws — mock branch onconfig.shopifyMockActive(), hard fetch timeout, same idioms as the rest oflib/shopify.ts.Testing evidence
src/lib/__tests__/shopify.test.ts, +8):setProgramListingArchivedarchive→archived/ un-archive→active, product-id-from-variant derivation, no-listing no-op, mockbranch, missing-creds
false, failure path (returns false + emails admins); plusadjustProgramInventoryarchived no-op. 34 pass.src/app/__tests__/programArchiveShopifyAPI.integration.test.ts, new, 9pass): authz (401 anon / 403 plain user), archive stamps+audits, un-archive clears,
idempotent no-op, no-listing 400, bad-body 400 / missing 404, and cross-route gating —
sync-shopify 400 while archived then repairs once un-archived, discount-code
{ code: null }while archived, PATCH capacity push suppresses the inventory warning while archived (live
control still warns).
jest --ci --runInBand, ~74s) green;tsc --noEmitandeslint --max-warnings 0clean.Migration
20260708030000_shopify_listing_archive— additive, nullable, expand-only, no backfill(every existing row reads as "live"). Regenerated
classifications.tscommitted.Deferred
Program-archive chaining (integration point named), inventory reconcile on un-archive (manual
sync), bulk archive.
🤖 Generated with Claude Code