Repository navigation
PAY-005A1 — Prevent invalid partial refunds from becoming full refunds #200
Description
Officer impact: An invalid partial-refund amount must stop with a plain error. It must never silently become a full refund. This is a source safeguard only until the exact Functions revision is deployed and verified with Stripe test mode.
Officer documentation: Update only the refund safety/status section of docs/officers/EVENTS_SHOP_MEMBERS.md after #197 releases its separate My Account phone section. Officers must not test refunds with real orders, registrations, members, cards, or Stripe live mode.
Deployment evidence: None at issue creation. Closure must report source, tests, merge, Firebase deployment, Stripe configuration, and production behavior as separate states. No live refund or production-data test is allowed.
Problem
Both admin refund Functions convert amountCents with Number() and omit Stripe's amount unless the converted value is positive and strictly below the stored original total. Equal, over-limit, Infinity, and the string "Infinity" can therefore survive the first guard, reach Stripe without an amount, and be interpreted as a full refund.
This is reachable through current admin screens because they check only that a value is positive, and through direct callable requests because numeric coercion accepts malformed types. An accidental entry or compromised admin session can turn a requested partial refund into a full one.
Stripe documents that omitting amount refunds the full remaining amount, while a partial refund supplies a positive integer amount: Stripe refund creation.
Atomic outcome
Make partial-refund admission fail closed before any Stripe or Firestore side effect. Every admitted partial refund must carry the exact validated integer amount to Stripe. Only the explicit full-refund action may omit amount.
flowchart TD
A["Officer requests partial refund"] --> B{"Primitive safe integer cents?"}
B -- "No" --> Stop["Stop before Stripe or data write"]
B -- "Yes" --> C{"Greater than zero and below stored original?"}
C -- "No" --> Stop
C -- "Yes" --> D["Send exact amount to Stripe"]
D --> E["Record partial result only after provider success"]
Full["Explicit full-refund action"] --> F["Only path allowed to omit amount"]
Text alternative: A partial amount must be a positive whole number of cents smaller than the stored original total. Invalid values stop before Stripe or Firestore. Only an explicit full-refund request can omit the amount.
Invariants
refund_partialaccepts only a primitive JavaScript number that is a positive safe integer in cents and strictly less than the authoritative stored original cents.- No numeric strings, booleans, arrays, objects, fractions,
NaN, infinities, zero, negative, equal, or over-limit values are accepted. - The authoritative stored original cents must itself be a positive safe integer. Missing or malformed stored state fails closed.
- Every admitted partial call includes the exact Stripe
amount; onlyrefund_fullmay omit it. - Invalid caller input fails before Stripe client construction or refund creation and before any Firestore write, audit, status, or refund-ID mutation.
- Invalid stored state fails before provider/write side effects.
- Status and audit classification derive from the requested action, not from truthiness or presence of
refundPayload.amount. - Errors are fixed and reveal no amount, payment identifier, request payload, member/customer data, or provider detail.
- A Stripe failure leaves business state unchanged.
Scope
- New shared pure refund amount validator and focused unit tests in
functions/ functions/adminOrderAction.jsfunctions/adminRegistrationAction.jsfunctions/commerceControlEntryPoints.test.jsfor both endpoint side-effect boundaries- Minimum authoritative security/issue/runbook status documentation
- Only the separate refund section of
docs/officers/EVENTS_SHOP_MEMBERS.md, after DATA-001C2 — Stop profile setup from copying Firebase Auth phone numbers #197 releases that file
Acceptance criteria
- Both merchandise and race partial-refund paths use one fail-closed validator.
- Missing, null, zero, negative, equal, over, fraction,
NaN, positive/negative infinity, numeric string, boolean, object/array, and unsafe-integer inputs fail before Stripe or Firestore mutation. - Missing, string, non-positive, fraction, infinite, or unsafe stored totals fail closed before Stripe or Firestore mutation.
- Valid boundary values
1andoriginal - 1include the exact Stripeamountand remain classified as partial. - Explicit full-refund actions omit
amountand remain classified as full. - Stripe rejection leaves Firestore state unchanged.
- Focused and full Functions tests, lint, frontend tests/build, Rules tests, and safety checks pass.
- Plain-language officer documentation says invalid partial amounts stop and cannot become full refunds, while clearly marking the safeguard source-only until deployment.
- Source, tests, merge, Firebase deployment, Stripe provider state, and live behavior are reported separately.
Dependencies and coordination
- This is the narrow containment child PAY-005A1 under catalogued PAY-005/PAY-005A; it does not complete the refund architecture.
- DATA-001C2 — Stop profile setup from copying Firebase Auth phone numbers #197 owns
docs/officers/EVENTS_SHOP_MEMBERS.mdonly for the My Account phone-pause section. Do not edit that file until DATA-001C2 — Stop profile setup from copying Firebase Auth phone numbers #197 merges/releases it, then refresh from the exact main commit and edit only the refund section. - CI-001 — Establish trustworthy quality gates and protected deployment #105/CI-001C — Configure protected short-lived Firebase deployment identity #133/CI-001D — Prove backend-first staging and deploy the profile repair #136 own protected deployment and live evidence.
- No open live refund-specific issue was found at publication.
Explicitly out of scope
- Refund idempotency keys/command journal, remaining-refundable provider readback, concurrency control, webhook reconciliation, capability design, or broader PAY-005 state-machine work.
- Browser-only validation as a security boundary. A later UX child may add client bounds after this server contract merges.
- Stripe live mode, production Firebase deployment, production refunds, real orders/registrations, provider configuration, legal/refund-policy decisions, or production-data inspection.
Claim protocol
Creation is not a claim. Before editing, assign the issue, post CLAIMED by <canonical agent> at <UTC>; branch <branch>, and re-read live comments. Keep the change to this one containment outcome.
Activity
daliu commentedon Jul 14, 2026
CLAIMED by /root at 2026-07-14T00:56:51Z; branch
codex/issue-200-partial-refund-containment; exact basea8801770e97cf21d81f3307f5893734115140d8f.Owned runtime/test paths:
functions/refundValidation.jsfunctions/refundValidation.test.jsfunctions/adminOrderAction.jsfunctions/adminRegistrationAction.jsfunctions/commerceControlEntryPoints.test.jsOwned documentation after current owners release the affected hunks: minimum PAY-005A1 status sections in
SECURITY.md,GITHUB_ISSUES.md,GITHUB_ISSUE_SLICES.md, andOPERATIONS_RUNBOOK.md; only the refund section ofdocs/officers/EVENTS_SHOP_MEMBERS.mdafter #197 merges/releases that file.#197 runtime/System Design/My Account phone hunks and all #196 Rules/test/officer-system-map paths are excluded. No frontend refund input, package, workflow, provider, deploy, webhook, journal, command-lease, production-data, or Stripe-live change is in scope.
daliu commentedon Jul 14, 2026
EXTRACTION FINISHED — 2026-07-14T01:14:10Z
PR #201 is ready for review at exact head
346df6c6726ca46ac1f46e5b36af5364865e8ff1.Evidence:
091ad489adec4608f451569a7af3eda24de599e2c38018432d920616ad77ca1bwith no findings.Hosted PR checks are running. No Firebase deployment or Stripe provider call/configuration occurred; the safeguard is source/test-only and not live. The claim stays active until controlled merge, post-merge evidence, and explicit release.
daliu commentedon Jul 14, 2026
COMPLETE / CLAIM RELEASED — 2026-07-14T01:18:34Z
PR #201 merged to
mainas squash commitcaae31ff427e91325d34a6c901faf81ad1dc49dffrom exact reviewed source head346df6c6726ca46ac1f46e5b36af5364865e8ff1.Outcome:
amount;amount;Verification:
29298036773passed all required jobs;29298127585passed frontend, Functions, Rules, artifact scrubber, and commerce journal jobs.Deployment truth:
runmprc.comverification, real refund, production record, member/customer data, or live card was used;Residual PAY-005 work remains open: stable command identity/idempotency, provider remaining-balance truth, concurrency, event-driven reconciliation, finance capabilities/MFA/recent-auth, approved refund policy, protected deployment, provider readback, and live verification. An unknown provider result must be reconciled and never blindly retried.
Released paths:
functions/adminOrderAction.jsfunctions/adminRegistrationAction.jsfunctions/commerceControlEntryPoints.test.jsfunctions/refundValidation.jsfunctions/refundValidation.test.jsGITHUB_ISSUES.mdGITHUB_ISSUE_SLICES.mdSECURITY.mdOPERATIONS_RUNBOOK.mddocs/officers/EVENTS_SHOP_MEMBERS.md