feat(rental): Sprint E — emails + plugin toggle + tests
Some checks failed
CI / test (pull_request) Failing after 1m10s
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>
This commit is contained in:
parent
0723e50189
commit
5607a51980
13 changed files with 313 additions and 8 deletions
|
|
@ -5,6 +5,11 @@ import { auth } from "@/auth";
|
|||
import { PaymentStatus, RentalBookingStatus } from "@/generated/prisma/enums";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { recordAudit } from "@/lib/admin/audit";
|
||||
import {
|
||||
sendRentalRequestedProvider,
|
||||
sendRentalRequestedTenant,
|
||||
} from "@/lib/email";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { CART_COOKIE, EMPTY_CART, diffDays, parseCart } from "@/lib/rental-cart";
|
||||
import {
|
||||
|
|
@ -28,6 +33,9 @@ function parseDateOnly(s: string): Date {
|
|||
}
|
||||
|
||||
export async function POST() {
|
||||
if (!(await isPluginEnabled("gear-rental"))) {
|
||||
return NextResponse.json({ error: "Service de location indisponible." }, { status: 404 });
|
||||
}
|
||||
const session = await auth();
|
||||
if (!session?.user?.id || !session.user.email) {
|
||||
return NextResponse.json({ error: "Connectez-vous pour finaliser." }, { status: 401 });
|
||||
|
|
@ -241,6 +249,46 @@ export async function POST() {
|
|||
},
|
||||
});
|
||||
|
||||
// Emails best-effort : 1 mail au locataire (récap par prestataire) + 1 mail
|
||||
// à chaque prestataire (sa demande). En cas d'échec d'envoi, on ne bloque pas.
|
||||
try {
|
||||
const fullBookings = await prisma.rentalBooking.findMany({
|
||||
where: { id: { in: rentalBookingIds } },
|
||||
include: {
|
||||
provider: { select: { name: true, contactEmail: true } },
|
||||
lines: { include: { item: { select: { name: true } } } },
|
||||
},
|
||||
});
|
||||
const tenantName = session.user.name ?? session.user.email!;
|
||||
for (const rb of fullBookings) {
|
||||
const lineSummary = rb.lines.map((l) => ({ qty: l.qty, itemName: l.item.name }));
|
||||
await sendRentalRequestedTenant(
|
||||
session.user.email!,
|
||||
tenantName,
|
||||
rb.id,
|
||||
rb.provider.name,
|
||||
rb.startDate,
|
||||
rb.endDate,
|
||||
rb.amount.toString(),
|
||||
rb.currency,
|
||||
lineSummary,
|
||||
);
|
||||
if (rb.provider.contactEmail) {
|
||||
await sendRentalRequestedProvider(
|
||||
rb.provider.contactEmail,
|
||||
rb.provider.name,
|
||||
rb.id,
|
||||
tenantName,
|
||||
rb.startDate,
|
||||
rb.endDate,
|
||||
lineSummary,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[rental.checkout] email send failed:", e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
// Vide le panier
|
||||
jar.set(CART_COOKIE, JSON.stringify(EMPTY_CART), {
|
||||
httpOnly: false,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
SubscriptionStatus,
|
||||
} from "@/generated/prisma/enums";
|
||||
import { refreshCarbetLastBookedAt } from "@/lib/carbet-last-booked";
|
||||
import { sendRentalConfirmed } from "@/lib/email";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { fromStripeTimestamp, getStripeClient } from "@/lib/stripe";
|
||||
|
||||
|
|
@ -64,6 +65,28 @@ async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
|
|||
status: RentalBookingStatus.CONFIRMED,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const rentals = await prisma.rentalBooking.findMany({
|
||||
where: { id: { in: ids } },
|
||||
include: {
|
||||
provider: { select: { name: true } },
|
||||
tenant: { select: { email: true, firstName: true } },
|
||||
},
|
||||
});
|
||||
for (const rb of rentals) {
|
||||
if (!rb.tenant.email) continue;
|
||||
await sendRentalConfirmed(
|
||||
rb.tenant.email,
|
||||
rb.tenant.firstName ?? rb.tenant.email,
|
||||
rb.id,
|
||||
rb.provider.name,
|
||||
rb.startDate,
|
||||
rb.endDate,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[webhook.rental] email send failed:", e instanceof Error ? e.message : e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue