fix: rétablir la suppression de facture en recette
This commit is contained in:
11
drizzle/0042_deleted_invoices_blacklist.sql
Normal file
11
drizzle/0042_deleted_invoices_blacklist.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Rattrapage additif : certains environnements historiques possèdent le code
|
||||
-- de blacklist sans avoir exécuté la migration 0035 correspondante.
|
||||
CREATE TABLE IF NOT EXISTS `deletedInvoices` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`userId` int NOT NULL,
|
||||
`invoiceNumber` varchar(100) NOT NULL,
|
||||
`totalAmount` varchar(50),
|
||||
`supplierName` varchar(255),
|
||||
`deletedAt` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `deletedInvoices_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
@@ -295,6 +295,13 @@
|
||||
"when": 1790785209695,
|
||||
"tag": "0041_acoustic_microchip",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 42,
|
||||
"version": "5",
|
||||
"when": 1790960584104,
|
||||
"tag": "0042_deleted_invoices_blacklist",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
26
server/db.ts
26
server/db.ts
@@ -291,12 +291,26 @@ export async function deleteInvoice(id: number) {
|
||||
// Enregistrer dans la blacklist avant suppression
|
||||
const invoice = await db.select().from(invoices).where(eq(invoices.id, id)).limit(1);
|
||||
if (invoice[0] && invoice[0].invoiceNumber) {
|
||||
await db.insert(deletedInvoices).values({
|
||||
userId: invoice[0].userId,
|
||||
invoiceNumber: invoice[0].invoiceNumber,
|
||||
totalAmount: invoice[0].totalAmount ?? undefined,
|
||||
supplierName: invoice[0].supplierName ?? undefined,
|
||||
}).onDuplicateKeyUpdate({ set: { deletedAt: new Date() } });
|
||||
const blacklistEntry = await db.select({ id: deletedInvoices.id })
|
||||
.from(deletedInvoices)
|
||||
.where(and(
|
||||
eq(deletedInvoices.userId, invoice[0].userId),
|
||||
eq(deletedInvoices.invoiceNumber, invoice[0].invoiceNumber),
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (blacklistEntry[0]) {
|
||||
await db.update(deletedInvoices)
|
||||
.set({ deletedAt: new Date() })
|
||||
.where(eq(deletedInvoices.id, blacklistEntry[0].id));
|
||||
} else {
|
||||
await db.insert(deletedInvoices).values({
|
||||
userId: invoice[0].userId,
|
||||
invoiceNumber: invoice[0].invoiceNumber,
|
||||
totalAmount: invoice[0].totalAmount ?? undefined,
|
||||
supplierName: invoice[0].supplierName ?? undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
await db.delete(invoices).where(eq(invoices.id, id));
|
||||
}
|
||||
|
||||
30
server/invoiceDeletion.test.ts
Normal file
30
server/invoiceDeletion.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { INVOICE_DELETE_ERROR, getSafeInvoiceDeletionError } from "./invoiceDeletionError";
|
||||
|
||||
const migrationPath = resolve(process.cwd(), "drizzle/0042_deleted_invoices_blacklist.sql");
|
||||
const dbPath = resolve(process.cwd(), "server/db.ts");
|
||||
|
||||
describe("suppression de facture", () => {
|
||||
it("ne renvoie jamais les détails SQL au navigateur", () => {
|
||||
const error = new Error("Failed query: insert into `deletedInvoices` values (?, ?, ?)");
|
||||
|
||||
expect(getSafeInvoiceDeletionError(error)).toBe(INVOICE_DELETE_ERROR);
|
||||
});
|
||||
|
||||
it("crée la blacklist de manière additive et idempotente", () => {
|
||||
const migration = readFileSync(migrationPath, "utf8");
|
||||
|
||||
expect(migration).toContain("CREATE TABLE IF NOT EXISTS `deletedInvoices`");
|
||||
expect(migration).toContain("CONSTRAINT `deletedInvoices_id` PRIMARY KEY(`id`)");
|
||||
});
|
||||
|
||||
it("met à jour une entrée existante sans exiger de contrainte unique historique", () => {
|
||||
const source = readFileSync(dbPath, "utf8");
|
||||
|
||||
expect(source).toContain("const blacklistEntry = await db.select");
|
||||
expect(source).toContain("await db.update(deletedInvoices)");
|
||||
expect(source).not.toContain("}).onDuplicateKeyUpdate({ set: { deletedAt: new Date() } });");
|
||||
});
|
||||
});
|
||||
10
server/invoiceDeletionError.ts
Normal file
10
server/invoiceDeletionError.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/** Message utilisateur sans détails de requête ni données de facture. */
|
||||
export const INVOICE_DELETE_ERROR = "La facture ne peut pas être supprimée pour le moment. Réessayez dans quelques instants.";
|
||||
|
||||
/**
|
||||
* Les erreurs de persistance peuvent contenir une requête SQL et des paramètres.
|
||||
* Ils restent réservés aux journaux serveur et ne doivent jamais atteindre le navigateur.
|
||||
*/
|
||||
export function getSafeInvoiceDeletionError(_error: unknown): string {
|
||||
return INVOICE_DELETE_ERROR;
|
||||
}
|
||||
@@ -110,6 +110,7 @@ import { resolveBapExportDestination } from "./exportDestinationResolver";
|
||||
import { createEmailImportAccountController, startEmailImportService, stopEmailImportService, isEmailImportServiceRunning, triggerEmailCheck, triggerManualEmailCheck, testImapConnection } from "./emailImportService";
|
||||
import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService";
|
||||
import { getSafeWebImportSourceError, logWebImportSourceSaveFailure } from "./webImportSourcesError";
|
||||
import { getSafeInvoiceDeletionError } from "./invoiceDeletionError";
|
||||
import { getManualWebImportAvailability, runManualWebImport } from "./manualWebImport";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { processFreeproExcel } from "./freeproService";
|
||||
@@ -509,7 +510,18 @@ export const appRouter = router({
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
await deleteInvoice(input.id);
|
||||
try {
|
||||
await deleteInvoice(input.id);
|
||||
} catch (error) {
|
||||
console.error("[InvoiceDelete] Archive or deletion failed", {
|
||||
invoiceId: input.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: getSafeInvoiceDeletionError(error),
|
||||
});
|
||||
}
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user