src/app/api/mobile/conversations/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
6 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
4 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
JWT / Auth backend
1 occurrenceCe fichier touche au système d'authentification (JWT, session, getSessionFromRequest). Voir le contrat API pour la logique complète.
Voir l'article général
2 exports
GETPOST
Code source· typescript
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { getSessionFromRequest } from "@/lib/auth/session";
// GET /api/mobile/conversations
// CLIENT : liste des conversations du client (tous tenants)
// GERANT : liste des conversations du tenant
export async function GET(req: NextRequest) {
const session = await getSessionFromRequest(req);
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
const where =
session.role === "CLIENT"
? { clientAccountId: session.userId }
: session.role === "TENANT_ADMIN" && session.tenantId
? { tenantId: session.tenantId }
: null;
if (!where) return NextResponse.json({ error: "Rôle non supporté" }, { status: 403 });
const conversations = await prisma.conversation.findMany({
where,
orderBy: { derniereMessageAt: "desc" },
take: 100,
include: {
tenant: { select: { id: true, nom: true, subdomain: true, logoUrl: true } },
clientAccount: { select: { id: true, prenom: true, nom: true, name: true, photoUrl: true } },
produit: { select: { id: true, nom: true, prix: true, devise: true, medias: { take: 1, select: { url: true } } } },
prestation: { select: { id: true, nom: true, prixMin: true, prixMax: true, devise: true, medias: { take: 1, select: { url: true } } } },
messages: { orderBy: { createdAt: "desc" }, take: 1, select: { texte: true, auteur: true, createdAt: true } },
},
});
return NextResponse.json({
conversations: conversations.map((c) => ({
id: c.id,
tenant: c.tenant,
client: c.clientAccount,
contexte: c.contexte,
produit: c.produit,
prestation: c.prestation,
derniereMessageAt: c.derniereMessageAt.toISOString(),
nonLu: session.role === "CLIENT" ? c.nonLuClient : c.nonLuTenant,
dernierMessage: c.messages[0]
? { texte: c.messages[0].texte, auteur: c.messages[0].auteur, createdAt: c.messages[0].createdAt.toISOString() }
: null,
})),
});
}
// POST /api/mobile/conversations
// CLIENT uniquement (le gérant répond à une conv existante, il n'en ouvre pas).
// Body: { tenantId, contexte?, produitId?, prestationId? }. Upsert : retourne existing si déjà créée.
export async function POST(req: NextRequest) {
const session = await getSessionFromRequest(req);
if (!session || session.role !== "CLIENT") {
return NextResponse.json({ error: "Authentification client requise" }, { status: 401 });
}
let body: { tenantId?: string; contexte?: string; produitId?: string; prestationId?: string };
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "JSON invalide" }, { status: 400 });
}
const tenantId = typeof body.tenantId === "string" ? body.tenantId : null;
if (!tenantId) return NextResponse.json({ error: "tenantId requis" }, { status: 400 });
const tenant = await prisma.tenant.findUnique({
where: { id: tenantId },
select: { id: true, actif: true, messagerieActive: true },
});
if (!tenant || !tenant.actif) return NextResponse.json({ error: "Tenant introuvable" }, { status: 404 });
if (!tenant.messagerieActive) return NextResponse.json({ error: "Messagerie désactivée par ce vendeur" }, { status: 403 });
const contexte = typeof body.contexte === "string" && body.contexte.trim() ? body.contexte.trim().slice(0, 200) : null;
const produitId = typeof body.produitId === "string" && body.produitId.trim() ? body.produitId.trim() : null;
const prestationId = typeof body.prestationId === "string" && body.prestationId.trim() ? body.prestationId.trim() : null;
if (produitId) {
const p = await prisma.produit.findUnique({ where: { id: produitId }, select: { tenantId: true } });
if (!p || p.tenantId !== tenantId) {
return NextResponse.json({ error: "Produit introuvable ou n'appartient pas à ce vendeur" }, { status: 400 });
}
}
if (prestationId) {
const p = await prisma.prestation.findUnique({ where: { id: prestationId }, select: { tenantId: true } });
if (!p || p.tenantId !== tenantId) {
return NextResponse.json({ error: "Prestation introuvable ou n'appartient pas à ce vendeur" }, { status: 400 });
}
}
const updateData: { contexte?: string; produitId?: string; prestationId?: string } = {};
if (contexte) updateData.contexte = contexte;
if (produitId) updateData.produitId = produitId;
if (prestationId) updateData.prestationId = prestationId;
const conv = await prisma.conversation.upsert({
where: { clientAccountId_tenantId: { clientAccountId: session.userId, tenantId } },
create: {
clientAccountId: session.userId,
tenantId,
contexte,
produitId,
prestationId,
},
update: updateData,
include: {
tenant: { select: { id: true, nom: true, subdomain: true, logoUrl: true } },
produit: { select: { id: true, nom: true, prix: true, devise: true, medias: { take: 1, select: { url: true } } } },
prestation: { select: { id: true, nom: true, prixMin: true, prixMax: true, devise: true, medias: { take: 1, select: { url: true } } } },
},
});
return NextResponse.json({
conversation: {
id: conv.id,
tenant: conv.tenant,
contexte: conv.contexte,
produit: conv.produit,
prestation: conv.prestation,
derniereMessageAt: conv.derniereMessageAt.toISOString(),
nonLu: 0,
},
});
}
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { getSessionFromRequest } from "@/lib/auth/session";
// GET /api/mobile/conversations
// CLIENT : liste des conversations du client (tous tenants)
// GERANT : liste des conversations du tenant
export async function GET(req: NextRequest) {
const session = await getSessionFromRequest(req);
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
const where =
session.role === "CLIENT"
? { clientAccountId: session.userId }
: session.role === "TENANT_ADMIN" && session.tenantId
? { tenantId: session.tenantId }
: null;
if (!where) return NextResponse.json({ error: "Rôle non supporté" }, { status: 403 });
const conversations = await prisma.conversation.findMany({
where,
orderBy: { derniereMessageAt: "desc" },
take: 100,
include: {
tenant: { select: { id: true, nom: true, subdomain: true, logoUrl: true } },
clientAccount: { select: { id: true, prenom: true, nom: true, name: true, photoUrl: true } },
produit: { select: { id: true, nom: true, prix: true, devise: true, medias: { take: 1, select: { url: true } } } },
prestation: { select: { id: true, nom: true, prixMin: true, prixMax: true, devise: true, medias: { take: 1, select: { url: true } } } },
messages: { orderBy: { createdAt: "desc" }, take: 1, select: { texte: true, auteur: true, createdAt: true } },
},
});
return NextResponse.json({
conversations: conversations.map((c) => ({
id: c.id,
tenant: c.tenant,
client: c.clientAccount,
contexte: c.contexte,
produit: c.produit,
prestation: c.prestation,
derniereMessageAt: c.derniereMessageAt.toISOString(),
nonLu: session.role === "CLIENT" ? c.nonLuClient : c.nonLuTenant,
dernierMessage: c.messages[0]
? { texte: c.messages[0].texte, auteur: c.messages[0].auteur, createdAt: c.messages[0].createdAt.toISOString() }
: null,
})),
});
}
// POST /api/mobile/conversations
// CLIENT uniquement (le gérant répond à une conv existante, il n'en ouvre pas).
// Body: { tenantId, contexte?, produitId?, prestationId? }. Upsert : retourne existing si déjà créée.
export async function POST(req: NextRequest) {
const session = await getSessionFromRequest(req);
if (!session || session.role !== "CLIENT") {
return NextResponse.json({ error: "Authentification client requise" }, { status: 401 });
}
let body: { tenantId?: string; contexte?: string; produitId?: string; prestationId?: string };
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "JSON invalide" }, { status: 400 });
}
const tenantId = typeof body.tenantId === "string" ? body.tenantId : null;
if (!tenantId) return NextResponse.json({ error: "tenantId requis" }, { status: 400 });
const tenant = await prisma.tenant.findUnique({
where: { id: tenantId },
select: { id: true, actif: true, messagerieActive: true },
});
if (!tenant || !tenant.actif) return NextResponse.json({ error: "Tenant introuvable" }, { status: 404 });
if (!tenant.messagerieActive) return NextResponse.json({ error: "Messagerie désactivée par ce vendeur" }, { status: 403 });
const contexte = typeof body.contexte === "string" && body.contexte.trim() ? body.contexte.trim().slice(0, 200) : null;
const produitId = typeof body.produitId === "string" && body.produitId.trim() ? body.produitId.trim() : null;
const prestationId = typeof body.prestationId === "string" && body.prestationId.trim() ? body.prestationId.trim() : null;
if (produitId) {
const p = await prisma.produit.findUnique({ where: { id: produitId }, select: { tenantId: true } });
if (!p || p.tenantId !== tenantId) {
return NextResponse.json({ error: "Produit introuvable ou n'appartient pas à ce vendeur" }, { status: 400 });
}
}
if (prestationId) {
const p = await prisma.prestation.findUnique({ where: { id: prestationId }, select: { tenantId: true } });
if (!p || p.tenantId !== tenantId) {
return NextResponse.json({ error: "Prestation introuvable ou n'appartient pas à ce vendeur" }, { status: 400 });
}
}
const updateData: { contexte?: string; produitId?: string; prestationId?: string } = {};
if (contexte) updateData.contexte = contexte;
if (produitId) updateData.produitId = produitId;
if (prestationId) updateData.prestationId = prestationId;
const conv = await prisma.conversation.upsert({
where: { clientAccountId_tenantId: { clientAccountId: session.userId, tenantId } },
create: {
clientAccountId: session.userId,
tenantId,
contexte,
produitId,
prestationId,
},
update: updateData,
include: {
tenant: { select: { id: true, nom: true, subdomain: true, logoUrl: true } },
produit: { select: { id: true, nom: true, prix: true, devise: true, medias: { take: 1, select: { url: true } } } },
prestation: { select: { id: true, nom: true, prixMin: true, prixMax: true, devise: true, medias: { take: 1, select: { url: true } } } },
},
});
return NextResponse.json({
conversation: {
id: conv.id,
tenant: conv.tenant,
contexte: conv.contexte,
produit: conv.produit,
prestation: conv.prestation,
derniereMessageAt: conv.derniereMessageAt.toISOString(),
nonLu: 0,
},
});
}