espace-paie-odentas/app/api/staff/naa/generate/route.ts

503 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
export const dynamic = "force-dynamic";
const s3Client = new S3Client({
region: process.env.AWS_REGION || "eu-west-3",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
// Fonction de polling pour attendre la génération du PDF
async function pollDocumentStatus(
documentUrl: string,
apiKey: string,
maxAttempts = 15,
intervalMs = 2000
): Promise<{ status: string; download_url?: string }> {
for (let i = 0; i < maxAttempts; i++) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
const res = await fetch(documentUrl, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) {
throw new Error(`Poll failed: ${res.statusText}`);
}
const data = await res.json();
const doc = data.document || data;
const status = doc.status;
console.log(`[NAA] Poll attempt ${i + 1}/${maxAttempts}, status: ${status}`);
if (status === "success") {
return { status, download_url: doc.download_url };
}
if (status === "failure") {
throw new Error("PDFMonkey document generation failed");
}
}
throw new Error("PDFMonkey polling timed out");
}
type Prestation = {
client: string;
code: string;
type_prestation: string;
quantite: number;
tarif: number;
total: number;
};
export async function POST(req: NextRequest) {
try {
const supabase = createRouteHandlerClient({ cookies });
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
}
// Vérifier que l'utilisateur est staff
const { data: staffUser } = await supabase
.from("staff_users")
.select("is_staff")
.eq("user_id", session.user.id)
.single();
if (!staffUser || !staffUser.is_staff) {
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
}
const body = await req.json();
const {
referrer_code,
periode,
callsheet_date,
limit_date,
transfer_reference,
solde_compte_apporteur = 0,
deposit = 0,
prestations = []
}: {
referrer_code: string;
periode: string;
callsheet_date: string;
limit_date?: string;
transfer_reference?: string;
solde_compte_apporteur: number;
deposit: number;
prestations: Prestation[];
} = body;
// Validation
if (!referrer_code || !periode || !callsheet_date) {
return NextResponse.json(
{ error: "Champs obligatoires manquants" },
{ status: 400 }
);
}
// Récupérer les infos de l'apporteur
const { data: referrer, error: referrerError } = await supabase
.from("referrers")
.select("*")
.eq("code", referrer_code)
.single();
if (referrerError || !referrer) {
return NextResponse.json(
{ error: "Apporteur non trouvé" },
{ status: 404 }
);
}
// Récupérer les clients apportés par cet apporteur avec les factures de la période
// Format de période attendu : "Février 2025" -> on extrait le mois et l'année
const [monthName, year] = periode.split(" ");
const monthMap: { [key: string]: string } = {
"Janvier": "01", "Février": "02", "Mars": "03", "Avril": "04",
"Mai": "05", "Juin": "06", "Juillet": "07", "Août": "08",
"Septembre": "09", "Octobre": "10", "Novembre": "11", "Décembre": "12"
};
const month = monthMap[monthName];
if (!month || !year) {
return NextResponse.json(
{ error: "Format de période invalide (attendu: 'Mois YYYY')" },
{ status: 400 }
);
}
// Calculer le mois suivant (pour la date d'émission des factures)
const periodMonth = parseInt(month);
const periodYear = parseInt(year);
let emissionMonth = periodMonth + 1;
let emissionYear = periodYear;
if (emissionMonth > 12) {
emissionMonth = 1;
emissionYear += 1;
}
const emissionMonthStr = emissionMonth.toString().padStart(2, '0');
// Date de début et fin du mois d'émission (1er au 10 du mois suivant généralement)
const emissionStartDate = `${emissionYear}-${emissionMonthStr}-01`;
const emissionEndDate = `${emissionYear}-${emissionMonthStr}-15`; // Buffer de 15 jours
// Récupérer les clients apportés
const { data: referredClients, error: clientsError } = await supabase
.from("organization_details")
.select(`
org_id,
referrer_code,
commission_rate,
code_employeur,
organizations!organization_details_org_id_fkey (
id,
name
)
`)
.eq("is_referred", true)
.eq("referrer_code", referrer_code);
if (clientsError) {
console.error("[NAA] Erreur récupération clients apportés:", clientsError);
return NextResponse.json(
{ error: "Erreur lors de la récupération des clients" },
{ status: 500 }
);
}
console.log(`[NAA] Trouvé ${referredClients?.length || 0} clients apportés par ${referrer_code}`);
if (referredClients && referredClients.length > 0) {
console.log("[NAA] Clients:", referredClients.map(c => {
const org = Array.isArray(c.organizations) ? c.organizations[0] : c.organizations;
return {
name: org?.name || 'N/A',
code: c.code_employeur,
commission_rate: c.commission_rate
};
}));
}
// Pour chaque client, récupérer les factures du mois concerné
const lineItems = [];
let totalCommission = 0;
for (const client of referredClients) {
const org = Array.isArray(client.organizations) ? client.organizations[0] : client.organizations;
const clientCode = client.code_employeur;
const clientName = org?.name || 'N/A';
console.log(`[NAA] Recherche facture pour client ${clientName} (${clientCode}), période: ${periode}`);
console.log(`[NAA] org_id utilisé pour la recherche: ${client.org_id}`);
// Stratégie 1 : Recherche exacte par period_label
let invoice = await supabase
.from("invoices")
.select("amount_ht, created_at, period_label")
.eq("org_id", client.org_id)
.eq("period_label", periode)
.order("created_at", { ascending: false })
.limit(1)
.maybeSingle()
.then(res => res.data);
console.log(`[NAA] Stratégie 1 (period_label="${periode}"): ${invoice ? `Trouvé ${invoice.amount_ht}` : 'Rien trouvé'}`);
// Stratégie 2 : Si pas trouvé, chercher par date de création (factures émises début du mois suivant)
if (!invoice) {
invoice = await supabase
.from("invoices")
.select("amount_ht, created_at, period_label")
.eq("org_id", client.org_id)
.gte("created_at", emissionStartDate)
.lte("created_at", emissionEndDate)
.order("created_at", { ascending: false })
.limit(1)
.maybeSingle()
.then(res => res.data);
console.log(`[NAA] Stratégie 2 (dates ${emissionStartDate} à ${emissionEndDate}): ${invoice ? `Trouvé ${invoice.amount_ht}` : 'Rien trouvé'}`);
}
// Stratégie 3 : Chercher dans une plage élargie si toujours rien
if (!invoice) {
// Chercher toutes les factures du client dans une plage de ±2 mois
const { data: invoices } = await supabase
.from("invoices")
.select("amount_ht, created_at, period_label")
.eq("org_id", client.org_id)
.gte("created_at", `${periodYear}-${month}-01`)
.lte("created_at", `${emissionYear}-${emissionMonthStr}-31`)
.order("created_at", { ascending: false });
console.log(`[NAA] Stratégie 3 (plage élargie): ${invoices?.length || 0} facture(s) trouvée(s)`);
if (invoices && invoices.length > 0) {
invoice = invoices[0]; // Prendre la plus récente
console.log(`[NAA] Utilisation de la facture la plus récente: ${invoice.amount_ht}€ (${invoice.period_label})`);
}
}
if (invoice && invoice.amount_ht > 0) {
const caHT = parseFloat(invoice.amount_ht);
const commissionRate = client.commission_rate || 0;
const commission = caHT * commissionRate;
totalCommission += commission;
console.log(`[NAA] ✅ Commission calculée: ${caHT}× ${(commissionRate * 100).toFixed(2)}% = ${commission.toFixed(2)}`);
lineItems.push({
id: clientCode || "UNKNOWN",
client: org.name,
code: clientCode,
comactuelle: commissionRate,
caht: caHT,
commission: commission
});
} else {
console.log(`[NAA] ⚠️ Aucune facture trouvée ou montant HT = 0`);
}
}
console.log(`[NAA] Total commission calculée: ${totalCommission.toFixed(2)}`);
console.log(`[NAA] Nombre de line items: ${lineItems.length}`);
const nbreClients = lineItems.length;
const nbrePrestations = prestations.length;
const totalFacture = totalCommission - deposit + solde_compte_apporteur;
// Utiliser la référence de virement comme numéro de NAA
const finalNAANumber = transfer_reference || `NAA-${Date.now()}`;
// Créer le document NAA dans la base
const { data: naaDoc, error: naaError } = await supabase
.from("naa_documents")
.insert({
naa_number: finalNAANumber,
referrer_id: referrer.id,
referrer_code: referrer_code,
periode,
callsheet_date,
limit_date: limit_date || null,
total_commission: totalCommission,
solde_compte_apporteur,
total_facture: totalFacture,
deposit,
nbre_clients: nbreClients,
nbre_prestations: nbrePrestations,
transfer_reference: transfer_reference || null,
status: "draft",
created_by: session.user.id
})
.select()
.single();
if (naaError) {
console.error("Erreur création NAA:", naaError);
return NextResponse.json(
{ error: "Erreur lors de la création de la NAA" },
{ status: 500 }
);
}
// Insérer les line items
if (lineItems.length > 0) {
const lineItemsToInsert = lineItems.map(item => {
const clientData = referredClients?.find(c => {
const clientCode = c.code_employeur || "";
return clientCode === item.code;
});
return {
naa_id: naaDoc.id,
organization_id: clientData?.org_id,
client_name: item.client,
client_code: item.code,
commission_rate: item.comactuelle,
ca_ht: item.caht,
commission: item.commission
};
});
await supabase.from("naa_line_items").insert(lineItemsToInsert);
}
// Insérer les prestations
if (prestations.length > 0) {
const prestationsToInsert = prestations.map(p => ({
naa_id: naaDoc.id,
client_name: p.client,
client_code: p.code,
type_prestation: p.type_prestation,
quantite: p.quantite,
tarif: p.tarif,
total: p.total
}));
await supabase.from("naa_prestations").insert(prestationsToInsert);
}
// Préparer les données pour PDFMonkey
const pdfMonkeyPayload = {
apporteur_address: referrer.address,
apporteur_cp: referrer.postal_code,
apporteur_city: referrer.city,
apporteur_code: referrer.code,
apporteur_name: referrer.name,
apporteur_contact: referrer.contact_name,
callsheet_date: new Date(callsheet_date).toLocaleDateString("fr-FR"),
limit_date: limit_date ? new Date(limit_date).toLocaleDateString("fr-FR") : "",
callsheet_number: finalNAANumber,
periode,
total_commission: totalCommission,
solde_compte_apporteur,
total_facture: totalFacture,
deposit,
nbre_clients: nbreClients,
nbre_prestations: nbrePrestations,
transfer_reference: transfer_reference || "",
logo_odentas: "",
lineItems,
prestations: prestations.map(p => ({
client: p.client,
code: p.code,
type_prestation: p.type_prestation,
quantite: p.quantite,
tarif: p.tarif,
total: p.total
}))
};
// Envoyer à PDFMonkey pour générer le PDF
const templateId = process.env.PDFMONKEY_NAA_TEMPLATE_ID || "422DA8A6-69E1-4798-B4A3-DF75D892BF2D";
const pdfMonkeyUrl = process.env.PDFMONKEY_URL || "https://api.pdfmonkey.io/api/v1/documents";
const pdfMonkeyApiKey = process.env.PDFMONKEY_API_KEY;
if (!pdfMonkeyApiKey) {
console.error("[NAA] Missing PDFMONKEY_API_KEY");
return NextResponse.json({ error: "Missing PDFMONKEY_API_KEY" }, { status: 500 });
}
console.log("[NAA] Calling PDFMonkey API...");
const pdfMonkeyResponse = await fetch(pdfMonkeyUrl, {
method: "POST",
headers: {
"Authorization": `Bearer ${pdfMonkeyApiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
document: {
document_template_id: templateId,
status: "pending",
payload: pdfMonkeyPayload,
meta: {
_filename: `${finalNAANumber}.pdf`
}
}
})
});
if (!pdfMonkeyResponse.ok) {
const errorText = await pdfMonkeyResponse.text();
console.error("[NAA] PDFMonkey create error:", errorText);
return NextResponse.json(
{ error: "PDFMonkey API error", details: errorText },
{ status: pdfMonkeyResponse.status }
);
}
const pdfMonkeyData = await pdfMonkeyResponse.json();
console.log("[NAA] PDFMonkey response:", pdfMonkeyData);
const documentId = pdfMonkeyData.document?.id;
if (!documentId) {
console.error("[NAA] No document ID in response");
return NextResponse.json({ error: "No document ID returned from PDFMonkey" }, { status: 500 });
}
// Construct the document URL for polling
const documentUrl = `${pdfMonkeyUrl}/${documentId}`;
console.log("[NAA] Document URL for polling:", documentUrl);
// Poll for completion
console.log("[NAA] Polling document status...");
const { status, download_url } = await pollDocumentStatus(documentUrl, pdfMonkeyApiKey, 15, 2000);
console.log("[NAA] Poll result:", { status, has_download_url: !!download_url });
if (status !== "success" || !download_url) {
console.error("[NAA] PDF generation failed or timed out");
return NextResponse.json({ error: "PDF generation failed or timed out", status }, { status: 500 });
}
// Download PDF
console.log("[NAA] Downloading PDF from:", download_url);
const pdfRes = await fetch(download_url);
if (!pdfRes.ok) {
console.error("[NAA] Failed to download PDF:", pdfRes.status, pdfRes.statusText);
return NextResponse.json({ error: "Failed to download PDF" }, { status: 500 });
}
const pdfBuffer = Buffer.from(await pdfRes.arrayBuffer());
console.log("[NAA] PDF downloaded, size:", pdfBuffer.length, "bytes");
// Uploader sur S3
console.log("[NAA] Uploading to S3...");
const bucketName = process.env.AWS_S3_BUCKET_NAME || "odentas-docs";
const s3Key = `naa/${finalNAANumber}.pdf`;
const uploadCommand = new PutObjectCommand({
Bucket: bucketName,
Key: s3Key,
Body: pdfBuffer,
ContentType: "application/pdf",
});
await s3Client.send(uploadCommand);
console.log("[NAA] S3 upload successful");
const s3Url = `https://${bucketName}.s3.${process.env.AWS_REGION || "eu-west-3"}.amazonaws.com/${s3Key}`;
// Générer une URL présignée valide 1 heure pour afficher le PDF
console.log("[NAA] Generating presigned URL...");
const getObjectCommand = new GetObjectCommand({
Bucket: bucketName,
Key: s3Key,
});
const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, { expiresIn: 3600 });
console.log("[NAA] Presigned URL generated");
// Mettre à jour le document NAA avec l'URL du PDF
console.log("[NAA] Updating NAA document with PDF URL...");
await supabase
.from("naa_documents")
.update({
pdf_url: s3Url,
s3_key: s3Key,
status: "sent"
})
.eq("id", naaDoc.id);
console.log("[NAA] ✅ NAA generation completed successfully");
return NextResponse.json({
success: true,
naa_number: finalNAANumber,
pdf_url: s3Url,
presigned_url: presignedUrl
});
} catch (error: any) {
console.error("[NAA] ❌ Error POST /api/staff/naa/generate:", error);
return NextResponse.json(
{ error: error.message || "Erreur serveur" },
{ status: 500 }
);
}
}