From c564028ca996422470732391cbb7dc55e6642ce0 Mon Sep 17 00:00:00 2001
From: Ubuntu
Date: Wed, 3 Jun 2026 02:17:58 +0000
Subject: [PATCH] =?UTF-8?q?feat(rental):=20Sprint=20M=20=E2=80=94=20refond?=
=?UTF-8?q?s=20+=20annulations=20Stripe?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Politique de remboursement v1 :
- > 7 jours du début → FULL (location + caution)
- 1 à 7 jours → PARTIAL_50 (50% location + caution intégrale)
- < 24h ou passé → DEPOSIT_ONLY (caution seulement, pas de remboursement
sur la location)
src/lib/rental-refund.ts (NEW) : computeRentalRefund({startDate,
itemsTotal, depositTotal}) → { itemsRefund, depositRefund, totalRefund,
policy, policyLabel }. Arrondi au centime, support de Decimal.
POST /api/rentals/[id]/cancel :
- Auth multi-rôle : tenant de la booking, RENTAL_PROVIDER nominal ou
CE_MANAGER de l'org du provider, ADMIN. Détecte `cancelledBy` pour
adapter l'email.
- Refuse si status ∉ {PENDING, CONFIRMED} (HANDED_OVER → non
annulable, contacter Karbé).
- Calcule le refund selon la politique.
- Stripe refund best-effort si paymentStatus=SUCCEEDED + stripeSessionId
existante + isStripeConfigured + totalRefund > 0. Retrieve session →
payment_intent → refunds.create. Échec Stripe = audit-logged mais
le flip status continue (l'asso pourra rembourser manuellement).
- Transaction : update RentalBooking (CANCELLED + paymentStatus
REFUNDED si SUCCEEDED sinon FAILED) + delete RentalItemAvailability
(libère stock).
- Audit log rental.cancel avec montants, policy, cancelledBy,
stripeRefundId, stripeRefundError.
- Email best-effort : sendRentalCancelled à tenant + provider (sauf si
provider est le canceller).
src/components/CancelRentalButton.tsx : composant client confirm dialog
inline avec textarea motif (max 500 chars). Branché sur :
- /mes-locations : « Annuler ma location » sur résa PENDING/CONFIRMED
- BookingDecision (utilisé par /espace-prestataire/reservations ET
/espace-ce/materiel/reservations) : remplace l'ancienne mini-confirm
qui flippait juste le status, désormais via la vraie API refund
sendRentalCancelled email : adapté selon cancelledBy ("Vous avez annulé"
/ " a annulé" / "L'équipe Karbé a annulé").
tests/lib/rental-refund.test.ts : 8 cas (FULL @ 10+ et 7j, PARTIAL_50,
DEPOSIT_ONLY < 24h et passé, arrondi centime, zéro caution, policyLabel).
Total projet : 70/70 ✓.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
src/app/api/rentals/[id]/cancel/route.ts | 193 ++++++++++++++++++
.../_components/BookingDecision.tsx | 37 +---
src/app/mes-locations/page.tsx | 7 +
src/components/CancelRentalButton.tsx | 100 +++++++++
src/lib/email.ts | 32 +++
src/lib/rental-refund.ts | 73 +++++++
tests/lib/rental-refund.test.ts | 95 +++++++++
7 files changed, 503 insertions(+), 34 deletions(-)
create mode 100644 src/app/api/rentals/[id]/cancel/route.ts
create mode 100644 src/components/CancelRentalButton.tsx
create mode 100644 src/lib/rental-refund.ts
create mode 100644 tests/lib/rental-refund.test.ts
diff --git a/src/app/api/rentals/[id]/cancel/route.ts b/src/app/api/rentals/[id]/cancel/route.ts
new file mode 100644
index 0000000..49aa9ec
--- /dev/null
+++ b/src/app/api/rentals/[id]/cancel/route.ts
@@ -0,0 +1,193 @@
+import { NextResponse } from "next/server";
+
+import { auth } from "@/auth";
+import {
+ PaymentStatus,
+ RentalBookingStatus,
+ UserRole,
+} from "@/generated/prisma/enums";
+import { recordAudit } from "@/lib/admin/audit";
+import { canManageRentalProvider } from "@/lib/rental-access";
+import { sendRentalCancelled } from "@/lib/email";
+import { isStripeConfigured, getStripeClient } from "@/lib/stripe";
+import { prisma } from "@/lib/prisma";
+import { computeRentalRefund } from "@/lib/rental-refund";
+
+export const runtime = "nodejs";
+
+const CANCELLABLE_STATUSES: RentalBookingStatus[] = [
+ RentalBookingStatus.PENDING,
+ RentalBookingStatus.CONFIRMED,
+];
+
+type Body = { reason?: string };
+
+export async function POST(
+ req: Request,
+ { params }: { params: Promise<{ id: string }> },
+) {
+ const session = await auth();
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: "Non authentifié." }, { status: 401 });
+ }
+ const { id } = await params;
+ const body: Body = await req.json().catch(() => ({}));
+ const reason = body.reason?.toString().trim().slice(0, 500) ?? null;
+
+ const rb = await prisma.rentalBooking.findUnique({
+ where: { id },
+ include: {
+ provider: { select: { id: true, name: true, contactEmail: true, organizationId: true } },
+ tenant: { select: { id: true, email: true, firstName: true } },
+ lines: { select: { qty: true, item: { select: { name: true } } } },
+ },
+ });
+ if (!rb) {
+ return NextResponse.json({ error: "Réservation introuvable." }, { status: 404 });
+ }
+
+ // Détecte qui annule pour l'auth + l'email :
+ // - tenant de la booking
+ // - provider's manager (RENTAL_PROVIDER nominal ou CE_MANAGER de l'org du provider)
+ // - admin
+ const role = session.user.role;
+ const isTenant = rb.tenantId === session.user.id;
+ const isAdmin = role === UserRole.ADMIN;
+ const canManage = await canManageRentalProvider(
+ session.user.id,
+ role,
+ rb.providerId,
+ session.user.organizationId,
+ );
+ const cancelledBy: "tenant" | "provider" | "admin" = isAdmin
+ ? "admin"
+ : canManage
+ ? "provider"
+ : "tenant";
+
+ if (!isAdmin && !canManage && !isTenant) {
+ return NextResponse.json({ error: "Accès refusé." }, { status: 403 });
+ }
+
+ if (!CANCELLABLE_STATUSES.includes(rb.status)) {
+ return NextResponse.json(
+ { error: `Impossible d'annuler une réservation en statut ${rb.status}.` },
+ { status: 409 },
+ );
+ }
+
+ // Calcule le remboursement selon la politique
+ const refund = computeRentalRefund({
+ startDate: rb.startDate,
+ itemsTotal: rb.itemsTotal,
+ depositTotal: rb.depositTotal,
+ });
+
+ // Stripe refund best-effort si paiement déjà SUCCEEDED + session Stripe existante
+ let stripeRefundId: string | null = null;
+ let stripeRefundError: string | null = null;
+ if (
+ rb.paymentStatus === PaymentStatus.SUCCEEDED &&
+ rb.stripeSessionId &&
+ isStripeConfigured() &&
+ refund.totalRefund.gt(0)
+ ) {
+ try {
+ const stripe = getStripeClient();
+ const sess = await stripe.checkout.sessions.retrieve(rb.stripeSessionId, {
+ expand: ["payment_intent"],
+ });
+ const piId =
+ typeof sess.payment_intent === "string"
+ ? sess.payment_intent
+ : sess.payment_intent?.id;
+ if (piId) {
+ const stripeRefund = await stripe.refunds.create({
+ payment_intent: piId,
+ amount: Math.round(Number(refund.totalRefund) * 100),
+ reason: "requested_by_customer",
+ });
+ stripeRefundId = stripeRefund.id;
+ }
+ } catch (e) {
+ stripeRefundError = e instanceof Error ? e.message : String(e);
+ console.error("[rental.cancel] Stripe refund failed:", stripeRefundError);
+ }
+ }
+
+ // Transaction : update booking + delete availability blocks
+ await prisma.$transaction([
+ prisma.rentalBooking.update({
+ where: { id },
+ data: {
+ status: RentalBookingStatus.CANCELLED,
+ paymentStatus:
+ rb.paymentStatus === PaymentStatus.SUCCEEDED
+ ? PaymentStatus.REFUNDED
+ : PaymentStatus.FAILED,
+ },
+ }),
+ prisma.rentalItemAvailability.deleteMany({
+ where: { rentalBookingId: id },
+ }),
+ ]);
+
+ await recordAudit({
+ scope: "rental",
+ event: "rental.cancel",
+ target: id,
+ actorEmail: session.user.email ?? null,
+ details: {
+ cancelledBy,
+ reason,
+ policy: refund.policy,
+ itemsRefund: refund.itemsRefund.toString(),
+ depositRefund: refund.depositRefund.toString(),
+ totalRefund: refund.totalRefund.toString(),
+ stripeRefundId,
+ stripeRefundError,
+ },
+ });
+
+ // Email best-effort : tenant + provider
+ try {
+ await sendRentalCancelled(
+ rb.tenant.email,
+ rb.tenant.firstName,
+ rb.id,
+ rb.provider.name,
+ refund.totalRefund.toString(),
+ rb.currency,
+ refund.policyLabel,
+ cancelledBy,
+ );
+ if (rb.provider.contactEmail && cancelledBy !== "provider") {
+ await sendRentalCancelled(
+ rb.provider.contactEmail,
+ rb.provider.name,
+ rb.id,
+ rb.provider.name,
+ refund.totalRefund.toString(),
+ rb.currency,
+ refund.policyLabel,
+ cancelledBy,
+ );
+ }
+ } catch (e) {
+ console.error("[rental.cancel] email send failed:", e instanceof Error ? e.message : e);
+ }
+
+ return NextResponse.json({
+ ok: true,
+ rentalBookingId: id,
+ refund: {
+ itemsRefund: refund.itemsRefund.toNumber(),
+ depositRefund: refund.depositRefund.toNumber(),
+ totalRefund: refund.totalRefund.toNumber(),
+ policy: refund.policy,
+ policyLabel: refund.policyLabel,
+ },
+ stripeRefundId,
+ stripeRefundError,
+ });
+}
diff --git a/src/app/espace-prestataire/reservations/_components/BookingDecision.tsx b/src/app/espace-prestataire/reservations/_components/BookingDecision.tsx
index 2d6fa77..b38622f 100644
--- a/src/app/espace-prestataire/reservations/_components/BookingDecision.tsx
+++ b/src/app/espace-prestataire/reservations/_components/BookingDecision.tsx
@@ -3,6 +3,7 @@
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
+import { CancelRentalButton } from "@/components/CancelRentalButton";
import { RentalBookingStatus } from "@/generated/prisma/enums";
import { updateBookingStatusAction } from "../../actions";
@@ -14,14 +15,11 @@ export function BookingDecision({ bookingId, status }: { bookingId: string; stat
const router = useRouter();
const [pending, startTransition] = useTransition();
const [error, setError] = useState(null);
- const [confirmCancel, setConfirmCancel] = useState(false);
-
function set(next: string) {
setError(null);
startTransition(async () => {
const res = await updateBookingStatusAction(bookingId, next);
if (res && res.ok === false) setError(res.error);
- setConfirmCancel(false);
router.refresh();
});
}
@@ -58,37 +56,8 @@ export function BookingDecision({ bookingId, status }: { bookingId: string; stat
Marquer retourné
) : null}
- {status !== RentalBookingStatus.CANCELLED && status !== RentalBookingStatus.RETURNED ? (
- confirmCancel ? (
-
- Annuler ?
- set(RentalBookingStatus.CANCELLED)}
- disabled={pending}
- className="rounded bg-rose-700 px-2 py-1 text-[11px] font-semibold text-white hover:bg-rose-800 disabled:opacity-50"
- >
- Oui
-
- setConfirmCancel(false)}
- disabled={pending}
- className="text-[11px] text-zinc-500 hover:text-zinc-900"
- >
- Non
-
-
- ) : (
- setConfirmCancel(true)}
- disabled={pending}
- className={`${btnBase} border border-rose-300 bg-white text-rose-700 hover:bg-rose-50`}
- >
- Annuler
-
- )
+ {status === RentalBookingStatus.PENDING || status === RentalBookingStatus.CONFIRMED ? (
+
) : null}
{error ? {error} : null}
diff --git a/src/app/mes-locations/page.tsx b/src/app/mes-locations/page.tsx
index 5e3d737..53f7eb6 100644
--- a/src/app/mes-locations/page.tsx
+++ b/src/app/mes-locations/page.tsx
@@ -1,5 +1,6 @@
import Link from "next/link";
+import { CancelRentalButton } from "@/components/CancelRentalButton";
import { requireAuth } from "@/lib/authorization";
import { requirePluginOr404 } from "@/lib/plugins/guard";
import { prisma } from "@/lib/prisma";
@@ -134,6 +135,12 @@ export default async function MyRentalsPage({ searchParams }: { searchParams: Se
{rb.provider.contactEmail ? ✉ {rb.provider.contactEmail} : null}
) : null}
+
+ {(rb.status === "PENDING" || rb.status === "CONFIRMED") ? (
+
+
+
+ ) : null}
))}
diff --git a/src/components/CancelRentalButton.tsx b/src/components/CancelRentalButton.tsx
new file mode 100644
index 0000000..963f9d8
--- /dev/null
+++ b/src/components/CancelRentalButton.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+
+type Props = {
+ rentalBookingId: string;
+ /** Label adapté au contexte d'appel : « Annuler ma location » côté tenant, etc. */
+ label?: string;
+ /** Affichage compact dans une grille d'actions (pas de margin auto). */
+ compact?: boolean;
+};
+
+export function CancelRentalButton({
+ rentalBookingId,
+ label = "Annuler",
+ compact = false,
+}: Props) {
+ const router = useRouter();
+ const [pending, startTransition] = useTransition();
+ const [error, setError] = useState(null);
+ const [confirmOpen, setConfirmOpen] = useState(false);
+ const [reason, setReason] = useState("");
+
+ function submit() {
+ setError(null);
+ startTransition(async () => {
+ const res = await fetch(`/api/rentals/${rentalBookingId}/cancel`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ reason }),
+ });
+ const json = await res.json().catch(() => ({}));
+ if (!res.ok) {
+ setError(json?.error || `Erreur ${res.status}`);
+ return;
+ }
+ setConfirmOpen(false);
+ router.refresh();
+ });
+ }
+
+ if (!confirmOpen) {
+ return (
+ setConfirmOpen(true)}
+ className={
+ "rounded-md border border-rose-200 px-3 py-1.5 text-sm text-rose-700 hover:bg-rose-50 " +
+ (compact ? "" : "")
+ }
+ >
+ {label}
+
+ );
+ }
+
+ return (
+
+
Confirmer l'annulation
+
+ Le remboursement est calculé selon la politique : 100 % si annulation à plus de 7 jours,
+ 50 % entre 1 et 7 jours, caution seulement à moins de 24h.
+
+
+ Motif (optionnel)
+
+ {error ?
{error}
: null}
+
+ {
+ setConfirmOpen(false);
+ setError(null);
+ }}
+ disabled={pending}
+ className="rounded-md border border-zinc-300 bg-white px-3 py-1 text-xs text-zinc-700 hover:bg-zinc-50"
+ >
+ Garder la résa
+
+
+ {pending ? "Annulation…" : "Confirmer"}
+
+
+
+ );
+}
diff --git a/src/lib/email.ts b/src/lib/email.ts
index 4305922..894bf1a 100644
--- a/src/lib/email.ts
+++ b/src/lib/email.ts
@@ -355,6 +355,38 @@ export async function sendRentalRequestedProvider(
});
}
+export async function sendRentalCancelled(
+ to: string,
+ firstName: string,
+ rentalBookingId: string,
+ providerName: string,
+ refundAmount: string,
+ currency: string,
+ policyLabel: string,
+ cancelledBy: "tenant" | "provider" | "admin",
+): Promise {
+ const actor =
+ cancelledBy === "tenant"
+ ? "Vous avez annulé"
+ : cancelledBy === "provider"
+ ? `${providerName} a annulé`
+ : "L'équipe Karbé a annulé";
+ await sendEmail({
+ to,
+ subject: `Location annulée — ${providerName}`,
+ html: wrap(
+ "Location annulée",
+ `Bonjour ${firstName},
+ ${actor} votre location auprès de ${providerName} .
+ Politique appliquée : ${policyLabel}
+ Remboursement : ${Number(refundAmount).toFixed(2)} ${currency}
+ Si un paiement avait été reçu, le remboursement est traité par Stripe sous 3-5 jours ouvrés.
+ Mes locations
+ Référence : ${rentalBookingId}
`,
+ ),
+ });
+}
+
export async function sendRentalConfirmed(
to: string,
firstName: string,
diff --git a/src/lib/rental-refund.ts b/src/lib/rental-refund.ts
new file mode 100644
index 0000000..4fd77d3
--- /dev/null
+++ b/src/lib/rental-refund.ts
@@ -0,0 +1,73 @@
+import "server-only";
+
+import { Prisma } from "@/generated/prisma/client";
+
+const DAY_MS = 24 * 60 * 60 * 1000;
+
+export type RefundPolicy = "FULL" | "PARTIAL_50" | "DEPOSIT_ONLY";
+
+export type RefundCalculation = {
+ /** Montant remboursé sur la location (hors caution). */
+ itemsRefund: Prisma.Decimal;
+ /** Montant remboursé sur la caution (rendue intégralement tant que pas HANDED_OVER). */
+ depositRefund: Prisma.Decimal;
+ /** Total remboursé (itemsRefund + depositRefund). */
+ totalRefund: Prisma.Decimal;
+ policy: RefundPolicy;
+ /** Description lisible de la politique appliquée, à inclure dans l'email. */
+ policyLabel: string;
+};
+
+/**
+ * Politique de remboursement v1 (simple, paramétrable plus tard) :
+ * - Annulation > 7 jours avant le début → remboursement intégral (FULL)
+ * - Annulation entre 1 et 7 jours avant le début → remboursement 50% items + caution intégrale (PARTIAL_50)
+ * - Annulation < 24h avant le début → seulement la caution est rendue (DEPOSIT_ONLY)
+ *
+ * La caution est TOUJOURS rendue tant que le matériel n'a pas été remis
+ * (`HANDED_OVER`), puisqu'elle ne couvre que les dégâts pendant l'usage.
+ */
+export function computeRentalRefund(opts: {
+ startDate: Date;
+ itemsTotal: string | number | Prisma.Decimal;
+ depositTotal: string | number | Prisma.Decimal;
+ now?: Date;
+}): RefundCalculation {
+ const now = opts.now ?? new Date();
+ const msUntilStart = opts.startDate.getTime() - now.getTime();
+ const daysUntilStart = msUntilStart / DAY_MS;
+
+ const itemsDecimal = new Prisma.Decimal(opts.itemsTotal.toString());
+ const depositDecimal = new Prisma.Decimal(opts.depositTotal.toString());
+
+ let policy: RefundPolicy;
+ let itemsRefund: Prisma.Decimal;
+ let policyLabel: string;
+
+ if (daysUntilStart >= 7) {
+ policy = "FULL";
+ itemsRefund = itemsDecimal;
+ policyLabel = "Annulation > 7 jours : remboursement intégral";
+ } else if (daysUntilStart >= 1) {
+ policy = "PARTIAL_50";
+ itemsRefund = itemsDecimal.mul("0.5").toDecimalPlaces(2);
+ policyLabel = "Annulation entre 1 et 7 jours : 50 % du montant location";
+ } else {
+ policy = "DEPOSIT_ONLY";
+ itemsRefund = new Prisma.Decimal(0);
+ policyLabel = "Annulation tardive : caution rendue, location non remboursée";
+ }
+
+ // La caution est toujours rendue tant que pas HANDED_OVER (vérifié côté action
+ // avant d'appeler ce helper).
+ const depositRefund = depositDecimal;
+ const totalRefund = itemsRefund.add(depositRefund).toDecimalPlaces(2);
+
+ return {
+ itemsRefund: itemsRefund.toDecimalPlaces(2),
+ depositRefund: depositRefund.toDecimalPlaces(2),
+ totalRefund,
+ policy,
+ policyLabel,
+ };
+}
diff --git a/tests/lib/rental-refund.test.ts b/tests/lib/rental-refund.test.ts
new file mode 100644
index 0000000..05260a6
--- /dev/null
+++ b/tests/lib/rental-refund.test.ts
@@ -0,0 +1,95 @@
+import { describe, it, expect, vi } from "vitest";
+
+// `server-only` n'est pas résolu sous vitest — stub minimal.
+vi.mock("server-only", () => ({}));
+
+const { computeRentalRefund } = await import("@/lib/rental-refund");
+
+function daysFromNow(d: number): Date {
+ return new Date(Date.now() + d * 24 * 60 * 60 * 1000);
+}
+
+describe("computeRentalRefund", () => {
+ it("FULL refund quand annulation à 10+ jours du début", () => {
+ const r = computeRentalRefund({
+ startDate: daysFromNow(10),
+ itemsTotal: 100,
+ depositTotal: 50,
+ });
+ expect(r.policy).toBe("FULL");
+ expect(r.itemsRefund.toNumber()).toBe(100);
+ expect(r.depositRefund.toNumber()).toBe(50);
+ expect(r.totalRefund.toNumber()).toBe(150);
+ });
+
+ it("FULL refund pile à 7 jours du début", () => {
+ const r = computeRentalRefund({
+ startDate: daysFromNow(7),
+ itemsTotal: 200,
+ depositTotal: 100,
+ });
+ expect(r.policy).toBe("FULL");
+ expect(r.totalRefund.toNumber()).toBe(300);
+ });
+
+ it("PARTIAL_50 quand annulation entre 1 et 7 jours", () => {
+ const r = computeRentalRefund({
+ startDate: daysFromNow(3),
+ itemsTotal: 200,
+ depositTotal: 100,
+ });
+ expect(r.policy).toBe("PARTIAL_50");
+ expect(r.itemsRefund.toNumber()).toBe(100);
+ expect(r.depositRefund.toNumber()).toBe(100);
+ expect(r.totalRefund.toNumber()).toBe(200);
+ });
+
+ it("DEPOSIT_ONLY quand annulation < 24h", () => {
+ const r = computeRentalRefund({
+ startDate: daysFromNow(0.5),
+ itemsTotal: 200,
+ depositTotal: 100,
+ });
+ expect(r.policy).toBe("DEPOSIT_ONLY");
+ expect(r.itemsRefund.toNumber()).toBe(0);
+ expect(r.depositRefund.toNumber()).toBe(100);
+ expect(r.totalRefund.toNumber()).toBe(100);
+ });
+
+ it("DEPOSIT_ONLY quand startDate déjà passée", () => {
+ const r = computeRentalRefund({
+ startDate: daysFromNow(-1),
+ itemsTotal: 200,
+ depositTotal: 100,
+ });
+ expect(r.policy).toBe("DEPOSIT_ONLY");
+ expect(r.itemsRefund.toNumber()).toBe(0);
+ expect(r.depositRefund.toNumber()).toBe(100);
+ });
+
+ it("arrondit au centime pour PARTIAL_50", () => {
+ const r = computeRentalRefund({
+ startDate: daysFromNow(3),
+ itemsTotal: 33.33,
+ depositTotal: 0,
+ });
+ expect(r.itemsRefund.toNumber()).toBe(16.67); // 33.33 / 2 = 16.665 → arrondi 16.67
+ expect(r.totalRefund.toNumber()).toBe(16.67);
+ });
+
+ it("Zéro caution → totalRefund = itemsRefund", () => {
+ const r = computeRentalRefund({
+ startDate: daysFromNow(10),
+ itemsTotal: 50,
+ depositTotal: 0,
+ });
+ expect(r.depositRefund.toNumber()).toBe(0);
+ expect(r.totalRefund.toNumber()).toBe(50);
+ });
+
+ it("policyLabel contient un texte lisible pour chaque branche", () => {
+ expect(computeRentalRefund({ startDate: daysFromNow(10), itemsTotal: 100, depositTotal: 0 }).policyLabel).toContain("intégral");
+ expect(computeRentalRefund({ startDate: daysFromNow(3), itemsTotal: 100, depositTotal: 0 }).policyLabel).toContain("50");
+ expect(computeRentalRefund({ startDate: daysFromNow(0), itemsTotal: 100, depositTotal: 0 }).policyLabel).toContain("tardive");
+ });
+});