Checkpoint: Fix: bouton de téléchargement PDF BAP pour factures validées sans PDF (NOVRH VF11521, EVOLUCARE ET098857). Ajout procédure regenerateBapPdf côté serveur + bouton orange RefreshCw côté frontend pour régénérer le PDF à la volée. Déployé en recette et production.

This commit is contained in:
Manus
2026-06-17 03:41:18 -04:00
parent 756f8ab392
commit b97efb5c57
3 changed files with 139 additions and 4 deletions

View File

@@ -887,6 +887,114 @@ export const appRouter = router({
return { success: true, processed, errors, results };
}),
// ── Régénérer le PDF BAP pour une facture validée sans PDF ─────────────────
regenerateBapPdf: protectedProcedure
.input(z.object({ invoiceId: z.number() }))
.mutation(async ({ input, ctx }) => {
const invoice = await getInvoiceById(input.invoiceId);
if (!invoice || invoice.userId !== ctx.user.id) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Facture introuvable' });
}
if (!invoice.bapValidated) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'La facture n\'est pas validée BAP' });
}
const fs = await import('fs/promises');
const path = await import('path');
const { PDFDocument } = await import('pdf-lib');
const { localStoragePut, generateStorageKey } = await import('./localStorage');
const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), 'storage');
if (!invoice.fileKey && !invoice.fileUrl) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Fichier PDF source introuvable' });
}
let pdfBytes: Buffer;
const sourcePath = path.join(STORAGE_BASE_PATH, invoice.fileKey || '');
try {
pdfBytes = await fs.readFile(sourcePath);
} catch (_) {
const fileUrl = invoice.fileUrl;
if (!fileUrl) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Fichier PDF source introuvable' });
let absoluteUrl = fileUrl;
if (fileUrl.startsWith('/')) {
const baseUrl = process.env.APP_BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
absoluteUrl = `${baseUrl}${fileUrl}`;
}
const response = await fetch(absoluteUrl);
if (!response.ok) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Impossible de télécharger le PDF source' });
pdfBytes = Buffer.from(await response.arrayBuffer());
}
const pdfDoc = await PDFDocument.load(pdfBytes);
// Récupération de la signature du service
let sigBytesForCartouche: Buffer | undefined;
let sigMimeForCartouche: 'image/png' | 'image/jpeg' | undefined;
let signatureName: string | null = null;
if (invoice.serviceConcerne) {
const serviceAssociations = await getServiceSignaturesByUser(ctx.user.id);
const assoc = serviceAssociations.find(a => a.serviceName.toLowerCase() === (invoice.serviceConcerne || '').toLowerCase());
if (assoc) {
const sig = await getSignatureById(assoc.signatureId);
if (sig) {
signatureName = `${sig.firstName} ${sig.lastName}`;
try {
const sigImagePath = path.join(STORAGE_BASE_PATH, sig.imageKey);
try { sigBytesForCartouche = await fs.readFile(sigImagePath); } catch (_) {
const sigUrl = sig.imageUrl;
if (sigUrl) {
let absoluteSigUrl = sigUrl;
if (sigUrl.startsWith('/')) {
const baseUrl = process.env.APP_BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
absoluteSigUrl = `${baseUrl}${sigUrl}`;
}
const sigResp = await fetch(absoluteSigUrl);
if (sigResp.ok) sigBytesForCartouche = Buffer.from(await sigResp.arrayBuffer());
}
}
sigMimeForCartouche = sig.imageKey.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
} catch (_) { /* ignore */ }
}
}
}
// Placement du cartouche BAP
const validatedAt = invoice.bapValidatedAt ? new Date(invoice.bapValidatedAt) : new Date();
await drawBapCartouche(pdfDoc, pdfBytes, {
typeAchat: invoice.typeAchat || 'N/A',
destinataire: (invoice as any).recipientName || 'TOUS',
serviceConcerne: invoice.serviceConcerne || '-',
ventilationComptable: invoice.ventilationComptable || '-',
validatedAt,
signatureImageBytes: sigBytesForCartouche,
signatureMimeType: sigMimeForCartouche,
signatureName: signatureName || undefined,
});
const signedPdfBytes = await pdfDoc.save();
const _bapDateStr = invoice.invoiceDate ? new Date(invoice.invoiceDate).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
const _bapSupplier = (invoice.supplierName || 'Fournisseur').replace(/[^a-zA-Z0-9\u00e0-\u00ff \-]/g, '').trim();
const _bapNumber = (invoice.invoiceNumber || '').replace(/[^a-zA-Z0-9\-]/g, '').trim();
const bapFilename = [_bapDateStr, _bapSupplier, _bapNumber].filter(Boolean).join(' - ') + '.pdf';
const bapKey = generateStorageKey(ctx.user.id, bapFilename);
const { url } = await localStoragePut(bapKey, Buffer.from(signedPdfBytes), 'application/pdf');
// Mettre à jour la dernière entrée bapHistory de cette facture avec le nouveau pdfUrl
const allEntries = await getBapHistoryByUser(ctx.user.id);
const latestEntry = allEntries
.filter(e => e.invoiceId === input.invoiceId)
.sort((a, b) => new Date(b.validatedAt).getTime() - new Date(a.validatedAt).getTime())[0];
if (latestEntry) {
await updateBapHistoryPdfUrl(latestEntry.id, url);
}
// Mettre à jour le statut export de la facture
await updateInvoice(input.invoiceId, { exportStatus: 'exported' as any });
return { success: true, pdfUrl: url };
}),
// // ── Historique BAP ────────────────────
search: protectedProcedure
.input(z.object({ query: z.string() }))