This morning customers started paying. Tonight we finish the money story — where the money goes. 💸 Barbers get paid through Stripe Connect, your 10% platform fee shows transparently in their earnings, and refunds work automatically when a customer cancels >24h before the appointment.
Paste this into Claude Code / Cursor (agent mode): ⬇️
Finish the money flow for ClipBook (Next.js 14 App Router, TypeScript, Tailwind CSS, Supabase, Stripe test mode from Day 11A).
CONTEXT: This morning built PaymentIntent at booking (full or 20% deposit via PAYMENT_MODE setting) and a webhook flipping bookings to 'confirmed'. The /barber/earnings page from Day 9B currently shows GROSS amounts — it must now show NET payouts.
1. MONEY HELPERS — lib/money.ts:
- PLATFORM_FEE_PERCENT = 10 (single constant, easy to change later).
- Functions: centsToDisplay, grossToFee(gross), grossToNet(gross). All money in integer cents, display in dollars. No float math anywhere.
2. EARNINGS PAGE UPGRADE (/barber/earnings, from Day 9B):
- Stat cards become: Gross collected / Platform fee (10%) / Net payout / Refunded. The week/month totals recompute the same way.
- Per-booking breakdown table gains three columns: Gross | Fee (10%) | Net — computed per booking, and totals must equal the stat cards.
- A "Payout method" card: shows connected payout account status. For now a Stripe Connect onboarding placeholder button ("Set up payouts") that marks status as pending — real Express onboarding goes live when Stripe is in live mode.
3. CONNECT SCHEMA — add stripe_account_id (nullable text) to barber_profiles via a migration. One barber = one account; keep it simple.
4. REFUND POLICY + CANCEL LOGIC:
- Policy constant: CANCELLATION_WINDOW_HOURS = 24.
- On the booking confirmation page, show the policy line: "Free cancellation up to 24h before your appointment. Later cancellations are not refunded."
- Cancel flow on /bookings/[id]: customer clicks Cancel → if more than 24h out: call POST /api/payments/refund (server-side, creates a Stripe refund against the booking's payment intent, marks booking 'refunded', updates earnings). If under 24h: block with "This booking can't be refunded under the 24h policy."
- Barber cancels (Day 8 management): ALWAYS auto-refund the customer regardless of window.
5. WEBHOOK — extend /api/payments/webhook: handle charge.refunded → update the booking row and any cached earnings so the earnings page reflects it. Idempotent: a refund event for an already-refunded booking changes nothing.
6. POLISH: refunds show as a separate "Refunded" stat, never silently subtracted; the booking status timeline from Day 6 gains a 'refunded' state; dark theme; mobile 375px clean.
ACCEPTANCE CRITERIA:
- Every earnings number reconciles: Gross − Fee − Refunded = Net, on both stat cards and the table total
- Cancelling >24h out refunds via Stripe test mode and the booking shows 'refunded'
- Cancelling <24h out is blocked with the policy message; no Stripe call fires
- charge.refunded webhook updates earnings; replaying the event is a no-op
- All amounts computed in integer cents — no $19.999999 anywhere
- `npm run build` passes with no errors
What you'll get when it works 👇