Skip to content

PAY-005A2 — Validate Stripe refund results before recording success #204

Closed

Description

@daliu

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"]
Loading

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 lowercase usd for 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 primitive re_ ID, exact primitive PaymentIntent binding, exact expected currency, positive safe-integer amount, and exact status: "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 amount for 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.js
  • functions/adminRegistrationAction.js
  • functions/commerceControlEntryPoints.test.js for both endpoint no-write boundaries
  • Minimum exact PAY-005 status/evidence hunks in SECURITY.md, OPERATIONS_RUNBOOK.md, GITHUB_ISSUES.md, and GITHUB_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

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

  1. added
    priority:P0Launch blocker or urgent security risk
    type:securitySecurity or privacy boundary
    area:firebaseFirebase services and data
    size:SSmall focused 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 14, 2026
  2. self-assigned this
    on Jul 14, 2026
  3. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    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 in functions/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.

  4. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    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.

  5. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    EXTRACTION FINISHED by /root at 2026-07-14T02:10:55Z.

    Exact source commit: fbc9494c60b22f25c3c30134580f4bd93a02a715
    Exact base: 2c7332ca547ba98a57863a6f2f41849ffb386e9f
    Exact diff SHA-256: f193a0b8b9e4731ff92d20f141e889e2abe345fbf3cea46107cd87397f46ca42
    Exact branch: codex/issue-204-refund-response-validation
    Owned paths: 10; worktree clean after commit.

    Red evidence:

    • The original dual-endpoint matrix exposed 46 unsafe stored-target/provider-result cases.
    • Two endpoint cases exposed raw pre-commit storage errors after a succeeded provider result.
    • The final matrix also models commit-then-lost-acknowledgement, proving why local state must be treated as unknown.

    Green local evidence on Node 20:

    • Focused validator plus both endpoints: 2 suites / 174 tests.
    • Full Functions: 18 passed suites, 2 emulator-only skipped; 1,180 passed, 32 skipped.
    • Functions lint passed.
    • Frontend: 11 suites / 198 tests; SPA callback 11/11; diagnostic build passed.
    • Firestore Rules: 4 suites / 348 tests.
    • Artifact safety: 22/22; workflow/release safety: 12/12.
    • git diff --check passed; 15 changed-document relative links resolve.
    • Three independent reviews approved the corrected exact snapshot with no findings.

    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.

  6. added a commit that references this issue on Jul 14, 2026
    0dbac06
  7. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    COMPLETE / CLAIM RELEASED by /root at 2026-07-14T02:15:51Z.

    Source and merge:

    • Exact independently reviewed source: fbc9494c60b22f25c3c30134580f4bd93a02a715
    • PR: PAY-005A2: validate Stripe refund results #205
    • Squash merge on main: 0dbac066a49837e31af72c9821c8044d287035fc
    • Exact diff SHA-256: f193a0b8b9e4731ff92d20f141e889e2abe345fbf3cea46107cd87397f46ca42
    • Three independent security, test, and officer-documentation reviews approved with no remaining findings.

    Hosted proof:

    Deployment/provider/live truth:

    • No website production publication or runmprc.com verification occurred; this change has no frontend runtime artifact.
    • No protected release was dispatched. The current fixed backend release plan is profile-recovery and does not include these refund Functions.
    • Firebase Functions were not deployed or read back.
    • Stripe configuration was not changed, no Stripe test/live request was made, and no real refund or production record was touched.
    • Production behavior is not verified. The officer refund procedure remains NOT AVAILABLE YET.
    • A lost local-write acknowledgement remains an unknown local state: do not retry; reconcile.

    All acceptance boxes are checked, the status claim is removed, and the ten-path #204 claim is released.

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

Metadata

Metadata

Assignees

Labels

area:firebaseFirebase services and dataarea:stripeStripe payments and financial lifecyclepriority:P0Launch blocker or urgent security risksize:SSmall focused issuetype: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