Repository navigation
PAY-002A1 — Add pure canonical commerce-state reducers and a safe legacy classifier #161
Description
Officer impact: No current officer screen, payment flow, or procedure changes. This source-only child defines the state rules that later race, merchandise, refund, and webhook work must share.
Officer documentation: None — the library is not imported by a runtime endpoint and gives officers no usable action. Current commerce remains NOT AVAILABLE YET.
Deployment evidence: None at issue creation. Closure proves source, synthetic tests, review, merge, and exact-commit CI only. It must not claim a Firestore migration, Stripe/Firebase deployment, provider configuration, website publication, or live payment behavior.
Parent and dependencies
Atomic source child of catalogued PAY-002/PAY-002A.
- PAY-001A/PAY-001A — Add bounded request validation and safe logging primitives #157 is merged and supplies dependency-free request-safety primitives.
- PAY-003A/PAY-003A — Harden Stripe webhook ingress and monotonic event reduction #101 is merged but its current webhook transitions remain immutable in this child.
- CONFIG-001 admission source exists, but this pure library reads no config and performs no command.
- Real legacy inventory/disposition remains blocked by BACKEND-001 — Audit the Firebase cutover and approve legacy data disposition #113.
- PAY-002B will own command/attempt identity and idempotency journals.
- PAY-003B/PAY-003 — Build idempotent asynchronous Stripe event processing #106 will own webhook adoption and Stripe API-version adaptation.
- PAY-005 will own refund-command pending/retry state.
No live PAY-002A issue, claim, or duplicate existed at creation.
Problem
The repository still overloads one legacy status field with payment, registration, fulfillment, cancellation, refund, transfer, and dispute meaning. Independent race, shop, webhook, and refund code can therefore allow impossible regressions or interpret the same record differently.
A shared reducer must exist before those runtimes are integrated. It must be pure, exhaustive, retry-safe, and able to classify legacy shapes without guessing or exposing record identifiers.
Atomic outcome
Create a CommonJS source/test library with:
- numeric
stateSchemaVersion: 1; - separate payment, registration, fulfillment, confirmed-refund aggregate, and per-dispute state enums;
- pure transition reducers that return frozen fixed-shape results;
- a pure cross-dimension validator;
- a deterministic legacy classifier that returns a canonical projection plus fixed review reasons;
- a synthetic dry-run summary containing counts and fixed reason codes only.
The library is not exported from functions/index.js and no runtime imports it in this child.
Canonical states
| Dimension | States |
|---|---|
| Payment | not_required, checkout_creating, checkout_open, checkout_failed, processing, paid, failed, expired, cancelled |
| Registration | reserved, confirmed, attended, no_show, transferred, cancelled |
| Fulfillment | unfulfilled, picking, packed, shipped, ready_for_pickup, delivered, picked_up, return_requested, returned, written_off, cancelled, migration-only fulfilled_legacy |
| Confirmed refund status | none, partially_refunded, refunded |
| Per-dispute | none, warning_needs_response, warning_under_review, warning_closed, needs_response, under_review, won, lost, prevented |
Orders and registrations have no canonical singular disputeStatus. Each dispute is validated separately against paid payment context and later persists as its own server-only record; a singular summary may exist only as derived compatibility.
prevented was added in Stripe API version 2025-08-27.basil. The server client request version and webhook endpoint Event version are independent; the provider setting is unverified, and current webhook source neither accepts prevented nor validates event.api_version. PAY-003B must inventory and validate/pin the endpoint Event contract before adopting a newer value.
Refund-command pending is not an aggregate refund state. It belongs to future PAY-005 operation records so a later partial-refund request cannot erase the amount already confirmed refunded.
Allowed transitions
- Payment:
checkout_creating -> checkout_open | checkout_failedcheckout_open -> processing | paid | failed | expired | cancelledprocessing -> paid | failednot_required,checkout_failed,paid,failed,expired, andcancelledare terminal in this generic reducer.
- Registration:
reserved -> confirmed | cancelledconfirmed -> attended | no_show | transferred | cancelled- the remaining states are terminal.
- Fulfillment:
unfulfilled -> picking | cancelledpicking -> packed | cancelledpacked -> shipped | ready_for_pickup | cancelledshipped -> deliveredready_for_pickup -> picked_updelivered | picked_up | fulfilled_legacy -> return_requestedreturn_requested -> returnedreturned -> written_off- no normal transition enters
fulfilled_legacy.
- Refund:
none -> partially_refunded | refundedpartially_refunded -> refunded
- Dispute:
nonemay accept any supported observation because an event can arrive first/out of order;needs_response -> under_review | won | lost;under_review -> won | lost;warning_needs_response -> warning_under_review | warning_closed | needs_response;warning_under_review -> warning_closed | needs_response;- rare provider correction
lost -> wonis structurally allowed; won,warning_closed, andpreventedare terminal.
Every same-state input returns unchanged; it does not prove an Event, command, amount, or evidence duplicate. PAY-003 Event IDs and PAY-002B/PAY-005 command identity prove replay while still allowing a second confirmed partial refund or new dispute evidence at the same enum value. Every forbidden or unknown transition returns a fixed rejection. Starting a later checkout attempt after failure/expiry requires PAY-002B attempt identity; a raw regression to checkout_creating is forbidden.
Cross-dimension invariants
- Confirmed refund/dispute evidence requires payment
paid. The status does not contain cumulative refunded cents; PAY-003B/PAY-005 must preserve verified integer totals separately, including a higher total while status remainspartially_refunded. - Confirmed/attended/no-show/transferred registration requires payment
paidornot_required. - Active/completed fulfillment beyond
unfulfilledrequires paymentpaid. - Payment
paidplus domaincancelledis preserved but markedreview_required; it is never erased or silently declared valid. - Contradictory/unknown evidence returns fixed review/rejection reasons and no guessed mutation.
- An allowed edge is structural only. It never authorizes a caller or proves waiver, refund/cancellation/return/write-off policy, Session/provider state, amount, or evidence; downstream AUTH/PAY/MERCH/LEGAL gates remain mandatory.
Legacy classification rules
paid_after_cancellationmaps to paymentpaidplus domaincancelledand a review reason. It is never a canonical payment enum.- Stored Session, Payment Link, PaymentIntent, Charge, refund, and dispute IDs prove only references, never provider state. Stripe IDs are opaque bounded strings; A1 never assumes a fixed prefix or format. A reference that could contradict legacy status returns review-required with no guessed projection; dispute references require separate records.
partially_refunded/refundedsplit into paymentpaidplus the corresponding confirmed refund state.- Registration refund does not imply registration cancellation without separate cancellation evidence.
- Order
fulfilledcan becomefulfilled_legacyonly with separate compatible payment evidence; it must not pretend payment, delivery, or pickup is proven. - Operational
transferredandcancelledlikewise require separate compatible payment evidence; otherwise classification is review-required with no projection. - Legacy timestamps are supporting evidence, never authority.
fulfilledAtpluscancelledAt, paid evidence beside an unpaid/failed state, refund time without refund status, or cancellation time beside fulfilled/confirmed status requires fixed review/rejection; no timestamp silently overrides explicit state. - Unknown, accessor-backed, proxy-throwing, or contradictory input must fail safely with fixed reasons and no logging.
- Existing valid numeric
stateSchemaVersion: 1canonical input is a deterministic no-op; unknown/future versions fail closed. Stripe metadataschemaVersion: "1"is a separate string reference-binding field and never makes a legacy record canonical. - This classifier emits no Firestore write patch and performs no real inventory/backfill. Write-time legacy derivation and runtime adoption require later reviewed children.
Exact source boundary
Create only:
functions/commerceState.jsfunctions/commerceState.test.js
Minimum exact design/status hunks only in:
STRIPE_COMMERCE_DESIGN.mdSYSTEM_DESIGN.mdSECURITY.mdIMPLEMENTATION_PLAN.mdGITHUB_ISSUES.mdGITHUB_ISSUE_SLICES.md
Post a scope amendment before editing any existing runtime, webhook, creator/admin endpoint, Rule, workflow, package/lockfile, index/export, UI, officer guide, environment file, provider setting, or production data.
Security and correctness invariants
- No Firebase, Stripe, network, filesystem, logger, clock, random, process-environment, or provider import/call.
- No real member/customer/runner data, provider object, ID, URL, key, token, or secret in fixtures, output, logs, issue, or PR.
- Reducers never mutate inputs and return frozen fixed-shape results with fixed reason enums. Same-state means unchanged, not deduplicated.
- Unknown values are never echoed into result reasons.
- Dry-run summaries contain aggregate counts and fixed reason codes only—never IDs or source records.
- Current
stripeWebhook.jsbehavior remains unchanged until PAY-003B adopts the canonical reducer with separate integration tests. - A source library does not authorize live checkout, refunds, migration, or deployment.
Acceptance criteria
- All five state enums and numeric
stateSchemaVersion: 1are exported as immutable values; metadataschemaVersion: "1"remains distinct. - Pure reducers enforce the exact matrices, classify same-state input as unchanged (not deduplicated), and reject every other state pair with fixed results.
- Results and canonical projections are frozen; inputs are not mutated.
- Cross-dimension validation returns valid, review-required, or rejected without erasing evidence.
- All seven registration and six order legacy statuses plus current webhook-only
processing,failed,expired, andpaid_after_cancellationhave deterministic synthetic classifications. - Mapping the same fixture twice is byte-equivalent and makes no call.
- Synthetic dry-run summary contains only totals/counts/fixed reasons and no IDs/PII.
- Hostile proxy/accessor/canary inputs fail closed with fixed output and zero logs.
- Node 20 focused/full Functions tests, Functions lint, frontend/Rules regression CI, diff/sensitive-pattern review, and independent reviews pass.
- Root docs distinguish source/tests, merge, runtime adoption, migration, provider deployment, and live behavior.
- No existing runtime, package, lockfile, workflow, Rule, environment, officer, provider, or production setting changes.
Required tests
- Exhaust every known state x state pair for every reducer.
- Same-state unchanged, frozen/non-mutating output, and fixed unknown-state rejection; explicitly do not infer Event/command duplication.
- Direct async failure from
checkout_open, plus late processing/failure/expiry/open after paid. - Partial refund after full refund; record downstream requirement for increasing confirmed cents while status stays partial.
- Inquiry warning lifecycle/escalation, rare
lost -> wonlate win, and version-gatedprevented. - Composite valid/review/impossible-state matrix.
- Every legacy status for registration and order, timestamp conflict handling, unverified provider-reference review without guessed projection, ambiguous evidence, unknown evidence, metadata-schema/version separation, valid/invalid v1, repeated mapping, and redacted aggregate summary.
- Hostile getter, proxy, prototype, nested object, and canary cases; assert no console/logger output.
Excluded
- Command IDs, payload fingerprints, leases, attempts, Stripe idempotency keys, or journals (PAY-002B).
- Checkout persistence-first sagas (PAY-002C/D).
- Webhook/runtime adoption or Stripe API-version changes (PAY-003B/PAY-003 — Build idempotent asynchronous Stripe event processing #106).
- Refund command or refund-operation state (PAY-005).
- Capacity/inventory counters, releases, or returns (RACE/MERCH).
- Real record inventory, backfill, migration, deletion, or writes (BACKEND-001 — Audit the Firebase cutover and approve legacy data disposition #113 and a later migration child).
- Provider/Firebase/website deployment, live payments, or production testing.
- Legal, refund, cancellation, transfer, fulfillment, or return policy decisions.
Scope amendment — 2026-07-13
Independent pre-implementation review corrected the contract before staging or review:
- same-state is
unchanged, never proof of duplicate Event/command/evidence/amount; checkout_open -> failed, inquiry escalation, and rarelost -> wonare structurally allowed;- provider IDs are opaque references only; A1 never assumes their prefix/format or infers Session, Payment Link, PaymentIntent, Charge, refund, or dispute state;
fulfilled,transferred, andcancelledoperational status never manufactures payment state;- no order/registration projection contains a singular canonical dispute status; each dispute is validated separately;
- numeric local
stateSchemaVersion: 1is distinct from Stripe metadataschemaVersion: "1"; - legacy timestamps support review but never override conflicting explicit state;
- allowed transitions are structural possibilities, not authorization, provider proof, or club policy;
- webhook Event API version is independent of client requests and remains PAY-003B/provider work.
No file boundary widened.
Claim protocol
Creation is not a claim. Before editing: confirm exact current main, assign the issue, post CLAIMED by /root at <UTC>; branch codex/issue-<number>-commerce-state-reducers, apply status:in-progress, and use an isolated worktree.
Activity
daliu commentedon Jul 13, 2026
CLAIMED by /root at 2026-07-13T14:54:07Z; branch
codex/issue-161-commerce-state-reducers; exact base317b800cd39af136828de0d4eb23fbef66c2b390. Exact ownership is only the two new pure Functions files and the six minimum root design/status documents listed in the issue. Existing runtimes, webhook, endpoints, Rules, index, packages, workflows, provider settings, and production data remain immutable.daliu commentedon Jul 13, 2026
SCOPE AMENDED by /root at 2026-07-13T15:08:48Z after independent pre-implementation review.
No file boundary widened. The issue body now records the corrected structural contract: same-state is unchanged rather than deduplicated; direct async failure, inquiry escalation, and rare late win are allowed; stored provider IDs never prove provider state; timestamps never override conflicts; numeric local state version is separate from Stripe metadata version; allowed edges do not authorize policy or provider facts; webhook Event API version remains PAY-003B/provider work.
daliu commentedon Jul 13, 2026
EXTRACTION FINISHED by /root at 2026-07-13T15:35:12Z; branch codex/issue-161-commerce-state-reducers.
Exact staged scope: 8 claimed paths only. No runtime, webhook, endpoint, index, Rule, package, workflow, UI, officer guide, environment, provider, or production file changed.
Outcome: pure numeric-v1 payment/registration/fulfillment/confirmed-refund/per-dispute reducers, separate per-dispute validation, cross-dimension validation, and a deterministic redacted synthetic legacy classifier/report. Same-state is unchanged—not replay proof. Stored provider references never prove state. Ambiguous payment/refund/dispute/timestamp/operational evidence returns fixed review/rejection with no guessed migration patch. No runtime imports this library.
Node 20 verification on the exact staged tree:
Two independent exact-file reviews approved with no remaining code, security, documentation, scope, or ownership findings.
Officer impact: None. Current officer screens, payment flow, and procedures remain unchanged; commerce remains NOT AVAILABLE YET.
Officer documentation: None — this library is not imported by any runtime and creates no officer action.
Deployment evidence: source/test extraction only. Nothing has been merged, published, migrated, deployed to Firebase, configured at Stripe, or verified live by this marker.
feat(commerce): add canonical state reducers (#161) (#162)
daliu commentedon Jul 13, 2026
Completion and release evidence — 2026-07-13T15:43:50Z
PAY-002A1 is complete as a source, test, review, and merge outcome.
1db6fe3ffc5bc6f9032a57990dc76469e19b4712main:68dbcae6b3741cf5fae731a9d970e25a39b94852Deployment truth
A protected production release was explicitly started for the exact merged commit: run 29263341385.
It failed closed before building or publishing because the required public App Check site key was absent from the GitHub production configuration. The backend-deploy and Pages-publish jobs were skipped. Netlify's automatic production build for the same commit also ended in
errorwith no publication timestamp.Therefore:
runmprc.comverified at this commit: noThe last independently verified Netlify production publication remains the older commit
e86a0f702cff6495f50630c5de3337290db8b8cb. PAY-002A1 adds pure, unused foundations only; no endpoint imports the module, so this failed release does not leave a partially enabled commerce flow.