Skip to content

Staff finance and refunds (Phase 4 M) - #11

Merged
roncodes merged 24 commits into
release/v2.0.0from
feat/staff-finance
Oct 24, 2026
Merged

roncodes merged 24 commits into
release/v2.0.0from
feat/staff-finance

Conversation

@roncodes

@roncodes roncodes commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Stacked on #10 (feat/commerce). Finishes Phase 4 Workstream M: the staff finance view and refunds that the plan deferred.

What's in it

Finance API (/v1/admin/finance, behind the same auth:token,web, scope:review and staff middleware as the other admin routes)

  • GET /v1/admin/finance?from=&to= returns totals per currency, months per month and currency, and publishers per organization and currency (highest fee revenue first). Without a range it covers the last 12 months, including this one.
  • GET /v1/admin/finance/purchases?from=&to=&status=&publisher=&extension=&page= lists purchases with the publisher, customer, fee, refunds, refundable (what is left to refund), the dispute and the Stripe ids.
  • GET /v1/admin/finance/monthly.csv?from=&to=[&by=publisher] gives one row per month and currency, or per month, publisher and currency.
  • The numbers come from purchases (fee_amount, net_amount, status, meta.amount_refunded, meta.dispute), not from Stripe, so the view works without Stripe. fee_refunded is the fee in proportion to the amount refunded, which is what refund_application_fee returns. publisher_net subtracts the publisher's share of refunds (the reversed transfer). Disputes are counted but not subtracted.
  • Each purchase counts in the month of the sale, and so do its refunds and disputes. This is documented in the README.

Refunds: POST /v1/admin/purchases/{id}/refund takes {amount?, reason?, note?}.

  • It calls Stripe refunds->create with payment_intent, reverse_transfer: true and refund_application_fee: true. The idempotency key covers the purchase's refunded-so-far and the request, so a double click returns the same refund.
  • It records a purchase.refund_requested Event with the staff member, the amount, the reason and the note, and answers 202. Revoking the licence is still left to the existing charge.refunded webhook.
  • Refusals: 409 when the purchase is already refunded or disputed (Stripe won't refund a disputed charge), 422 above what is left or when there is no payment intent, 502 when Stripe refuses (through the existing handler), and 503 when Stripe isn't configured.

Finance page at /review/finance, with a Finance tab in the review console:

  • A date range, links to the monthly and by-publisher CSVs, and tiles per currency for fee revenue, gross, refunded and disputed.
  • Tables by month and by publisher.
  • Purchases filtered by status and publisher, with a Refund dialog. The dialog takes an amount (all or part), a reason and a note, and shows what the publisher and Fleetbase each give back and whether the licence will be revoked.

Notes

  • recordedSale() moved from PayoutsAndBillingTest.php to tests/Helpers/commerce.php so both test files can use it.
  • The Stripe fake answers POST /v1/refunds from the new tests/Fixtures/stripe/refund.json.

Tests

  • Pest: 443 passed after merging the latest feat/commerce (cae865f) (8 new tests in tests/Feature/Commerce/FinanceTest.php)
  • Vitest (Node 22): 131 passed (6 new tests in resources/js/components/review/Finance.test.jsx)
  • Pint and ESLint are clean

…d sale pricing; document the instance checkout contract
…m-side Stripe failures, highlight the selected organization on Billing
…voke the licence, staff licence management, return URLs on the instance host, members see licences
…otals, monthly CSV, refunds through Stripe, and the Finance page (Phase 4 M)
…st/{version} behind the licence gate, and dist size, sha256 and builtAgainst on catalog and update releases
# Conflicts:
#	app/Models/Instance.php
@roncodes
roncodes changed the base branch from feat/commerce to release/v2.0.0 October 24, 2026 09:22
@roncodes
roncodes merged commit 2e1cbc6 into release/v2.0.0 Oct 24, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant