Skip to content

feat(payments): manual payout tracking for single-account providers (#493) - #503

Merged
guillermoscript merged 1 commit into
masterfrom
feat/manual-payouts-tracking-493
Jul 24, 2026
Merged

feat(payments): manual payout tracking for single-account providers (#493)#503
guillermoscript merged 1 commit into
masterfrom
feat/manual-payouts-tracking-493

Conversation

@guillermoscript

Copy link
Copy Markdown
Owner

Description

Part of epic #493 (Payout & non-payment enforcement integrity). This is the foundational piece the epic's Phase 2/3 sub-issues (#496#500) build on top of — it had been implemented and live-verified locally but never actually committed anywhere, so nothing in the epic's Phase 2/3 list could be worked on until it existed on a branch.

PayPal, Binance Pay (merchant/hosted), and Lemon Squeezy all settle 100% of every sale into the platform's own account — unlike Stripe Connect (per-tenant connected account, automatic application_fee_amount split) or Solana (on-chain split in the same transaction). For those three providers, the school's revenue share has to be paid out manually, and the payouts table existed in the schema but nothing had ever inserted into it.

Changes made

  • lib/payments/types.ts — new ProviderCapabilities.settlesToPlatformAccount flag, set per-provider (true for paypal, lemonsqueezy, binance; false for stripe, solana, solana_subs, manual, binance_personal) — the single source of truth for which providers route through this manual-payout path.
  • lib/payments/payouts-owed.ts — pure computeOwedBalances(): gross collected per platform-settled provider × the tenant's revenue_splits.school_percentage, minus manual payouts already recorded, per tenant. No DB access, fully unit-tested in isolation.
  • app/actions/platform/payouts.tsgetPayoutsOwed() (super-admin-gated, wires the DB reads to the pure function) and markPayoutPaid() (inserts a payouts row with payout_method: 'manual').
  • app/[locale]/platform/payouts/page.tsx + components/platform/mark-payout-paid-dialog.tsx — metric cards (currently owed / collected / paid out all-time) + a by-school table with a "Mark as Paid" action, mirroring the visual pattern already used by the other /platform/** admin pages.
  • components/platform-sidebar.tsx — new "Payouts" nav item.
  • supabase/migrations/20260724120000_manual_payouts.sql — extends the previously-dead payouts table with payout_method (stripe_connect | manual), note, recorded_by, and makes period_start/period_end nullable (manual payouts are a running balance, not tied to a Connect payout period).

Type of change

  • New feature

Part of #493 (not closing it — the epic's Phase 2 money-accuracy issues #496#499 and Phase 3 deployment issue #500 are follow-up work building on this).

Testing

  • npx vitest run tests/unit/payouts-owed.test.ts — 10/10 passing.
  • npm run test:unit — 269/269 passing, no regressions.
  • npm run typecheck — clean (also fixed one pre-existing any in lib/payments/types.ts that lint-staged surfaced because the file was touched, unrelated to this feature's logic).
  • npm run lint — no errors on any changed file.
  • npm run build — succeeds; /platform/payouts registered as a route.
  • Manual verification against local Supabase: applied the migration via db:reset, logged in as a super admin (owner@e2etest.com), loaded /platform/payouts — metric cards, empty-state table, and the disabled "Mark as Paid" state (at $0 owed) all render correctly. A full PayPal-sale-to-payout flow was live-verified in an earlier local session (see epic EPIC: Payout & non-payment enforcement integrity — revenue-split accuracy and edge cases #493's description: $10 sale → $8.00 owed at 80% split → Mark as Paid → $0.00, payouts row inserted correctly) — not re-run here since the computation logic is what changed shape, and that's covered by the unit tests.

Known gaps, tracked as follow-up issues, not fixed here: revenue split isn't time-sliced (#496), amounts aren't grouped by currency (#497), no refund/chargeback clawback (#498), no overpayment guardrail or tenant-facing visibility (#499). This PR intentionally ships the feature as originally built so those can be reviewed and fixed as independent, reviewable diffs rather than one large change.

QA verification steps

  1. Log in as a super admin (locally: owner@e2etest.com / password123) and open /platform/payouts.
  2. With no successful platform-settled transactions, confirm all three metric cards read $0.00 and "Mark as Paid" is disabled for every school.
  3. Create a successful PayPal transaction for a tenant (or use existing seed data), reload the page, and confirm "Currently Owed" reflects amount × school_percentage.
  4. Click "Mark as Paid", confirm the pre-filled amount matches the owed amount, submit, and confirm the row now shows the payout recorded and "Owed" drops to $0.00.

Screenshot

A screenshot of the empty-state /platform/payouts page is attached as a PR comment.

…493)

PayPal, Binance Pay (merchant), and Lemon Squeezy settle 100% of every sale
into the platform's own account with no automatic split (unlike Stripe
Connect or the on-chain Solana split) — the school's share has to be paid
out manually, and the previously dead `payouts` table never recorded that.

Adds /platform/payouts (metric cards + by-school table, mirroring the
existing platform admin page pattern) backed by a pure computeOwedBalances()
function: gross collected per platform-settled provider, minus manual
payouts already recorded, per tenant. `ProviderCapabilities.settlesToPlatformAccount`
is the single source of truth for which providers route through this path.

Live-verified against a real PayPal sale locally (issue #493 epic notes:
$10 sale -> $8.00 owed at 80% split -> Mark as Paid -> $0.00, payouts row
inserted correctly). Known gaps in the money math (revenue split not
time-sliced, no currency grouping, no refund clawback, no overpayment
guardrail) are tracked as epic #493's Phase 2 sub-issues (#496-#499) and
addressed in follow-up commits; deploying the migration to cloud is #500.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@guillermoscript guillermoscript added enhancement New feature or request payments Payment provider / billing related labels Jul 24, 2026
@guillermoscript guillermoscript self-assigned this Jul 24, 2026
@guillermoscript

Copy link
Copy Markdown
Owner Author

Verification screenshot — /platform/payouts loaded as a super admin against local seed data (no successful platform-settled transactions yet, so metric cards read $0.00 and "Mark as Paid" is correctly disabled at $0 owed). The "Payouts" sidebar nav item and the "Billing Health" item from the earlier PR now coexist correctly. Please sanity-check the visual before merging.

payouts-page

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

Labels

enhancement New feature or request payments Payment provider / billing related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant