All checks were successful
CI / test (pull_request) Successful in 2m40s
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>
37 lines
1.3 KiB
TypeScript
37 lines
1.3 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
|
|
vi.mock("server-only", () => ({}));
|
|
// payouts.ts importe prisma qui jette si DATABASE_URL absent — mock le module entier.
|
|
vi.mock("@/lib/prisma", () => ({ prisma: {} }));
|
|
|
|
const { monthKey, formatMonth } = await import("@/lib/payouts");
|
|
|
|
describe("monthKey", () => {
|
|
it("normalise à minuit UTC du 1er du mois", () => {
|
|
const k = monthKey(new Date("2026-03-15T14:30:00Z"));
|
|
expect(k.getUTCFullYear()).toBe(2026);
|
|
expect(k.getUTCMonth()).toBe(2); // mars = 2 (0-indexed)
|
|
expect(k.getUTCDate()).toBe(1);
|
|
expect(k.getUTCHours()).toBe(0);
|
|
expect(k.getUTCMinutes()).toBe(0);
|
|
});
|
|
|
|
it("idempotent (mêmes inputs → même sortie)", () => {
|
|
const a = monthKey(new Date("2026-06-30T23:59:59Z"));
|
|
const b = monthKey(new Date("2026-06-01T00:00:00Z"));
|
|
expect(a.getTime()).toBe(b.getTime());
|
|
});
|
|
|
|
it("traverse janvier sans bug", () => {
|
|
const k = monthKey(new Date("2026-01-15T10:00:00Z"));
|
|
expect(k.toISOString().slice(0, 10)).toBe("2026-01-01");
|
|
});
|
|
});
|
|
|
|
describe("formatMonth", () => {
|
|
it("rend un libellé fr-FR lisible", () => {
|
|
const label = formatMonth(new Date(Date.UTC(2026, 5, 1)));
|
|
expect(label).toContain("juin");
|
|
expect(label).toContain("2026");
|
|
});
|
|
});
|