Some checks failed
CI / test (pull_request) Failing after 1m10s
3 nouveaux templates email (best-effort, dry-run sans Resend) : - sendRentalRequestedTenant : récap de demande au locataire (par RB) - sendRentalRequestedProvider : nouvelle demande au prestataire - sendRentalConfirmed : confirmation paiement reçu Branchements : - POST /api/rentals/checkout : envoie tenant + provider après création des RentalBooking (PENDING), catch global pour ne pas bloquer - Webhook Stripe rental-bundle : envoie sendRentalConfirmed à chaque locataire après update CONFIRMED+SUCCEEDED Plugin gear-rental : - Ajout au registry (catégorie business) - layout.tsx /materiel + /espace-prestataire avec requirePluginOr404 - requirePluginOr404 dans /panier et /mes-locations - isPluginEnabled guard dans POST /api/rentals/checkout (404 si off) - SiteHeader masque liens Matériel / Mes locations / Espace prestataire + CartBadge si plugin désactivé - CompleteYourStay renvoie null si plugin désactivé Décision admin → activable depuis /admin/plugins comme tous les autres. Tests vitest (tests/lib/rentals.test.ts, 16 tests) : - diffDays (mêmes dates, 1 nuit, 7 jours, négatif) - parseCart (null/garbage/schéma invalide/valide/format date) - serializeCart (updatedAt, roundtrip) - commission formula (0%, 15%, arrondi centime) - availability arithmetic (totalQty libre, soustractions, plancher 0) 53 tests pass total. Build OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import { diffDays, parseCart, serializeCart, EMPTY_CART } from "@/lib/rental-cart";
|
|
|
|
describe("diffDays", () => {
|
|
it("renvoie 0 pour mêmes dates", () => {
|
|
expect(diffDays("2026-06-01", "2026-06-01")).toBe(0);
|
|
});
|
|
it("compte 1 nuit entre J et J+1", () => {
|
|
expect(diffDays("2026-06-01", "2026-06-02")).toBe(1);
|
|
});
|
|
it("compte 7 jours sur une semaine", () => {
|
|
expect(diffDays("2026-06-01", "2026-06-08")).toBe(7);
|
|
});
|
|
it("ne renvoie pas de valeur négative si end < start", () => {
|
|
expect(diffDays("2026-06-08", "2026-06-01")).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("parseCart", () => {
|
|
it("retourne EMPTY_CART pour null/undefined/garbage", () => {
|
|
expect(parseCart(null)).toEqual(EMPTY_CART);
|
|
expect(parseCart(undefined)).toEqual(EMPTY_CART);
|
|
expect(parseCart("")).toEqual(EMPTY_CART);
|
|
expect(parseCart("not json")).toEqual(EMPTY_CART);
|
|
});
|
|
it("retourne EMPTY_CART quand le schéma est invalide", () => {
|
|
expect(parseCart(JSON.stringify({ v: 99, items: [] }))).toEqual(EMPTY_CART);
|
|
expect(parseCart(JSON.stringify({ v: 1, items: "nope" }))).toEqual(EMPTY_CART);
|
|
});
|
|
it("accepte un panier valide", () => {
|
|
const valid = {
|
|
v: 1,
|
|
items: [
|
|
{ itemId: "abc", qty: 2, startDate: "2026-06-01", endDate: "2026-06-03" },
|
|
],
|
|
};
|
|
const out = parseCart(JSON.stringify(valid));
|
|
expect(out.items).toHaveLength(1);
|
|
expect(out.items[0].qty).toBe(2);
|
|
});
|
|
it("rejette une date au mauvais format", () => {
|
|
const bad = {
|
|
v: 1,
|
|
items: [{ itemId: "abc", qty: 1, startDate: "1/6/2026", endDate: "2026-06-03" }],
|
|
};
|
|
expect(parseCart(JSON.stringify(bad))).toEqual(EMPTY_CART);
|
|
});
|
|
});
|
|
|
|
describe("serializeCart", () => {
|
|
it("ajoute un updatedAt ISO", () => {
|
|
const s = serializeCart({ v: 1, items: [] });
|
|
const parsed = JSON.parse(s);
|
|
expect(parsed.v).toBe(1);
|
|
expect(parsed.updatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
});
|
|
it("roundtrip parse(serialize(x)) === x sur les items", () => {
|
|
const cart = {
|
|
v: 1 as const,
|
|
items: [
|
|
{ itemId: "k1", qty: 3, startDate: "2026-07-01", endDate: "2026-07-08" },
|
|
],
|
|
};
|
|
const round = parseCart(serializeCart(cart));
|
|
expect(round.items).toEqual(cart.items);
|
|
});
|
|
});
|
|
|
|
// Calcul de commission (snapshot de la logique métier dans l'API checkout).
|
|
// Ce test sert de garde-fou : si la formule change, faire évoluer aussi
|
|
// `/api/rentals/checkout` (cf. commissionAmount = itemsTotal * pct / 100).
|
|
describe("rental commission formula", () => {
|
|
function commission(itemsTotal: number, pct: number): number {
|
|
return Math.round((itemsTotal * pct) / 100 * 100) / 100;
|
|
}
|
|
|
|
it("0% commission System D", () => {
|
|
expect(commission(120, 0)).toBe(0);
|
|
});
|
|
it("15% sur 200€ = 30€", () => {
|
|
expect(commission(200, 15)).toBe(30);
|
|
});
|
|
it("arrondit au centime", () => {
|
|
expect(commission(33.33, 15)).toBe(5);
|
|
});
|
|
});
|
|
|
|
// Disponibilité : la quantité libre = totalQty - somme des qty bloquées
|
|
// chevauchant la fenêtre. Snapshot de la logique de `getItemAvailability`.
|
|
describe("rental availability arithmetic", () => {
|
|
function availableQty(totalQty: number, blockedQtys: number[]): number {
|
|
const used = blockedQtys.reduce((a, b) => a + b, 0);
|
|
return Math.max(0, totalQty - used);
|
|
}
|
|
it("totalQty quand rien n'est bloqué", () => {
|
|
expect(availableQty(5, [])).toBe(5);
|
|
});
|
|
it("soustrait les blocages", () => {
|
|
expect(availableQty(5, [2, 1])).toBe(2);
|
|
});
|
|
it("ne renvoie jamais de valeur négative", () => {
|
|
expect(availableQty(3, [5])).toBe(0);
|
|
});
|
|
});
|