src/app/api/mobile/vitrine/commandes/[id]/route.ts

route·app·10.7 KB · 300 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

GETPATCH

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

import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { getSessionFromRequest } from "@/lib/auth/session";
import { envoyerPaiementValide } from "@/lib/email";
import { sendPushToClient } from "@/lib/push";
import { captureError } from "@/lib/sentry";
import type { Prisma } from "@prisma/client";

const STATUTS_VALIDES = ["EN_ATTENTE", "CONFIRME", "PRET_RETRAIT", "EN_LIVRAISON", "LIVRE", "ANNULE"] as const;
const STATUTS_PAIEMENT_VALIDES = ["EN_ATTENTE", "PAYE", "REFUSE", "REMBOURSE"] as const;
const TRANSITIONS_STATUT: Record<string, string[]> = {
  EN_ATTENTE:   ["CONFIRME", "ANNULE"],
  CONFIRME:     ["PRET_RETRAIT", "EN_LIVRAISON", "ANNULE"],
  PRET_RETRAIT: ["LIVRE", "ANNULE"],
  EN_LIVRAISON: ["LIVRE", "ANNULE"],
  LIVRE:        [],
  ANNULE:       [],
};
const NOTE_MAX = 500;

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const session = await getSessionFromRequest(req);
  if (!session || session.role !== "TENANT_ADMIN" || !session.tenantId) {
    return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
  }

  const { id } = await params;

  try {
    const commande = await prisma.commande.findFirst({
      where: { id, tenantId: session.tenantId },
      include: {
        client: { select: { nom: true, email: true, telephone: true } },
        lignes: {
          include: {
            produit: {
              select: {
                nom: true,
                medias: { orderBy: { ordre: "asc" }, take: 1, select: { url: true } },
              },
            },
            variante: { select: { attributs: true } },
          },
        },
      },
    });
    // Note : modePaiement, refPaiement, instructionsPaiement sont déjà inclus
    // par défaut dans `commande` (Prisma retourne tous les champs scalaires
    // sans select explicite).

    if (!commande) {
      return NextResponse.json({ error: "Commande introuvable" }, { status: 404 });
    }

    return NextResponse.json({ commande });
  } catch (error) {
    captureError(error, {
      route: "/api/mobile/vitrine/commandes/[id]",
      tenantId: session.tenantId,
      userId: session.userId,
      role: session.role,
    });
    console.error("boutique/commandes/[id] error:", error);
    return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
  }
}

// PATCH générique — accepte statut (avec validation transitions strict),
// statutPaiement (toggle libre) et noteInterne (max 500 chars)
export async function PATCH(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const session = await getSessionFromRequest(req);
  if (!session || session.role !== "TENANT_ADMIN" || !session.tenantId) {
    return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
  }

  const { id } = await params;
  const body = await req.json().catch(() => ({}));

  const existing = await prisma.commande.findFirst({
    where: { id, tenantId: session.tenantId },
    select: { id: true, statut: true, statutPaiement: true },
  });
  if (!existing) {
    return NextResponse.json({ error: "Commande introuvable" }, { status: 404 });
  }

  const data: Prisma.CommandeUpdateInput = {};

  if (typeof body.statut === "string") {
    if (!STATUTS_VALIDES.includes(body.statut)) {
      return NextResponse.json({ error: "Statut invalide" }, { status: 400 });
    }
    if (body.statut !== existing.statut) {
      const transitions = TRANSITIONS_STATUT[existing.statut] ?? [];
      if (!transitions.includes(body.statut)) {
        return NextResponse.json(
          { error: `Transition ${existing.statut} → ${body.statut} non autorisée` },
          { status: 422 }
        );
      }
      data.statut = body.statut;
    }
  }

  if (typeof body.statutPaiement === "string") {
    if (!STATUTS_PAIEMENT_VALIDES.includes(body.statutPaiement)) {
      return NextResponse.json({ error: "Statut paiement invalide" }, { status: 400 });
    }
    data.statutPaiement = body.statutPaiement;
  }

  // M2 fix : permettre au gérant d'éditer la référence de paiement
  // (cas usage : client envoie ref par WhatsApp après le checkout).
  if ("refPaiement" in body) {
    if (body.refPaiement === null || body.refPaiement === "") {
      data.refPaiement = null;
    } else if (typeof body.refPaiement === "string") {
      data.refPaiement = body.refPaiement.trim().slice(0, 200) || null;
    }
  }

  if ("noteInterne" in body) {
    if (body.noteInterne === null || body.noteInterne === "") {
      data.noteInterne = null;
    } else if (typeof body.noteInterne === "string") {
      const trimmed = body.noteInterne.trim();
      if (trimmed.length > NOTE_MAX) {
        return NextResponse.json({ error: `Note trop longue (max ${NOTE_MAX} caractères)` }, { status: 400 });
      }
      data.noteInterne = trimmed || null;
    }
  }

  if (Object.keys(data).length === 0) {
    return NextResponse.json({ error: "Aucune modification" }, { status: 400 });
  }

  const updated = await prisma.commande.update({
    where: { id },
    data,
    select: {
      id: true,
      statut: true,
      statutPaiement: true,
      noteInterne: true,
      modePaiement: true,
      refPaiement: true,
      updatedAt: true,
    },
  });

  // Email + push client à la transition EN_ATTENTE → PAYE/REFUSE (WP-225 G + WP-227)
  if (
    typeof body.statutPaiement === "string"
    && existing.statutPaiement === "EN_ATTENTE"
    && (body.statutPaiement === "PAYE" || body.statutPaiement === "REFUSE")
  ) {
    void (async () => {
      try {
        const [full, tenantInfo] = await Promise.all([
          prisma.commande.findUnique({
            where: { id },
            select: {
              id: true, total: true, devise: true, tenantId: true,
              clientAccountId: true,
              client: { select: { nom: true, email: true, telephone: true } },
            },
          }),
          prisma.tenant.findUnique({
            where: { id: session.tenantId! },
            select: { nom: true, subdomain: true },
          }),
        ]);
        if (!full || !tenantInfo) return;

        // Email best-effort
        if (full.client.email && full.client.email.includes("@")) {
          await envoyerPaiementValide({
            clientEmail: full.client.email,
            clientNom: full.client.nom ?? null,
            tenantNom: tenantInfo.nom,
            tenantSubdomain: tenantInfo.subdomain,
            commandeId: full.id,
            montant: full.total,
            devise: full.devise,
            statut: body.statutPaiement as "PAYE" | "REFUSE",
          }).catch((e) => console.error("[commandes PATCH] paiement email failed:", e));
        }

        // Push client best-effort (WP-227) — lookup ClientAccount par email/phone
        // M6 — utiliser la FK directe Commande.clientAccountId
        // Fallback lookup OR pour les commandes pré-WP-227 sans clientAccountId.
        let ca: { id: string } | null = null;
        if (full.clientAccountId) {