Skip to content

PAY-002A1 — Add pure canonical commerce-state reducers and a safe legacy classifier #161

Closed

Description

@daliu

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.

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_failed
    • checkout_open -> processing | paid | failed | expired | cancelled
    • processing -> paid | failed
    • not_required, checkout_failed, paid, failed, expired, and cancelled are terminal in this generic reducer.
  • Registration:
    • reserved -> confirmed | cancelled
    • confirmed -> attended | no_show | transferred | cancelled
    • the remaining states are terminal.
  • Fulfillment:
    • unfulfilled -> picking | cancelled
    • picking -> packed | cancelled
    • packed -> shipped | ready_for_pickup | cancelled
    • shipped -> delivered
    • ready_for_pickup -> picked_up
    • delivered | picked_up | fulfilled_legacy -> return_requested
    • return_requested -> returned
    • returned -> written_off
    • no normal transition enters fulfilled_legacy.
  • Refund:
    • none -> partially_refunded | refunded
    • partially_refunded -> refunded
  • Dispute:
    • none may 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 -> won is structurally allowed;
    • won, warning_closed, and prevented are 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 remains partially_refunded.
  • Confirmed/attended/no-show/transferred registration requires payment paid or not_required.
  • Active/completed fulfillment beyond unfulfilled requires payment paid.
  • Payment paid plus domain cancelled is preserved but marked review_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_cancellation maps to payment paid plus domain cancelled and 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/refunded split into payment paid plus the corresponding confirmed refund state.
  • Registration refund does not imply registration cancellation without separate cancellation evidence.
  • Order fulfilled can become fulfilled_legacy only with separate compatible payment evidence; it must not pretend payment, delivery, or pickup is proven.
  • Operational transferred and cancelled likewise require separate compatible payment evidence; otherwise classification is review-required with no projection.
  • Legacy timestamps are supporting evidence, never authority. fulfilledAt plus cancelledAt, 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: 1 canonical input is a deterministic no-op; unknown/future versions fail closed. Stripe metadata schemaVersion: "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.js
  • functions/commerceState.test.js

Minimum exact design/status hunks only in:

  • STRIPE_COMMERCE_DESIGN.md
  • SYSTEM_DESIGN.md
  • SECURITY.md
  • IMPLEMENTATION_PLAN.md
  • GITHUB_ISSUES.md
  • GITHUB_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.js behavior 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: 1 are exported as immutable values; metadata schemaVersion: "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, and paid_after_cancellation have 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 -> won late win, and version-gated prevented.
  • 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

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 rare lost -> won are 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, and cancelled operational status never manufactures payment state;
  • no order/registration projection contains a singular canonical dispute status; each dispute is validated separately;
  • numeric local stateSchemaVersion: 1 is distinct from Stripe metadata schemaVersion: "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

  1. added
    priority:P0Launch blocker or urgent security risk
    type:securitySecurity or privacy boundary
    size:MMedium multi-file issue
    status:readyDependencies complete and ready to claim
    area:stripeStripe payments and financial lifecycle
    status:in-progressClaimed and actively being implemented
    and removed
    status:readyDependencies complete and ready to claim
    on Jul 13, 2026
  2. self-assigned this
    on Jul 13, 2026
  3. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    CLAIMED by /root at 2026-07-13T14:54:07Z; branch codex/issue-161-commerce-state-reducers; exact base 317b800cd39af136828de0d4eb23fbef66c2b390. 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.

  4. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    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.

  5. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    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:

    • focused commerce state: 110/110 passed
    • full Functions: 12 suites, 567 passed, 1 intentional skip
    • Functions lint: passed
    • frontend Jest: 132/132 passed
    • SPA navigation: 10/10 passed
    • diagnostic production build: passed
    • Firestore Rules on demo-rules-test with Java 17: 305/305 passed
    • staged diff/link/sensitive-pattern checks: passed

    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.

  6. daliu commented on Jul 13, 2026

    @daliu
    ContributorAuthor

    Completion and release evidence — 2026-07-13T15:43:50Z

    PAY-002A1 is complete as a source, test, review, and merge outcome.

    • Pull request: PAY-002A1: add canonical commerce state reducers #162
    • Reviewed source commit: 1db6fe3ffc5bc6f9032a57990dc76469e19b4712
    • Squash merge on main: 68dbcae6b3741cf5fae731a9d970e25a39b94852
    • Post-merge CI: run 29263093934, all Frontend, Functions, and Rules jobs passed.
    • Focused reducer/classifier tests: 110/110.
    • Full local proof: Functions 567 passed with one intentional skip; frontend 132/132; SPA 10/10; Rules 305/305; lint/build passed.
    • Two independent exact-snapshot reviews approved the code/security and documentation/scope boundaries.

    Deployment 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 error with no publication timestamp.

    Therefore:

    • Website source changed: yes
    • Tests passed: yes
    • Code merged: yes
    • Website published at this commit: no
    • runmprc.com verified at this commit: no
    • Firebase deployed: no
    • Stripe or other provider configured: no
    • Production commerce behavior verified: no

    The 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:stripeStripe payments and financial lifecyclepriority:P0Launch blocker or urgent security risksize:MMedium multi-file issuetype:architectureArchitecture and contract designtype:reliabilityReliability and recoverytype:securitySecurity or privacy boundary

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions