503 lines
17 KiB
TypeScript
503 lines
17 KiB
TypeScript
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 }
|
||
);
|
||
}
|
||
}
|