Skip to content

PAY-005A1 — Prevent invalid partial refunds from becoming full refunds #200

Closed

Description

@daliu

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

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_partial accepts 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; only refund_full may 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.js
  • functions/adminRegistrationAction.js
  • functions/commerceControlEntryPoints.test.js for 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 1 and original - 1 include the exact Stripe amount and remain classified as partial.
  • Explicit full-refund actions omit amount and 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

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

  1. self-assigned this
    on Jul 14, 2026
  2. 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
    type:testingTest infrastructure and quality gates
    on Jul 14, 2026
  3. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    CLAIMED by /root at 2026-07-14T00:56:51Z; branch codex/issue-200-partial-refund-containment; exact base a8801770e97cf21d81f3307f5893734115140d8f.

    Owned runtime/test paths:

    • new functions/refundValidation.js
    • new functions/refundValidation.test.js
    • functions/adminOrderAction.js
    • functions/adminRegistrationAction.js
    • functions/commerceControlEntryPoints.test.js

    Owned documentation after current owners release the affected hunks: minimum PAY-005A1 status sections in SECURITY.md, GITHUB_ISSUES.md, GITHUB_ISSUE_SLICES.md, and OPERATIONS_RUNBOOK.md; only the refund section of docs/officers/EVENTS_SHOP_MEMBERS.md after #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.

  4. added
    status:in-progressClaimed and actively being implemented
    and removed
    status:readyDependencies complete and ready to claim
    on Jul 14, 2026
  5. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    EXTRACTION FINISHED — 2026-07-14T01:14:10Z

    PR #201 is ready for review at exact head 346df6c6726ca46ac1f46e5b36af5364865e8ff1.

    Evidence:

    • Red proof: 52 new endpoint assertions fail on the previous implementation, including cases that resolve as refunds after the partial amount is omitted.
    • Green focused proof: 2 suites / 99 tests.
    • Full local proof: Functions 1,077 tests; frontend 198; Rules 348; SPA 11; release/workflow/artifact 42; Functions/frontend lint and diagnostic build pass.
    • Independent security, test, and backup-officer reviews approve the exact commit and full diff SHA-256 091ad489adec4608f451569a7af3eda24de599e2c38018432d920616ad77ca1b with 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.

  6. daliu commented on Jul 14, 2026

    @daliu
    ContributorAuthor

    COMPLETE / CLAIM RELEASED — 2026-07-14T01:18:34Z

    PR #201 merged to main as squash commit caae31ff427e91325d34a6c901faf81ad1dc49df from exact reviewed source head 346df6c6726ca46ac1f46e5b36af5364865e8ff1.

    Outcome:

    • invalid or malformed partial-refund amounts now fail before Stripe client construction, a Stripe refund request, or a Firestore business-record write;
    • every admitted partial refund sends the exact validated integer-cent amount;
    • only an explicit full-refund action may omit amount;
    • an uncertain provider result exposes no provider detail, keeps local state unchanged, says do not retry, and directs escalation for reconciliation.

    Verification:

    • red proof before the fix: 52 endpoint-matrix failures;
    • focused green proof: 2 suites / 99 tests;
    • full Functions: 17 suites / 1,077 tests, with the existing 2 suites / 32 emulator-only tests skipped by the harness;
    • frontend: 11 suites / 198 tests;
    • Firestore Rules: 4 suites / 348 tests;
    • SPA callback safety: 11 tests;
    • release/workflow/artifact safety: 42 tests;
    • Functions lint and diagnostic production build passed;
    • independent security, test, and backup-officer reviews approved the exact head and full diff with no findings;
    • exact-head PR CI run 29298036773 passed all required jobs;
    • exact-merge CI run 29298127585 passed frontend, Functions, Rules, artifact scrubber, and commerce journal jobs.

    Deployment truth:

    • source changed, tested, reviewed, merged, and post-merge CI passed;
    • no protected release ran for these two refund Functions;
    • Firebase Functions were not deployed;
    • no Stripe setting or provider state changed and no Stripe call was made;
    • no website production publication, runmprc.com verification, real refund, production record, member/customer data, or live card was used;
    • production behavior remains not verified.

    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.js
    • functions/adminRegistrationAction.js
    • functions/commerceControlEntryPoints.test.js
    • functions/refundValidation.js
    • functions/refundValidation.test.js
    • GITHUB_ISSUES.md
    • GITHUB_ISSUE_SLICES.md
    • SECURITY.md
    • OPERATIONS_RUNBOOK.md
    • docs/officers/EVENTS_SHOP_MEMBERS.md
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 boundarytype:testingTest infrastructure and quality gates

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions