feat(rental): Sprint O — reversements prestataires #89

Merged
tarzzan merged 1 commit from feat/rental-sprint-o into main 2026-06-03 02:59:50 +00:00
Owner

Sprint O — Reversements prestataires (payouts)

Comble le gap ops du marketplace rental : System D encaisse centralisé via Stripe, mais l'admin n'a aucune visibilité sur ce qu'il doit reverser à chaque prestataire chaque mois.

Schema (migration appliquée prod)

  • RentalPayoutMark { id, providerId, periodMonth, amount, reference, paidAt, paidByEmail }
  • Unique (providerId, periodMonth) → 1 mark = 1 mois = 1 virement par provider

src/lib/payouts.ts

  • monthKey(d) → 1er du mois minuit UTC (clé de période normalisée)
  • listProviderPayouts({monthsBack=6}) → grid provider × mois avec bookingsCount + grossAmount (itemsTotal) + commission + netAmount (gross-commission) + statut paid via mark
  • Exclut System D (commission 0 %, géré par l'asso elle-même)
  • createPayoutMark (idempotent via findUnique avant insert) + deletePayoutMark

Politique : net dû = itemsTotal - commissionAmount (la depositTotal n'entre pas dans le flux — elle est collectée par le provider auprès du client). Politique documentée dans le commentaire en tête de payouts.ts.

/admin/payouts/page.tsx

  • 3 KPIs (À payer / Déjà payé / Mois affichés)
  • Une section par mois (6 derniers), tableau provider × CA brut + commission + net dû + statut
  • MarkPaidForm : bouton « Marquer payé » → form inline (amount pré-rempli avec net dû, reference optionnelle) → action markPayoutPaidAction. Statut payé affiche amount + ref + bouton « Annuler marquage »

Server actions

  • markPayoutPaidAction (admin only, idempotent, audit admin.payouts/payout.mark ou payout.already_marked) → envoie sendPayoutSent au contactEmail du provider (best-effort)
  • unmarkPayoutPaidAction → delete + audit payout.unmark

Email sendPayoutSent

Notification au provider quand un virement est marqué payé. Inclut amount + reference + lien dashboard.

UX

Sidebar admin gagne entrée « Reversements » sous « Activité ».

Tests

tests/lib/payouts.test.ts (4 cas) :

  • monthKey normalisation UTC (n'importe quel jour du mois → 1er minuit UTC)
  • idempotence (même clé pour différents jours du même mois)
  • janvier sans bug (no off-by-one)
  • formatMonth libellé fr-FR

Total : 74/74 ✓ (70 précédents + 4 nouveaux). Lint + typecheck + build ✓.

Test plan

  • Vitest 74/74 ✓
  • ADMIN → /admin/payouts → liste 6 mois, System D absent, providers tiers présents
  • Provider avec 0 résa ce mois → ligne avec montant 0 + dash
  • Bouton « Marquer payé » → form inline → submit → mark créée + email dry-run vers contactEmail
  • Re-marquer même mois → idempotent (alreadyExists=true, pas de re-email)
  • Bouton « Annuler marquage » → mark supprimée + audit

🤖 Generated with Claude Code

## Sprint O — Reversements prestataires (payouts) Comble le gap ops du marketplace rental : System D encaisse centralisé via Stripe, mais l'admin n'a aucune visibilité sur ce qu'il doit reverser à chaque prestataire chaque mois. ### Schema (migration appliquée prod) - `RentalPayoutMark { id, providerId, periodMonth, amount, reference, paidAt, paidByEmail }` - Unique `(providerId, periodMonth)` → 1 mark = 1 mois = 1 virement par provider ### `src/lib/payouts.ts` - `monthKey(d)` → 1er du mois minuit UTC (clé de période normalisée) - `listProviderPayouts({monthsBack=6})` → grid provider × mois avec bookingsCount + grossAmount (itemsTotal) + commission + netAmount (gross-commission) + statut paid via mark - Exclut **System D** (commission 0 %, géré par l'asso elle-même) - `createPayoutMark` (idempotent via findUnique avant insert) + `deletePayoutMark` **Politique** : net dû = `itemsTotal - commissionAmount` (la `depositTotal` n'entre pas dans le flux — elle est collectée par le provider auprès du client). Politique documentée dans le commentaire en tête de `payouts.ts`. ### `/admin/payouts/page.tsx` - 3 KPIs (À payer / Déjà payé / Mois affichés) - Une section par mois (6 derniers), tableau provider × CA brut + commission + net dû + statut - `MarkPaidForm` : bouton « Marquer payé » → form inline (amount pré-rempli avec net dû, reference optionnelle) → action `markPayoutPaidAction`. Statut payé affiche amount + ref + bouton « Annuler marquage » ### Server actions - `markPayoutPaidAction` (admin only, idempotent, audit `admin.payouts/payout.mark` ou `payout.already_marked`) → envoie `sendPayoutSent` au contactEmail du provider (best-effort) - `unmarkPayoutPaidAction` → delete + audit `payout.unmark` ### Email `sendPayoutSent` Notification au provider quand un virement est marqué payé. Inclut amount + reference + lien dashboard. ### UX Sidebar admin gagne entrée « Reversements » sous « Activité ». ### Tests `tests/lib/payouts.test.ts` (4 cas) : - `monthKey` normalisation UTC (n'importe quel jour du mois → 1er minuit UTC) - idempotence (même clé pour différents jours du même mois) - janvier sans bug (no off-by-one) - `formatMonth` libellé fr-FR **Total : 74/74 ✓** (70 précédents + 4 nouveaux). Lint + typecheck + build ✓. ### Test plan - [x] Vitest 74/74 ✓ - [ ] ADMIN → `/admin/payouts` → liste 6 mois, System D absent, providers tiers présents - [ ] Provider avec 0 résa ce mois → ligne avec montant 0 + dash - [ ] Bouton « Marquer payé » → form inline → submit → mark créée + email dry-run vers contactEmail - [ ] Re-marquer même mois → idempotent (alreadyExists=true, pas de re-email) - [ ] Bouton « Annuler marquage » → mark supprimée + audit 🤖 Generated with [Claude Code](https://claude.com/claude-code)
tarzzan added 1 commit 2026-06-03 02:59:49 +00:00
feat(rental): Sprint O — reversements prestataires (payouts)
All checks were successful
CI / test (pull_request) Successful in 2m40s
5be62f012f
Marketplace encaisse centralisé sur System D → besoin de tracer les
virements mensuels aux prestataires tiers. Migration appliquée prod.

Schema :
- Modèle RentalPayoutMark { id, providerId, periodMonth, amount,
  reference, paidAt, paidByEmail }. Unique (providerId, periodMonth)
  → 1 mark = 1 mois = 1 virement par provider.

Lib src/lib/payouts.ts :
- monthKey(d) → 1er du mois minuit UTC (clé de période).
- listProviderPayouts({monthsBack=6}) → grid provider × mois avec
  bookingsCount + grossAmount (itemsTotal) + commission + netAmount
  (gross-commission) + statut paid via RentalPayoutMark. Exclut
  System D (commission 0%, géré par l'asso). Statut « payé » lu
  depuis les marks. Tri : mois desc puis providerName.
- createPayoutMark (idempotent via findUnique avant insert) +
  deletePayoutMark.

Politique : net dû = itemsTotal - commissionAmount (depositTotal
hors flux, collecté par le provider auprès du client). Politique
documentée dans le commentaire en tête de payouts.ts.

/admin/payouts/page.tsx :
- 3 KPIs (À payer / Déjà payé / Mois affichés).
- Une section par mois (6 derniers), tableau provider × CA brut +
  commission + net dû + statut.
- MarkPaidForm : bouton « Marquer payé » → form inline (amount
  pré-rempli avec net dû, reference optionnelle) → action
  markPayoutPaidAction. Statut payé montre amount + ref + bouton
  « Annuler marquage ».

Server actions :
- markPayoutPaidAction (admin only, idempotent, audit
  admin.payouts/payout.mark + payout.already_marked) → envoie
  sendPayoutSent au contactEmail du provider (best-effort).
- unmarkPayoutPaidAction → delete + audit payout.unmark.

Email sendPayoutSent : notification au provider quand un virement est
marqué payé. Inclut amount + reference + lien dashboard.

Sidebar admin gagne entrée « Reversements » sous Activité.

Tests vitest tests/lib/payouts.test.ts (4 cas) : monthKey
normalisation UTC + idempotence + janvier sans bug, formatMonth fr-FR.
Total : 74/74 ✓.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tarzzan merged commit eee052b2a8 into main 2026-06-03 02:59:50 +00:00
tarzzan deleted branch feat/rental-sprint-o 2026-06-03 02:59:50 +00:00
Sign in to join this conversation.
No reviewers
No labels
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: tarzzan/karbe#89
No description provided.