feat(carbet): track lastBookedAt on booking creation
This commit is contained in:
parent
5b6213e764
commit
08a5321cae
4 changed files with 720 additions and 6 deletions
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "Carbet"
|
||||
ADD COLUMN "lastBookedAt" TIMESTAMP(3);
|
||||
|
|
@ -1,8 +1,3 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Get a free hosted Postgres database in seconds: `npx create-db`
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
|
|
@ -12,4 +7,240 @@ datasource db {
|
|||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// Les modèles Karbé (carbets, réservations, utilisateurs…) seront ajoutés ici.
|
||||
enum UserRole {
|
||||
OWNER
|
||||
CE_MANAGER
|
||||
CE_MEMBER
|
||||
TOURIST
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum CarbetStatus {
|
||||
DRAFT
|
||||
PUBLISHED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum MediaType {
|
||||
PHOTO
|
||||
VIDEO
|
||||
}
|
||||
|
||||
enum AvailabilityScope {
|
||||
PUBLIC
|
||||
CE_ONLY
|
||||
}
|
||||
|
||||
enum AvailabilityBlockReason {
|
||||
NONE
|
||||
CE_BLOCKED
|
||||
WEEKEND_BLOCKED
|
||||
}
|
||||
|
||||
enum BookingStatus {
|
||||
PENDING
|
||||
CONFIRMED
|
||||
CANCELLED
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
PENDING
|
||||
AUTHORIZED
|
||||
SUCCEEDED
|
||||
FAILED
|
||||
REFUNDED
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
TRIAL
|
||||
ACTIVE
|
||||
PAST_DUE
|
||||
CANCELED
|
||||
}
|
||||
|
||||
model Organization {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String @unique
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
members User[]
|
||||
|
||||
@@index([name])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
passwordHash String
|
||||
firstName String
|
||||
lastName String
|
||||
phone String?
|
||||
role UserRole
|
||||
organizationId String?
|
||||
avatarUrl String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
|
||||
carbets Carbet[] @relation("CarbetOwner")
|
||||
bookings Booking[] @relation("BookingTenant")
|
||||
reviews Review[] @relation("ReviewAuthor")
|
||||
subscriptions Subscription[]
|
||||
|
||||
@@index([organizationId])
|
||||
@@index([role])
|
||||
}
|
||||
|
||||
model Carbet {
|
||||
id String @id @default(cuid())
|
||||
ownerId String
|
||||
title String
|
||||
slug String @unique
|
||||
description String
|
||||
river String
|
||||
latitude Decimal @db.Decimal(9, 6)
|
||||
longitude Decimal @db.Decimal(9, 6)
|
||||
embarkPoint String
|
||||
pirogueDurationMin Int
|
||||
capacity Int
|
||||
status CarbetStatus @default(DRAFT)
|
||||
lastBookedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
owner User @relation("CarbetOwner", fields: [ownerId], references: [id], onDelete: Restrict)
|
||||
amenities CarbetAmenity[]
|
||||
media Media[]
|
||||
availabilities Availability[]
|
||||
bookings Booking[]
|
||||
reviews Review[]
|
||||
subscriptions Subscription[]
|
||||
|
||||
@@index([ownerId])
|
||||
@@index([status])
|
||||
@@index([river])
|
||||
}
|
||||
|
||||
model Amenity {
|
||||
id String @id @default(cuid())
|
||||
key String @unique
|
||||
label String
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
carbets CarbetAmenity[]
|
||||
}
|
||||
|
||||
model CarbetAmenity {
|
||||
carbetId String
|
||||
amenityId String
|
||||
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade)
|
||||
amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([carbetId, amenityId])
|
||||
@@index([amenityId])
|
||||
}
|
||||
|
||||
model Media {
|
||||
id String @id @default(cuid())
|
||||
carbetId String
|
||||
type MediaType
|
||||
s3Key String
|
||||
s3Url String
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([carbetId, sortOrder])
|
||||
}
|
||||
|
||||
model Availability {
|
||||
id String @id @default(cuid())
|
||||
carbetId String
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
scope AvailabilityScope @default(PUBLIC)
|
||||
blockReason AvailabilityBlockReason @default(NONE)
|
||||
isAvailable Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([carbetId])
|
||||
@@index([scope, blockReason])
|
||||
@@index([startDate, endDate])
|
||||
}
|
||||
|
||||
model Booking {
|
||||
id String @id @default(cuid())
|
||||
carbetId String
|
||||
tenantId String
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
guestCount Int
|
||||
status BookingStatus @default(PENDING)
|
||||
amount Decimal @db.Decimal(10, 2)
|
||||
currency String @default("EUR")
|
||||
paymentStatus PaymentStatus @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Restrict)
|
||||
tenant User @relation("BookingTenant", fields: [tenantId], references: [id], onDelete: Restrict)
|
||||
|
||||
review Review?
|
||||
|
||||
@@index([carbetId])
|
||||
@@index([tenantId])
|
||||
@@index([status, paymentStatus])
|
||||
@@index([startDate, endDate])
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(cuid())
|
||||
ownerId String
|
||||
carbetId String
|
||||
provider String
|
||||
providerSubId String? @unique
|
||||
status SubscriptionStatus @default(TRIAL)
|
||||
startedAt DateTime @default(now())
|
||||
renewedAt DateTime?
|
||||
canceledAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Restrict)
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([ownerId])
|
||||
@@index([carbetId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model Review {
|
||||
id String @id @default(cuid())
|
||||
bookingId String @unique
|
||||
carbetId String
|
||||
authorId String
|
||||
rating Int
|
||||
comment String?
|
||||
hostResponse String?
|
||||
hostRespondedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
||||
carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Restrict)
|
||||
author User @relation("ReviewAuthor", fields: [authorId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@index([carbetId])
|
||||
@@index([authorId])
|
||||
}
|
||||
|
|
|
|||
220
src/app/api/bookings/route.ts
Normal file
220
src/app/api/bookings/route.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import {
|
||||
AvailabilityScope,
|
||||
BookingStatus,
|
||||
CarbetStatus,
|
||||
UserRole,
|
||||
} from "@/generated/prisma/enums";
|
||||
import {
|
||||
enumerateUtcDays,
|
||||
hasOverlap,
|
||||
isPublicAllowedByDefaultPolicy,
|
||||
isCeUserRole,
|
||||
normalizeUtcDayStart,
|
||||
parseIsoDate,
|
||||
} from "@/lib/booking";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
type CreateBookingBody = {
|
||||
carbetId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
guestCount?: number;
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Non authentifié." }, { status: 401 });
|
||||
}
|
||||
|
||||
let body: CreateBookingBody;
|
||||
try {
|
||||
body = (await request.json()) as CreateBookingBody;
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Corps JSON invalide." }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!body.carbetId) {
|
||||
return NextResponse.json({ error: "carbetId requis." }, { status: 400 });
|
||||
}
|
||||
|
||||
const startDateRaw = parseIsoDate(body.startDate);
|
||||
const endDateRaw = parseIsoDate(body.endDate);
|
||||
const guestCount = Number(body.guestCount);
|
||||
|
||||
if (!startDateRaw || !endDateRaw) {
|
||||
return NextResponse.json(
|
||||
{ error: "startDate et endDate valides sont requis." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const startDate = normalizeUtcDayStart(startDateRaw);
|
||||
const endDate = normalizeUtcDayStart(endDateRaw);
|
||||
|
||||
if (endDate <= startDate) {
|
||||
return NextResponse.json(
|
||||
{ error: "La date de fin doit être après la date de début." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(guestCount) || guestCount <= 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "guestCount doit être un entier strictement positif." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const carbet = await prisma.carbet.findUnique({
|
||||
where: { id: body.carbetId },
|
||||
select: {
|
||||
id: true,
|
||||
ownerId: true,
|
||||
capacity: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!carbet) {
|
||||
return NextResponse.json({ error: "Carbet introuvable." }, { status: 404 });
|
||||
}
|
||||
|
||||
const isManager =
|
||||
session.user.role === UserRole.ADMIN || session.user.id === carbet.ownerId;
|
||||
|
||||
if (!isManager && carbet.status !== CarbetStatus.PUBLISHED) {
|
||||
return NextResponse.json({ error: "Carbet indisponible." }, { status: 404 });
|
||||
}
|
||||
|
||||
if (guestCount > carbet.capacity) {
|
||||
return NextResponse.json(
|
||||
{ error: `Capacité max dépassée (${carbet.capacity}).` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const [overlappingBookings, availabilities] = await Promise.all([
|
||||
prisma.booking.findMany({
|
||||
where: {
|
||||
carbetId: carbet.id,
|
||||
status: { in: [BookingStatus.PENDING, BookingStatus.CONFIRMED] },
|
||||
startDate: { lt: endDate },
|
||||
endDate: { gt: startDate },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
},
|
||||
}),
|
||||
prisma.availability.findMany({
|
||||
where: {
|
||||
carbetId: carbet.id,
|
||||
startDate: { lt: endDate },
|
||||
endDate: { gt: startDate },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
isAvailable: true,
|
||||
scope: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (overlappingBookings.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "Ce créneau est déjà réservé." },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const ceAccess = isCeUserRole(session.user.role);
|
||||
const days = enumerateUtcDays(startDate, endDate);
|
||||
|
||||
for (const day of days) {
|
||||
const nextDay = new Date(day);
|
||||
nextDay.setUTCDate(nextDay.getUTCDate() + 1);
|
||||
|
||||
const coveredSlots = availabilities.filter((a) =>
|
||||
hasOverlap(day, nextDay, a.startDate, a.endDate),
|
||||
);
|
||||
|
||||
if (coveredSlots.length === 0) {
|
||||
const defaultAllowed = isManager || ceAccess || isPublicAllowedByDefaultPolicy(day);
|
||||
if (defaultAllowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Créneau non réservable le ${day.toISOString().slice(0, 10)} (week-end réservé CE).`,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const allowedSlot = coveredSlots.find((slot) => {
|
||||
if (!slot.isAvailable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slot.scope === AvailabilityScope.CE_ONLY && !ceAccess && !isManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!allowedSlot) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Créneau non réservable le ${day.toISOString().slice(0, 10)} (restriction CE ou blocage).`,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const booking = await prisma.$transaction(async (tx) => {
|
||||
const created = await tx.booking.create({
|
||||
data: {
|
||||
carbetId: carbet.id,
|
||||
tenantId: session.user.id,
|
||||
startDate,
|
||||
endDate,
|
||||
guestCount,
|
||||
status: BookingStatus.PENDING,
|
||||
amount: 0,
|
||||
currency: "EUR",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
carbetId: true,
|
||||
tenantId: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
guestCount: true,
|
||||
status: true,
|
||||
paymentStatus: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.carbet.update({
|
||||
where: { id: carbet.id },
|
||||
data: { lastBookedAt: created.createdAt },
|
||||
});
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
return NextResponse.json({ booking }, { status: 201 });
|
||||
}
|
||||
261
src/app/api/stripe/checkout/booking/route.ts
Normal file
261
src/app/api/stripe/checkout/booking/route.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import {
|
||||
AvailabilityScope,
|
||||
BookingStatus,
|
||||
CarbetStatus,
|
||||
PaymentStatus,
|
||||
UserRole,
|
||||
} from "@/generated/prisma/enums";
|
||||
import {
|
||||
enumerateUtcDays,
|
||||
hasOverlap,
|
||||
isCeUserRole,
|
||||
isPublicAllowedByDefaultPolicy,
|
||||
normalizeUtcDayStart,
|
||||
parseIsoDate,
|
||||
} from "@/lib/booking";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getStripeClient, toStripeAmountCents } from "@/lib/stripe";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
type BookingCheckoutBody = {
|
||||
carbetId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
guestCount?: number;
|
||||
amount?: number;
|
||||
currency?: string;
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Non authentifié." }, { status: 401 });
|
||||
}
|
||||
|
||||
let body: BookingCheckoutBody;
|
||||
try {
|
||||
body = (await request.json()) as BookingCheckoutBody;
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Corps JSON invalide." }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!body.carbetId) {
|
||||
return NextResponse.json({ error: "carbetId requis." }, { status: 400 });
|
||||
}
|
||||
|
||||
const startDateRaw = parseIsoDate(body.startDate);
|
||||
const endDateRaw = parseIsoDate(body.endDate);
|
||||
const guestCount = Number(body.guestCount);
|
||||
const amount = Number(body.amount);
|
||||
const currency = (body.currency ?? "EUR").toLowerCase();
|
||||
|
||||
if (!startDateRaw || !endDateRaw) {
|
||||
return NextResponse.json(
|
||||
{ error: "startDate et endDate valides sont requis." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const startDate = normalizeUtcDayStart(startDateRaw);
|
||||
const endDate = normalizeUtcDayStart(endDateRaw);
|
||||
|
||||
if (endDate <= startDate) {
|
||||
return NextResponse.json(
|
||||
{ error: "La date de fin doit être après la date de début." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(guestCount) || guestCount <= 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "guestCount doit être un entier strictement positif." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
return NextResponse.json({ error: "amount doit être > 0." }, { status: 400 });
|
||||
}
|
||||
|
||||
const carbet = await prisma.carbet.findUnique({
|
||||
where: { id: body.carbetId },
|
||||
select: {
|
||||
id: true,
|
||||
ownerId: true,
|
||||
title: true,
|
||||
capacity: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!carbet) {
|
||||
return NextResponse.json({ error: "Carbet introuvable." }, { status: 404 });
|
||||
}
|
||||
|
||||
const isManager =
|
||||
session.user.role === UserRole.ADMIN || session.user.id === carbet.ownerId;
|
||||
|
||||
if (!isManager && carbet.status !== CarbetStatus.PUBLISHED) {
|
||||
return NextResponse.json({ error: "Carbet indisponible." }, { status: 404 });
|
||||
}
|
||||
|
||||
if (guestCount > carbet.capacity) {
|
||||
return NextResponse.json(
|
||||
{ error: `Capacité max dépassée (${carbet.capacity}).` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const [overlappingBookings, availabilities] = await Promise.all([
|
||||
prisma.booking.findMany({
|
||||
where: {
|
||||
carbetId: carbet.id,
|
||||
status: { in: [BookingStatus.PENDING, BookingStatus.CONFIRMED] },
|
||||
startDate: { lt: endDate },
|
||||
endDate: { gt: startDate },
|
||||
},
|
||||
select: { id: true, startDate: true, endDate: true },
|
||||
}),
|
||||
prisma.availability.findMany({
|
||||
where: {
|
||||
carbetId: carbet.id,
|
||||
startDate: { lt: endDate },
|
||||
endDate: { gt: startDate },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
isAvailable: true,
|
||||
scope: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (overlappingBookings.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "Ce créneau est déjà réservé." },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const ceAccess = isCeUserRole(session.user.role);
|
||||
const days = enumerateUtcDays(startDate, endDate);
|
||||
|
||||
for (const day of days) {
|
||||
const nextDay = new Date(day);
|
||||
nextDay.setUTCDate(nextDay.getUTCDate() + 1);
|
||||
|
||||
const coveredSlots = availabilities.filter((a) =>
|
||||
hasOverlap(day, nextDay, a.startDate, a.endDate),
|
||||
);
|
||||
|
||||
if (coveredSlots.length === 0) {
|
||||
const defaultAllowed = isManager || ceAccess || isPublicAllowedByDefaultPolicy(day);
|
||||
if (defaultAllowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Créneau non réservable le ${day.toISOString().slice(0, 10)} (week-end réservé CE).`,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const allowedSlot = coveredSlots.find((slot) => {
|
||||
if (!slot.isAvailable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slot.scope === AvailabilityScope.CE_ONLY && !ceAccess && !isManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!allowedSlot) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Créneau non réservable le ${day.toISOString().slice(0, 10)} (restriction CE ou blocage).`,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const booking = await prisma.$transaction(async (tx) => {
|
||||
const created = await tx.booking.create({
|
||||
data: {
|
||||
carbetId: carbet.id,
|
||||
tenantId: session.user.id,
|
||||
startDate,
|
||||
endDate,
|
||||
guestCount,
|
||||
status: BookingStatus.PENDING,
|
||||
paymentStatus: PaymentStatus.PENDING,
|
||||
amount,
|
||||
currency: currency.toUpperCase(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.carbet.update({
|
||||
where: { id: carbet.id },
|
||||
data: { lastBookedAt: new Date() },
|
||||
});
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
const appUrl = process.env.APP_URL;
|
||||
if (!appUrl) {
|
||||
return NextResponse.json({ error: "APP_URL manquante." }, { status: 500 });
|
||||
}
|
||||
|
||||
const stripe = getStripeClient();
|
||||
const checkoutSession = await stripe.checkout.sessions.create({
|
||||
mode: "payment",
|
||||
success_url: `${appUrl}/reservations/${booking.id}?payment=success`,
|
||||
cancel_url: `${appUrl}/reservations/${booking.id}?payment=cancel`,
|
||||
customer_email: session.user.email ?? undefined,
|
||||
line_items: [
|
||||
{
|
||||
quantity: 1,
|
||||
price_data: {
|
||||
currency,
|
||||
unit_amount: toStripeAmountCents(amount),
|
||||
product_data: {
|
||||
name: `Réservation carbet: ${carbet.title}`,
|
||||
description: `${booking.startDate.toISOString().slice(0, 10)} au ${booking.endDate.toISOString().slice(0, 10)}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
bookingId: booking.id,
|
||||
type: "booking",
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
bookingId: booking.id,
|
||||
checkoutSessionId: checkoutSession.id,
|
||||
checkoutUrl: checkoutSession.url,
|
||||
},
|
||||
{ status: 201 },
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue