src/app/api/mobile/reservations/route.ts

route·app·28.2 KB · 703 lignes· Voir l'itinéraire
Annotation non disponible

Lance npm run annotate (nécessite ANTHROPIC_API_KEY dans .env.local) pour générer une annotation française par Claude Haiku 4.5.

2 exports

GETPOST

Code source· typescript· tronqué à 200 lignes sur 703

import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { getSessionFromRequest } from "@/lib/auth/session";
import type { Prisma } from "@prisma/client";
import { envoyerConfirmationReservation, envoyerNouvelleReservationAdmin } from "@/lib/email";
import { sendPushToTenantAdmin } from "@/lib/push";
import { initierPaiementRDV } from "@/lib/paydunya";
import { captureError } from "@/lib/sentry";

function dateLabelFR(d: Date): string {
  return new Date(d).toLocaleString("fr-FR", {
    weekday: "long", day: "numeric", month: "long",
    hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "UTC",
  });
}

const STATUTS_VALIDES = ["EN_ATTENTE", "CONFIRME", "ANNULE", "TERMINE", "NO_SHOW"] as const;
const MODES_PAIEMENT = ["CASH", "ORANGE_MONEY", "MOOV_MONEY", "VIREMENT", "PAYDUNYA"] as const;
const NOTE_CLIENT_MAX = 500;
const ICAL_BASE_URL = process.env.MAGIC_LINK_BASE_URL || "https://wari.pro";

// Pilier 2 V1 — durée réservation table par défaut 90 min (2 services possibles dans
// un créneau habituel 11h-13h ou 19h-21h). Personnalisable V1.5+.
const DUREE_TABLE_MS_DEFAULT = 90 * 60_000;

function parseDateInput(v: unknown): Date | null {
  if (typeof v !== "string") return null;
  const d = new Date(v);
  return Number.isNaN(d.getTime()) ? null : d;
}

function calculerAcompte(
  montantTotal: number | null,
  typeAcompte: "AUCUN" | "POURCENTAGE" | "MONTANT_FIXE",
  valeurAcompte: number | null
): number | null {
  if (typeAcompte === "AUCUN" || valeurAcompte == null || montantTotal == null) return null;
  if (typeAcompte === "POURCENTAGE") {
    const v = (montantTotal * valeurAcompte) / 100;
    return Math.round(v * 100) / 100;
  }
  return Math.min(valeurAcompte, montantTotal);
}

