Admin endpoint: record non-Stripe payments (cash/Apple Pay/check) #509

Open
opened 2026-04-23 00:52:11 +00:00 by forgejo_admin · 0 comments
Contributor

Type

Feature

Lineage

Standalone — discovered during 2026-04-22 T2 blast scoping. Two orders (Romial #51, Vince #70) required manual UPDATE to mark T1 Utah Invitational cash payments as paid. Need a self-serve path for future off-Stripe receipts.

Repo

forgejo_admin/basketball-api

User Story

As a Westside admin,
I want to record cash / Apple Pay / check receipts against an order,
So that parents who paid off-Stripe aren't double-billed in future blasts and aren't falsely shown as unpaid in reports.

Context

Our Stripe webhook (routes/webhooks.py::_handle_generic_order_completed) correctly flips orders.status='paid' and stamps stripe_payment_intent_id when parents pay via Stripe link. The 2026-04-22 reconciliation verified zero Stripe↔DB mismatches — the automated side works.

The gap is DB↔reality: cash, Apple Pay-to-Marcus, and check payments never enter Stripe, so nothing marks those orders as paid. Marcus has tracked off-ledger; our DB has always been incomplete by the cash-cohort size. This endpoint is the side-channel intake.

Uses existing orders.status + orders.custom_data jsonb columns — no schema change, no migration.

File Targets

Files the agent should modify or create:

  • src/basketball_api/routes/admin.py — add POST /admin/orders/{order_id}/record-payment route, follow existing admin-route style
  • tests/routes/test_admin.py — new tests for the endpoint

Files the agent should NOT touch:

  • src/basketball_api/models.py — no schema change needed; existing columns are sufficient
  • src/basketball_api/routes/webhooks.py — Stripe webhook is correct; do not modify
  • src/basketball_api/routes/checkout.py — unrelated

Acceptance Criteria

  • POST /admin/orders/{order_id}/record-payment exists, requires admin auth
  • Request body: {method: "cash" | "apple_pay" | "check", amount_cents: int, notes?: str, recorded_by: str}
  • On success: orders.status flips from pending to paid; orders.custom_data jsonb merged with {payment_method, recorded_by, recorded_at (iso8601), notes} — does NOT overwrite existing custom_data keys
  • Idempotent: re-calling on an already-paid order returns 409 with the current status
  • Unknown order_id returns 404
  • Non-admin caller returns 403
  • Does NOT create any Stripe objects (no PaymentIntent, no Customer, no charge)
  • Method values outside the allowed enum return 422

Test Expectations

  • Unit test: happy path marks a pending order paid with correct custom_data audit trail
  • Unit test: re-call on paid order returns 409, does not re-stamp audit trail
  • Unit test: unknown order returns 404
  • Unit test: non-admin caller returns 403
  • Unit test: invalid method value returns 422
  • Integration test: after endpoint call, a subsequent GET /admin/orders?status=paid includes this order
  • Run command: pytest tests/routes/test_admin.py -k record_payment -v

Constraints

  • Match existing admin-route auth pattern (reuse whatever admin.py currently uses for admin gating)
  • custom_data merge must use COALESCE(custom_data, '{}'::jsonb) || jsonb_build_object(...) equivalent — do not replace existing keys
  • Do not import Stripe SDK for this route; it must be Stripe-free
  • amount_cents stored in custom_data for audit; does NOT change orders.amount_cents (that's the product price)
  • No admin UI in this ticket — API only

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • project-westside-basketball — project this affects
  • Out-of-scope follow-ups: (a) variant for (player_id, product_id) when no Order row exists (orphan-roster case), (b) reconciliation report endpoint "who paid T1 in cash", (c) admin UI for this endpoint
### Type Feature ### Lineage Standalone — discovered during 2026-04-22 T2 blast scoping. Two orders (Romial #51, Vince #70) required manual UPDATE to mark T1 Utah Invitational cash payments as paid. Need a self-serve path for future off-Stripe receipts. ### Repo `forgejo_admin/basketball-api` ### User Story As a Westside admin, I want to record cash / Apple Pay / check receipts against an order, So that parents who paid off-Stripe aren't double-billed in future blasts and aren't falsely shown as unpaid in reports. ### Context Our Stripe webhook (`routes/webhooks.py::_handle_generic_order_completed`) correctly flips `orders.status='paid'` and stamps `stripe_payment_intent_id` when parents pay via Stripe link. The 2026-04-22 reconciliation verified zero Stripe↔DB mismatches — the automated side works. The gap is DB↔reality: cash, Apple Pay-to-Marcus, and check payments never enter Stripe, so nothing marks those orders as paid. Marcus has tracked off-ledger; our DB has always been incomplete by the cash-cohort size. This endpoint is the side-channel intake. Uses existing `orders.status` + `orders.custom_data` jsonb columns — **no schema change, no migration**. ### File Targets Files the agent should modify or create: - `src/basketball_api/routes/admin.py` — add `POST /admin/orders/{order_id}/record-payment` route, follow existing admin-route style - `tests/routes/test_admin.py` — new tests for the endpoint Files the agent should NOT touch: - `src/basketball_api/models.py` — no schema change needed; existing columns are sufficient - `src/basketball_api/routes/webhooks.py` — Stripe webhook is correct; do not modify - `src/basketball_api/routes/checkout.py` — unrelated ### Acceptance Criteria - [ ] `POST /admin/orders/{order_id}/record-payment` exists, requires admin auth - [ ] Request body: `{method: "cash" | "apple_pay" | "check", amount_cents: int, notes?: str, recorded_by: str}` - [ ] On success: `orders.status` flips from `pending` to `paid`; `orders.custom_data` jsonb merged with `{payment_method, recorded_by, recorded_at (iso8601), notes}` — does NOT overwrite existing custom_data keys - [ ] Idempotent: re-calling on an already-`paid` order returns 409 with the current status - [ ] Unknown order_id returns 404 - [ ] Non-admin caller returns 403 - [ ] Does NOT create any Stripe objects (no PaymentIntent, no Customer, no charge) - [ ] Method values outside the allowed enum return 422 ### Test Expectations - [ ] Unit test: happy path marks a `pending` order `paid` with correct `custom_data` audit trail - [ ] Unit test: re-call on `paid` order returns 409, does not re-stamp audit trail - [ ] Unit test: unknown order returns 404 - [ ] Unit test: non-admin caller returns 403 - [ ] Unit test: invalid method value returns 422 - [ ] Integration test: after endpoint call, a subsequent `GET /admin/orders?status=paid` includes this order - Run command: `pytest tests/routes/test_admin.py -k record_payment -v` ### Constraints - Match existing admin-route auth pattern (reuse whatever `admin.py` currently uses for admin gating) - `custom_data` merge must use `COALESCE(custom_data, '{}'::jsonb) || jsonb_build_object(...)` equivalent — do not replace existing keys - Do not import Stripe SDK for this route; it must be Stripe-free - `amount_cents` stored in custom_data for audit; does NOT change `orders.amount_cents` (that's the product price) - No admin UI in this ticket — API only ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `project-westside-basketball` — project this affects - Out-of-scope follow-ups: (a) variant for `(player_id, product_id)` when no Order row exists (orphan-roster case), (b) reconciliation report endpoint "who paid T1 in cash", (c) admin UI for this endpoint
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/basketball-api#509
No description provided.