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:
@@ -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() }))
|
||||
|
||||
Reference in New Issue
Block a user