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

route·app·14.3 KB · 409 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 409

import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { getSessionFromRequest } from "@/lib/auth/session";
import { sendPushToTenantAdmin, sendPushToClient } from "@/lib/push";
import { envoyerConfirmationCommande, envoyerNotificationAdmin } from "@/lib/email";
import {
  MODES_PAIEMENT_COMMANDE,
  type ModePaiementCommande,
} from "@/lib/ussd";
import { hookCommandeCompleteParrainage } from "@/lib/parrainage";
import { captureError } from "@/lib/sentry";

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 account = await prisma.clientAccount.findUnique({
    where: { id: session.userId },
    select: { email: true, phone: true },
  });
  if (!account) return NextResponse.json({ error: "Compte introuvable" }, { status: 404 });

  const identifiers = [account.email, account.phone].filter((v): v is string => !!v);
  if (identifiers.length === 0) {
    return NextResponse.json({ commandes: [], total: 0, page: 1, totalPages: 0 });
  }

  const clients = await prisma.client.findMany({
    where: { email: { in: identifiers } },
    select: { id: true },
  });
  const clientIds = clients.map((c) => c.id);
  if (clientIds.length === 0) {
    return NextResponse.json({ commandes: [], total: 0, page: 1, totalPages: 0 });
  }

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

  const where: any = { clientId: { in: clientIds } };
  if (enCours) {
    where.statut = { in: ["EN_ATTENTE", "CONFIRME", "PRET_RETRAIT", "EN_LIVRAISON"] };
  }

  const [commandes, total] = await Promise.all([
    prisma.commande.findMany({
      where,
      orderBy: { createdAt: "desc" },
      skip,
      take: limit,
      include: {
        lignes: {
          select: {
            quantite: true,
            produit: {
              select: {
                nom: true,
                medias: { take: 1, orderBy: { ordre: "asc" }, select: { url: true } },
              },
            },
          },
        },
      },
    }),
    prisma.commande.count({ where }),
  ]);

  const tenantIds = [...new Set(commandes.map((c) => c.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({
    commandes: commandes.map((c) => ({
      id: c.id,
      statut: c.statut,
      total: c.total,
      devise: c.devise,
      createdAt: c.createdAt.toISOString(),
      nbLignes: c.lignes.length,
      premiereLigne: c.lignes[0]
        ? {
            nom: c.lignes[0].produit.nom,
            quantite: c.lignes[0].quantite,
            imageUrl: c.lignes[0].produit.medias[0]?.url ?? null,
          }
        : null,
      tenant: tenantMap.get(c.tenantId) ?? null,
    })),
    total,
    page,
    totalPages: Math.ceil(total / limit),
  });
}

// ─── POST (création commande mobile) — WP-225 ────────────────────────────────

type LignePayload = {
  produitId: string;
  varianteId?: string | null;
  quantite: number;
  prix: number;
};

function isValidLigne(x: unknown): x is LignePayload {
  if (!x || typeof x !== "object") return false;
  const l = x as Record<string, unknown>;
  return (
    typeof l.produitId === "string"
    && typeof l.quantite === "number" && l.quantite > 0
    && typeof l.prix === "number" && l.prix >= 0
    && (l.varianteId === undefined || l.varianteId === null || typeof l.varianteId === "string")
  );
}

export async function POST(req: NextRequest) {
  try {
    const session = await getSessionFromRequest(req);
    if (!session || session.role !== "CLIENT") {
      return NextResponse.json(
        { error: "Connexion requise pour passer commande" },
        { status: 401 },
      );
    }
    const clientAccountId = session.userId;

    const body = await req.json().catch(() => null);
    if (!body || typeof body !== "object") {
      return NextResponse.json({ error: "Body invalide" }, { status: 400 });
    }

    const tenantId = typeof body.tenantId === "string" ? body.tenantId : null;
    const lignesRaw = Array.isArray(body.lignes) ? body.lignes : null;
    const modePaiement = typeof body.modePaiement === "string" ? body.modePaiement : null;
    const refPaiement = typeof body.refPaiement === "string" && body.refPaiement.trim()
      ? body.refPaiement.trim().slice(0, 200)
      : null;
    const noteClient = typeof body.noteClient === "string" && body.noteClient.trim()
      ? body.noteClient.trim().slice(0, 1000)
      : null;
    const instructionsPaiement = typeof body.instructionsPaiement === "string" && body.instructionsPaiement.trim()
      ? body.instructionsPaiement.trim().slice(0, 2000)
      : null;

    if (!tenantId) {
      return NextResponse.json({ error: "tenantId requis" }, { status: 400 });
    }
    if (!lignesRaw || lignesRaw.length === 0) {
      return NextResponse.json({ error: "Au moins 1 ligne requise" }, { status: 400 });
    }
    if (!lignesRaw.every(isValidLigne)) {
      return NextResponse.json({ error: "Format des lignes invalide" }, { status: 400 });
    }
    if (!modePaiement || !MODES_PAIEMENT_COMMANDE.includes(modePaiement as ModePaiementCommande)) {
      return NextResponse.json({ error: "modePaiement invalide" }, { status: 400 });
    }
    const lignes = lignesRaw as LignePayload[];

    const tenant = await prisma.tenant.findFirst({
      where: { id: tenantId, actif: true, vitrineActive: true },
      select: { id: true, nom: true },
    });
    if (!tenant) {
      return NextResponse.json({ error: "Vitrine introuvable" }, { status: 404 });
    }

    const produitIds = [...new Set(lignes.map((l) => l.produitId))];
    const produits = await prisma.produit.findMany({
      where: { id: { in: produitIds }, tenantId, disponible: true },
      select: { id: true, nom: true, stock: true, prix: true, disponibilite: true },
    });
    const produitsMap = new Map(produits.map((p) => [p.id, p]));

    for (const l of lignes) {
      const p = produitsMap.get(l.produitId);
      if (!p) {
        return NextResponse.json(
          { error: `Produit ${l.produitId} indisponible` },
          { status: 400 },
        );
      }
      // BUG-121 fix : stock négatif possible avant ce check.
      // Pas de vérif stock pour SUR_COMMANDE / A_PARTIR_DE / SUR_DEVIS — ces modes
      // acceptent par construction des commandes au-delà du stock physique.
      if (p.disponibilite === "IMMEDIATE" && p.stock < l.quantite) {
        return NextResponse.json(
          {
            error: `Stock insuffisant pour "${p.nom}" — ${p.stock} disponible(s)`,
            produitId: l.produitId,
            stockDisponible: p.stock,
          },
          { status: 409 },
        );