karbe/tests/lib/rentals.test.ts
Ubuntu 5607a51980
Some checks failed
CI / test (pull_request) Failing after 1m10s
feat(rental): Sprint E — emails + plugin toggle + tests
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>
2026-06-02 08:49:39 +00:00

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);
});
});