src/app/api/panier/convertir/route.ts
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.
Concepts détectés — comprends la théorie
ORM Prisma
7 occurrencesCe fichier accède à la base de données via Prisma. Prisma est l'ORM utilisé côté backend pour les requêtes typées sur PostgreSQL.
Voir l'article général
Route API Next.js
3 occurrencesCe fichier est une route API Next.js (App Router). Voir le contrat API complet pour les conventions de réponse et d'auth.
Voir l'article général
1 export
POST
Code source· typescript
import { NextRequest, NextResponse } from "next/server";
import { envoyerConfirmationCommande, envoyerNotificationAdmin } from "@/lib/email";
import { sendPushToTenantAdmin } from "@/lib/push";
import { prisma } from "@/lib/prisma/client";
import { getSession } from "@/lib/auth/session";
import { getTenant } from "@/lib/auth/get-tenant";
export async function POST(req: NextRequest) {
try {
const tenant = await getTenant();
if (!tenant) return NextResponse.json({ error: "Tenant introuvable" }, { status: 404 });
const session = await getSession();
if (!session || session.role !== "CLIENT") {
return NextResponse.json({ error: "Connexion requise pour commander" }, { status: 401 });
}
const panier = await prisma.panier.findFirst({
where: { tenantId: tenant.id, clientId: session.userId, statut: "ACTIF" },
include: { lignes: { include: { variante: true } } },
});
if (!panier || panier.lignes.length === 0) {
return NextResponse.json({ error: "Panier vide" }, { status: 400 });
}
// Vérifier que le client existe dans la table clients (tenant-side)
const client = await prisma.client.findUnique({
where: { tenantId_email: { tenantId: tenant.id, email: "" } },
});
const total = panier.lignes.reduce(
(acc, l) => acc + l.prixSnapshot * l.quantite,
0
);
// Créer la commande + lignes en transaction
const commande = await prisma.$transaction(async (tx) => {
// Récupérer ou créer le client tenant
const clientAccount = await tx.clientAccount.findUnique({
where: { id: session.userId },
});
if (!clientAccount) throw new Error("Compte client introuvable");
let clientTenant = await tx.client.findFirst({
where: {
tenantId: tenant.id,
email: clientAccount.email ?? clientAccount.phone ?? "",
},
});
if (!clientTenant) {
clientTenant = await tx.client.create({
data: {
tenantId: tenant.id,
email: clientAccount.email ?? clientAccount.phone ?? "",
nom: clientAccount.name ?? "",
telephone: clientAccount.phone ?? "",
},
});
}
const nouvelleCommande = await tx.commande.create({
data: {
tenantId: tenant.id,
clientId: clientTenant.id,
total,
devise: panier.lignes[0]?.devise ?? "FCFA",
statut: "EN_ATTENTE",
lignes: {
create: panier.lignes.map((l) => ({
produitId: l.produitId,
varianteId: l.varianteId ?? null,
quantite: l.quantite,
prixUnitaire: l.prixSnapshot,
})),
},
},
});
// Decrementer le stock des produits commandes
for (const ligne of panier.lignes) {
await tx.produit.update({
where: { id: ligne.produitId },
data: { stock: { decrement: ligne.quantite } },
})
// Marquer indisponible si stock <= 0 apres decrement
const produitMaj = await tx.produit.findUnique({
where: { id: ligne.produitId },
select: { stock: true },
})
if (produitMaj && produitMaj.stock <= 0) {
await tx.produit.update({
where: { id: ligne.produitId },
data: { disponible: false, stock: 0 },
})
}
}
// Marquer le panier comme converti
await tx.panier.update({
where: { id: panier.id },
data: { statut: "CONVERTI" },
});
return nouvelleCommande;
});
// Envoyer les emails de notification
try {
const [clientAccount, adminUser] = await Promise.all([
prisma.clientAccount.findUnique({ where: { id: session.userId }, select: { name: true, prenom: true, email: true, phone: true } }),
prisma.user.findFirst({ where: { tenantId: tenant.id, role: "TENANT_ADMIN" }, select: { email: true } }),
]);
const lignesEmail = panier.lignes.map((l) => ({
nom: l.produitId, quantite: l.quantite, prixUnitaire: l.prixSnapshot, devise: l.devise,
}));
// Récupérer les noms des produits
const produits = await prisma.produit.findMany({
where: { id: { in: panier.lignes.map((l) => l.produitId) } },
select: { id: true, nom: true },
});
const lignesEmailAvecNoms = panier.lignes.map((l) => {
const nomProduit = produits.find((p) => p.id === l.produitId)?.nom ?? "Produit";
const variante = l.variante;
const nomVariante = variante
? Object.entries(variante.attributs as Record<string, string>).map(([k, v]) => `${k}: ${v}`).join(" · ")
: null;
return {
nom: nomVariante ? `${nomProduit} — ${nomVariante}` : nomProduit,
quantite: l.quantite,
prixUnitaire: l.prixSnapshot,
devise: l.devise,
};
});
const emailData = {
tenantNom: tenant.nom,
clientNom: (clientAccount?.prenom ?? "") + " " + (clientAccount?.name ?? ""),
clientEmail: clientAccount?.email ?? clientAccount?.phone ?? "",
commandeId: commande.id,
lignes: lignesEmailAvecNoms,
total: commande.total,
devise: commande.devise,
};
const emailPromises = [];
if (emailData.clientEmail && emailData.clientEmail.includes("@")) {
emailPromises.push(envoyerConfirmationCommande(emailData));
}
if (adminUser?.email) {
emailPromises.push(envoyerNotificationAdmin({ ...emailData, adminEmail: adminUser.email }));
}
await Promise.allSettled(emailPromises);
} catch (emailError) {
console.error("email notification error:", emailError);
}
// Push notification au gérant TENANT_ADMIN
try {
const totalFmt = `${commande.total.toLocaleString("fr-FR")} ${commande.devise}`;
const c = await prisma.clientAccount.findUnique({
where: { id: session.userId },
select: { prenom: true, name: true, email: true, phone: true },
});
const clientNomShort =
(c?.prenom && c?.name) ? `${c.prenom} ${c.name}` :
c?.prenom ?? c?.name ?? c?.email ?? c?.phone ?? "Client";
await sendPushToTenantAdmin(tenant.id, {
title: "Nouvelle commande",
body: `${clientNomShort} · ${totalFmt}`,
data: { type: "commande", commandeId: commande.id, tenantId: tenant.id },
});
} catch (pushError) {
console.error("push notification error:", pushError);
}
return NextResponse.json({ success: true, commandeId: commande.id });
} catch (error) {
console.error("panier convertir error:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
import { NextRequest, NextResponse } from "next/server";
import { envoyerConfirmationCommande, envoyerNotificationAdmin } from "@/lib/email";
import { sendPushToTenantAdmin } from "@/lib/push";
import { prisma } from "@/lib/prisma/client";
import { getSession } from "@/lib/auth/session";
import { getTenant } from "@/lib/auth/get-tenant";
export async function POST(req: NextRequest) {
try {
const tenant = await getTenant();
if (!tenant) return NextResponse.json({ error: "Tenant introuvable" }, { status: 404 });
const session = await getSession();
if (!session || session.role !== "CLIENT") {
return NextResponse.json({ error: "Connexion requise pour commander" }, { status: 401 });
}
const panier = await prisma.panier.findFirst({
where: { tenantId: tenant.id, clientId: session.userId, statut: "ACTIF" },
include: { lignes: { include: { variante: true } } },
});
if (!panier || panier.lignes.length === 0) {
return NextResponse.json({ error: "Panier vide" }, { status: 400 });
}
// Vérifier que le client existe dans la table clients (tenant-side)
const client = await prisma.client.findUnique({
where: { tenantId_email: { tenantId: tenant.id, email: "" } },
});
const total = panier.lignes.reduce(
(acc, l) => acc + l.prixSnapshot * l.quantite,
0
);
// Créer la commande + lignes en transaction
const commande = await prisma.$transaction(async (tx) => {
// Récupérer ou créer le client tenant
const clientAccount = await tx.clientAccount.findUnique({
where: { id: session.userId },
});
if (!clientAccount) throw new Error("Compte client introuvable");
let clientTenant = await tx.client.findFirst({
where: {
tenantId: tenant.id,
email: clientAccount.email ?? clientAccount.phone ?? "",
},
});
if (!clientTenant) {
clientTenant = await tx.client.create({
data: {
tenantId: tenant.id,
email: clientAccount.email ?? clientAccount.phone ?? "",
nom: clientAccount.name ?? "",
telephone: clientAccount.phone ?? "",
},
});
}
const nouvelleCommande = await tx.commande.create({
data: {
tenantId: tenant.id,
clientId: clientTenant.id,
total,
devise: panier.lignes[0]?.devise ?? "FCFA",
statut: "EN_ATTENTE",
lignes: {
create: panier.lignes.map((l) => ({
produitId: l.produitId,
varianteId: l.varianteId ?? null,
quantite: l.quantite,
prixUnitaire: l.prixSnapshot,
})),
},
},
});
// Decrementer le stock des produits commandes
for (const ligne of panier.lignes) {
await tx.produit.update({
where: { id: ligne.produitId },
data: { stock: { decrement: ligne.quantite } },
})
// Marquer indisponible si stock <= 0 apres decrement
const produitMaj = await tx.produit.findUnique({
where: { id: ligne.produitId },
select: { stock: true },
})
if (produitMaj && produitMaj.stock <= 0) {
await tx.produit.update({
where: { id: ligne.produitId },
data: { disponible: false, stock: 0 },
})
}
}
// Marquer le panier comme converti
await tx.panier.update({
where: { id: panier.id },
data: { statut: "CONVERTI" },
});
return nouvelleCommande;
});
// Envoyer les emails de notification
try {
const [clientAccount, adminUser] = await Promise.all([
prisma.clientAccount.findUnique({ where: { id: session.userId }, select: { name: true, prenom: true, email: true, phone: true } }),
prisma.user.findFirst({ where: { tenantId: tenant.id, role: "TENANT_ADMIN" }, select: { email: true } }),
]);
const lignesEmail = panier.lignes.map((l) => ({
nom: l.produitId, quantite: l.quantite, prixUnitaire: l.prixSnapshot, devise: l.devise,
}));
// Récupérer les noms des produits
const produits = await prisma.produit.findMany({
where: { id: { in: panier.lignes.map((l) => l.produitId) } },
select: { id: true, nom: true },
});
const lignesEmailAvecNoms = panier.lignes.map((l) => {
const nomProduit = produits.find((p) => p.id === l.produitId)?.nom ?? "Produit";
const variante = l.variante;
const nomVariante = variante
? Object.entries(variante.attributs as Record<string, string>).map(([k, v]) => `${k}: ${v}`).join(" · ")
: null;
return {
nom: nomVariante ? `${nomProduit} — ${nomVariante}` : nomProduit,
quantite: l.quantite,
prixUnitaire: l.prixSnapshot,
devise: l.devise,
};
});
const emailData = {
tenantNom: tenant.nom,
clientNom: (clientAccount?.prenom ?? "") + " " + (clientAccount?.name ?? ""),
clientEmail: clientAccount?.email ?? clientAccount?.phone ?? "",
commandeId: commande.id,
lignes: lignesEmailAvecNoms,
total: commande.total,
devise: commande.devise,
};
const emailPromises = [];
if (emailData.clientEmail && emailData.clientEmail.includes("@")) {
emailPromises.push(envoyerConfirmationCommande(emailData));
}
if (adminUser?.email) {
emailPromises.push(envoyerNotificationAdmin({ ...emailData, adminEmail: adminUser.email }));
}
await Promise.allSettled(emailPromises);
} catch (emailError) {
console.error("email notification error:", emailError);
}
// Push notification au gérant TENANT_ADMIN
try {
const totalFmt = `${commande.total.toLocaleString("fr-FR")} ${commande.devise}`;
const c = await prisma.clientAccount.findUnique({
where: { id: session.userId },
select: { prenom: true, name: true, email: true, phone: true },
});
const clientNomShort =
(c?.prenom && c?.name) ? `${c.prenom} ${c.name}` :
c?.prenom ?? c?.name ?? c?.email ?? c?.phone ?? "Client";
await sendPushToTenantAdmin(tenant.id, {
title: "Nouvelle commande",
body: `${clientNomShort} · ${totalFmt}`,
data: { type: "commande", commandeId: commande.id, tenantId: tenant.id },
});
} catch (pushError) {
console.error("push notification error:", pushError);
}
return NextResponse.json({ success: true, commandeId: commande.id });
} catch (error) {
console.error("panier convertir error:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}