// ─── GET — Mes réservations (Bearer CLIENT) ────────────────────────────────────
export async function GET(req: NextRequest) {
  const session = await getSessionFromRequest(req);
  if (!session || session.role !== "CLIENT") {
    return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
  }

  const { searchParams } = req.nextUrl;
  const statut = searchParams.get("statut");
  const type = searchParams.get("type") as "PRESTATION" | "TABLE" | "EVENEMENT" | null;
  const enCours = searchParams.get("enCours") === "1";
  const page = Math.max(1, parseInt(searchParams.get("page") || "1"));
  const limit = Math.min(50, parseInt(searchParams.get("limit") || "20"));
  const skip = (page - 1) * limit;

  const where: Prisma.ReservationWhereInput = { clientId: session.userId };
  if (statut && (STATUTS_VALIDES as readonly string[]).includes(statut)) {
    where.statut = statut as (typeof STATUTS_VALIDES)[number];
  }
  if (type && ["PRESTATION", "TABLE", "EVENEMENT"].includes(type)) {
    where.type = type;
  }
  if (enCours) {
    where.statut = { in: ["EN_ATTENTE", "CONFIRME"] };
    where.dateDebut = { gte: new Date() };
  }

  try {
    const [reservations, total] = await Promise.all([
      prisma.reservation.findMany({
        where,
        orderBy: { dateDebut: enCours ? "asc" : "desc" },
        skip,
        take: limit,
        select: {
          id: true,
          icalToken: true,
          type: true,
          dateDebut: true,
          dateFin: true,
          nbCouverts: true,
          statut: true,
          montantTotal: true,
          montantAcompte: true,
          statutPaiement: true,
          modePaiement: true,
          noteClient: true,
          createdAt: true,
          tenantId: true,
          prestation: {
            select: {
              id: true,
              nom: true,
              duree: true,
              devise: true,
              medias: { orderBy: { ordre: "asc" }, take: 1, select: { url: true } },
              configReservation: { select: { delaiAnnulationHeures: true } },
            },
          },
          tableRestaurant: { select: { id: true, numero: true, capacite: true } },
          evenement: {
            select: {
              id: true,
              titre: true,
              lieu: true,
              prixPlace: true,
              devise: true,
              medias: { orderBy: { ordre: "asc" }, take: 1, select: { url: true } },
            },
          },
        },
      }),
      prisma.reservation.count({ where }),
    ]);

    const tenantIds = [...new Set(reservations.map((r) => r.tenantId))];
    const tenants = await prisma.tenant.findMany({
      where: { id: { in: tenantIds } },
      select: { id: true, subdomain: true, nom: true, logoUrl: true },
    });
    const tenantMap = new Map(tenants.map((t) => [t.id, t]));

    return NextResponse.json({
      reservations: reservations.map((r) => {
        const configReservation = r.prestation?.configReservation ?? null;
        const prestation = r.prestation
          ? {
              id: r.prestation.id,
              nom: r.prestation.nom,
              duree: r.prestation.duree,
              devise: r.prestation.devise,
              medias: r.prestation.medias,
            }
          : null;
        return {
          ...r,
          prestation,
          configReservation,
          dateDebut: r.dateDebut.toISOString(),
          dateFin: r.dateFin.toISOString(),
          createdAt: r.createdAt.toISOString(),
          tenant: tenantMap.get(r.tenantId) ?? null,
        };
      }),
      total,
      page,
      totalPages: Math.ceil(total / limit),
    });
  } catch (error) {
    captureError(error, {
      route: "/api/mobile/reservations",
      userId: session.userId,
      role: session.role,
    });
    console.error("mobile/reservations GET error:", error);
    return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
  }
}

// ─── POST — Créer une réservation (Bearer CLIENT) ──────────────────────────────
// Dispatch selon body.type (PRESTATION par défaut pour rétrocompat WP-221).
export async function POST(req: NextRequest) {
  const session = await getSessionFromRequest(req);
  if (!session || session.role !== "CLIENT") {
    return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
  }
  const body = await req.json().catch(() => ({}));
  const type = body?.type as "PRESTATION" | "TABLE" | "EVENEMENT" | undefined;

  if (type === "TABLE") return handlePostTable(body, session.userId);
  if (type === "EVENEMENT") return handlePostEvenement(body, session.userId);
  return handlePostPrestation(body, session.userId);
}

// ─── Handler PRESTATION (legacy WP-221 inchangé) ─────────────────────────────
async function handlePostPrestation(body: any, clientAccountId: string): Promise<NextResponse> {
  const { prestationId, tenantId, modePaiement } = body ?? {};
  const dateDebut = parseDateInput(body?.dateDebut);
  const dateFin = parseDateInput(body?.dateFin);

  if (typeof prestationId !== "string") {
    return NextResponse.json({ error: "prestationId requis" }, { status: 400 });
  }
  if (typeof tenantId !== "string") {
    return NextResponse.json({ error: "tenantId requis" }, { status: 400 });
  }
  if (!dateDebut || !dateFin) {
    return NextResponse.json({ error: "dateDebut/dateFin invalides" }, { status: 400 });
  }
  if (dateFin.getTime() <= dateDebut.getTime()) {
    return NextResponse.json({ error: "dateFin doit être > dateDebut" }, { status: 400 });
  }
  if (typeof modePaiement !== "string" || !(MODES_PAIEMENT as readonly string[]).includes(modePaiement)) {
    return NextResponse.json({ error: "modePaiement invalide" }, { status: 400 });
  }