src/app/api/paydunya/callback/route.ts

route·app·4.1 KB · 114 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

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,
  });
}