src/app/api/paydunya/callback/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
4 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
2 exports
POSTGET
Code source· typescript
// PayDunya IPN callback — appelé par PayDunya quand le statut d'une transaction change.
// On NE FAIT PAS confiance au payload reçu : on re-interroge l'API PayDunya via verifierPaiement()
// pour valider le statut, puis on met à jour la Reservation correspondante.
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { verifierPaiement, mapPayDunyaToStatutPaiement } from "@/lib/paydunya";
async function extractToken(req: NextRequest): Promise<string | null> {
const ct = req.headers.get("content-type") ?? "";
try {
if (ct.includes("application/json")) {
const body = (await req.json().catch(() => null)) as { token?: string; data?: { invoice?: { token?: string } } } | null;
if (typeof body?.token === "string") return body.token;
if (typeof body?.data?.invoice?.token === "string") return body.data.invoice.token;
return null;
}
if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) {
const form = await req.formData();
const direct = form.get("token");
if (typeof direct === "string" && direct) return direct;
const data = form.get("data");
if (typeof data === "string") {
const parsed = JSON.parse(data) as { invoice?: { token?: string } };
if (typeof parsed?.invoice?.token === "string") return parsed.invoice.token;
}
}
} catch (e) {
console.error("paydunya/callback parse error:", e);
}
// Fallback querystring (return_url)
return req.nextUrl.searchParams.get("token");
}
export async function POST(req: NextRequest) {
return handle(req);
}
export async function GET(req: NextRequest) {
// Certains environnements pingent en GET (return_url) — on accepte aussi
return handle(req);
}
async function handle(req: NextRequest) {
const token = await extractToken(req);
if (!token) {
return NextResponse.json({ error: "token PayDunya manquant" }, { status: 400 });
}
const verif = await verifierPaiement(token);
if (!verif.ok) {
console.error("paydunya/callback verify failed:", verif.error);
return NextResponse.json({ error: verif.error }, { status: 502 });
}
const reservationId = typeof verif.customData.reservationId === "string" ? verif.customData.reservationId : null;
if (!reservationId) {
console.warn("paydunya/callback: customData.reservationId absent");
return NextResponse.json({ ok: true, ignored: "Pas de reservationId associé" });
}
const reservation = await prisma.reservation.findUnique({
where: { id: reservationId },
select: {
id: true,
statut: true,
statutPaiement: true,
montantTotal: true,
montantAcompte: true,
paydunyaToken: true,
},
});
if (!reservation) {
return NextResponse.json({ ok: true, ignored: "Réservation introuvable" });
}
// Lie le token côté DB s'il manque (premier callback)
const tokenPatch = reservation.paydunyaToken ? {} : { paydunyaToken: token };
const hasAcompte = reservation.montantAcompte != null && reservation.montantAcompte > 0;
const nextStatutPaiement = mapPayDunyaToStatutPaiement(verif.status, hasAcompte);
if (!nextStatutPaiement) {
// pending / unknown — on n'écrit rien sauf le token éventuellement
if (Object.keys(tokenPatch).length > 0) {
await prisma.reservation.update({ where: { id: reservation.id }, data: tokenPatch });
}
return NextResponse.json({ ok: true, status: verif.status, applied: null });
}
// Auto-confirme la réservation si le paiement complet arrive et qu'elle est encore EN_ATTENTE
const promote = (
nextStatutPaiement === "PAYE_INTEGRALEMENT" &&
reservation.statut === "EN_ATTENTE"
);
await prisma.reservation.update({
where: { id: reservation.id },
data: {
...tokenPatch,
statutPaiement: nextStatutPaiement,
modePaiement: "PAYDUNYA",
...(promote ? { statut: "CONFIRME", confirmedAt: new Date() } : {}),
},
});
return NextResponse.json({
ok: true,
status: verif.status,
applied: nextStatutPaiement,
promotedToConfirme: promote,
});
}
// PayDunya IPN callback — appelé par PayDunya quand le statut d'une transaction change.
// On NE FAIT PAS confiance au payload reçu : on re-interroge l'API PayDunya via verifierPaiement()
// pour valider le statut, puis on met à jour la Reservation correspondante.
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma/client";
import { verifierPaiement, mapPayDunyaToStatutPaiement } from "@/lib/paydunya";
async function extractToken(req: NextRequest): Promise<string | null> {
const ct = req.headers.get("content-type") ?? "";
try {
if (ct.includes("application/json")) {
const body = (await req.json().catch(() => null)) as { token?: string; data?: { invoice?: { token?: string } } } | null;
if (typeof body?.token === "string") return body.token;
if (typeof body?.data?.invoice?.token === "string") return body.data.invoice.token;
return null;
}
if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) {
const form = await req.formData();
const direct = form.get("token");
if (typeof direct === "string" && direct) return direct;
const data = form.get("data");
if (typeof data === "string") {
const parsed = JSON.parse(data) as { invoice?: { token?: string } };
if (typeof parsed?.invoice?.token === "string") return parsed.invoice.token;
}
}
} catch (e) {
console.error("paydunya/callback parse error:", e);
}
// Fallback querystring (return_url)
return req.nextUrl.searchParams.get("token");
}
export async function POST(req: NextRequest) {
return handle(req);
}
export async function GET(req: NextRequest) {
// Certains environnements pingent en GET (return_url) — on accepte aussi
return handle(req);
}
async function handle(req: NextRequest) {
const token = await extractToken(req);
if (!token) {
return NextResponse.json({ error: "token PayDunya manquant" }, { status: 400 });
}
const verif = await verifierPaiement(token);
if (!verif.ok) {
console.error("paydunya/callback verify failed:", verif.error);
return NextResponse.json({ error: verif.error }, { status: 502 });
}
const reservationId = typeof verif.customData.reservationId === "string" ? verif.customData.reservationId : null;
if (!reservationId) {
console.warn("paydunya/callback: customData.reservationId absent");
return NextResponse.json({ ok: true, ignored: "Pas de reservationId associé" });
}
const reservation = await prisma.reservation.findUnique({
where: { id: reservationId },
select: {
id: true,
statut: true,
statutPaiement: true,
montantTotal: true,
montantAcompte: true,
paydunyaToken: true,
},
});
if (!reservation) {
return NextResponse.json({ ok: true, ignored: "Réservation introuvable" });
}
// Lie le token côté DB s'il manque (premier callback)
const tokenPatch = reservation.paydunyaToken ? {} : { paydunyaToken: token };
const hasAcompte = reservation.montantAcompte != null && reservation.montantAcompte > 0;
const nextStatutPaiement = mapPayDunyaToStatutPaiement(verif.status, hasAcompte);
if (!nextStatutPaiement) {
// pending / unknown — on n'écrit rien sauf le token éventuellement
if (Object.keys(tokenPatch).length > 0) {
await prisma.reservation.update({ where: { id: reservation.id }, data: tokenPatch });
}
return NextResponse.json({ ok: true, status: verif.status, applied: null });
}
// Auto-confirme la réservation si le paiement complet arrive et qu'elle est encore EN_ATTENTE
const promote = (
nextStatutPaiement === "PAYE_INTEGRALEMENT" &&
reservation.statut === "EN_ATTENTE"
);
await prisma.reservation.update({
where: { id: reservation.id },
data: {
...tokenPatch,
statutPaiement: nextStatutPaiement,
modePaiement: "PAYDUNYA",
...(promote ? { statut: "CONFIRME", confirmedAt: new Date() } : {}),
},
});
return NextResponse.json({
ok: true,
status: verif.status,
applied: nextStatutPaiement,
promotedToConfirme: promote,
});
}