Repository navigation
PAY-005A2 — Validate Stripe refund results before recording success #204
Description
Officer impact: A refund is recorded as complete only after Stripe returns a succeeded refund for the expected payment and currency. A pending, failed, canceled, action-required, malformed, or mismatched result stops with one plain do-not-retry message and must be escalated.
Officer documentation: Update only the refund safety/status section of docs/officers/EVENTS_SHOP_MEMBERS.md. Officers must not test this with a real order, registration, member, card, Stripe live mode, or production Firebase record.
Deployment evidence: None at issue creation. Closure must report source, tests, merge, Firebase deployment, Stripe configuration, and production behavior as separate states. A mock or green workflow does not prove a live refund.
Problem
PAY-005A1 #200 prevents malformed partial amounts from becoming full refunds. Both current admin Functions still treat every resolved stripe.refunds.create() value as success. They immediately write refunded or partially_refunded and use stripeRefund.id without checking the returned object.
Stripe documents that a Refund contains its identifier, object type, amount, currency, PaymentIntent, and status. Status may be pending, requires_action, succeeded, failed, or canceled; only succeeded is a final success: https://docs.stripe.com/api/refunds/object . Stripe also documents that partial refunds supply a positive integer amount and can be repeated only up to the remaining unrefunded balance: https://docs.stripe.com/api/refunds/create .
Atomic outcome
Validate one immutable expected-refund snapshot before the provider call and validate the resolved Refund before any business-record success write. Only a succeeded, correctly bound result may update the order or registration.
flowchart TD
A["Officer requests refund"] --> B{"Stored payment, currency, and amount are valid?"}
B -- "No" --> C["Stop before Stripe or data write"]
B -- "Yes" --> D["Create one Stripe refund"]
D --> E{"Succeeded result matches payment, currency, and amount rule?"}
E -- "No or unclear" --> F["Do not record success or retry; escalate"]
E -- "Yes" --> G["Write validated refund ID, actual cents, and final status"]
Text alternative: Validate the stored refund target before contacting Stripe. After one Stripe call, write local success only when the returned refund is final and matches the expected payment, currency, and amount rule. Otherwise do not retry and escalate.
Invariants
- Before Stripe client construction, the stored PaymentIntent ID must be a bounded primitive
pi_identifier, the original amount must be a positive safe integer, and currency must be exact lowercaseusdfor the current platform contract. - PAY-005A1 continues to validate partial input. This issue must not weaken or duplicate that boundary.
- A resolved provider value is accepted only when it is a plain Refund object with exact
object: "refund", a bounded primitivere_ID, exact primitive PaymentIntent binding, exact expected currency, positive safe-integer amount, and exactstatus: "succeeded". - A partial response amount must equal the admitted requested cents.
- An explicit full-refund response may be any positive safe-integer amount no greater than the stored original cents. Stripe receives no
amountfor this action, so the response is the full remaining provider balance and may be lower after an earlier or outside partial refund. - Missing/null/string/array/accessor/prototype-surprising results; malformed IDs; expanded PaymentIntent objects; wrong payment/currency/object/amount; and
pending,requires_action,failed,canceled, unknown, or missing status never write local success. - A provider throw and a resolved-but-unconfirmed result expose the same fixed message: result not confirmed, do not retry, escalate to treasurer and platform owner. Do not include provider fields or failure details.
- Stripe is called at most once. A resolved-but-unconfirmed result may already represent a provider-side mutation; it is ambiguous and must not be retried blindly.
- Only the validated refund ID and validated actual cents may enter the returned result, audit entry, refund ID array, or local status update.
- No request body, amount supplied by a caller, payment/refund identifier, provider response, member/customer data, or Stripe detail is logged.
Scope
- New pure
functions/refundResponseValidation.js - New focused
functions/refundResponseValidation.test.js functions/adminOrderAction.jsfunctions/adminRegistrationAction.jsfunctions/commerceControlEntryPoints.test.jsfor both endpoint no-write boundaries- Minimum exact PAY-005 status/evidence hunks in
SECURITY.md,OPERATIONS_RUNBOOK.md,GITHUB_ISSUES.md, andGITHUB_ISSUE_SLICES.md - Only the existing refund section of
docs/officers/EVENTS_SHOP_MEMBERS.md
Acceptance criteria
- One shared pure validator handles both race and merchandise Refund responses.
- Malformed stored PaymentIntent, amount, or currency stops before Stripe construction and every write.
- Valid partial and full calls preserve PAY-005A1 payload behavior and make exactly one provider call.
- Valid succeeded partial responses require exact requested cents; valid succeeded full responses accept the positive full-remaining amount up to the original total.
- Valid results write only the validated ID/actual cents and the correct partial/full status.
- Undefined, null, primitive, array, accessor/prototype-surprising, missing-field, malformed-ID, wrong-object, wrong-payment, expanded-payment, wrong-currency, invalid/over/mismatched-amount, non-succeeded, and unknown-status values return the fixed ambiguous result with no local success write.
- Provider rejection and resolved invalid response are byte-equivalent at the callable error boundary and do not expose provider detail.
- Focused and full Functions tests/lint, frontend tests/build, Rules tests, SPA callback, and workflow/artifact safety checks pass.
- Plain-language officer documentation says what stops, why not to retry, how to prove success safely, and that the safeguard is not live until deployed.
- Source, tests, merge, Firebase deployment, Stripe/provider state, and live behavior are reported separately.
Dependencies and coordination
- PAY-005A1/PAY-005A1 — Prevent invalid partial refunds from becoming full refunds #200 is complete and released at
caae31ff427e91325d34a6c901faf81ad1dc49df. - Exact starting
mainis2c7332ca547ba98a57863a6f2f41849ffb386e9fafter SUPPLY-001C/SUPPLY-001C — Upgrade Firebase web SDK to the final 11.x compatibility set #202. - No open refund issue, pull request, ready claim, or conflicting file claim existed at publication.
- 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.
Explicitly out of scope
- Refund command identity/idempotency, durable pre-send journal, provider retrieval/reconciliation, remaining-balance readback, concurrency/fencing, webhook finalization, capabilities/MFA/recent-auth, approved refund policy, browser UX, or the complete PAY-005 state machine.
- Stripe live mode, production Firebase deployment, real refunds or records, provider configuration, legal decisions, or production-data inspection.
- Treating the absence of a local write as proof that Stripe did nothing. Ambiguous outcomes still require later reconciliation.
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 resolved-response containment outcome.
Activity
daliu commentedon Jul 14, 2026
CLAIMED by /root at 2026-07-14T01:54:06Z; branch
codex/issue-204-refund-response-validation.Exact base:
2c7332ca547ba98a57863a6f2f41849ffb386e9f.Exact owned code boundary:
functions/refundResponseValidation.js,functions/refundResponseValidation.test.js,functions/adminOrderAction.js,functions/adminRegistrationAction.js, and refund-response cases infunctions/commerceControlEntryPoints.test.js. Exact documentation boundary is the listed PAY-005 status/evidence and officer refund sections only. No package, lockfile, Rules, workflow, index, provider, deployment, production-data, or other commerce-runtime path is claimed.daliu commentedon Jul 14, 2026
IN-SCOPE FAILURE-BOUNDARY ADDENDUM — 2026-07-14T02:02Z
The response tests exposed the adjacent post-provider boundary: Stripe can return a valid succeeded Refund and the single Firestore update can then throw or lose its response. A raw storage error would not tell the officer to avoid a second refund. The same two owned endpoint files now map that post-provider write failure to the exact fixed result-not-confirmed / do-not-retry / escalate result, without provider or storage detail. Two red endpoint tests prove the old leak; no new path, schema, provider call, or scope is added. This still does not solve the missing durable pre-send operation/idempotency/reconciliation work.
daliu commentedon Jul 14, 2026
EXTRACTION FINISHED by /root at 2026-07-14T02:10:55Z.
Exact source commit:
fbc9494c60b22f25c3c30134580f4bd93a02a715Exact base:
2c7332ca547ba98a57863a6f2f41849ffb386e9fExact diff SHA-256:
f193a0b8b9e4731ff92d20f141e889e2abe345fbf3cea46107cd87397f46ca42Exact branch:
codex/issue-204-refund-response-validationOwned paths: 10; worktree clean after commit.
Red evidence:
Green local evidence on Node 20:
git diff --checkpassed; 15 changed-document relative links resolve.Deployment truth: source/tests/commit only. No website publication, runmprc.com verification, Firebase Functions deployment, Stripe provider configuration or request, production data action, or live behavior verification occurred.
PAY-005A2: validate Stripe refund results (#205)
daliu commentedon Jul 14, 2026
COMPLETE / CLAIM RELEASED by /root at 2026-07-14T02:15:51Z.
Source and merge:
fbc9494c60b22f25c3c30134580f4bd93a02a715main:0dbac066a49837e31af72c9821c8044d287035fcf193a0b8b9e4731ff92d20f141e889e2abe345fbf3cea46107cd87397f46ca42Hosted proof:
6a559ad0a9ece400087af1cfsucceeded for PR review only.Deployment/provider/live truth:
profile-recoveryand does not include these refund Functions.All acceptance boxes are checked, the status claim is removed, and the ten-path #204 claim is released.