From dd8c699420f1355e2c7c235d27f3a7d7268378e8 Mon Sep 17 00:00:00 2001 From: BOHA Date: Wed, 10 Jun 2026 17:55:27 +0200 Subject: [PATCH] fix(nas): issued-order PDFs overwrite in place - no more _1 duplicate copies saveIssuedPdf ran through uniquePath, so whenever two archive calls raced (the fire-and-forget ?save=1 after a save vs. the /file fallback, or two quick saves) the second write landed as _1.pdf - and those copies were invisible to the reader and cleaner (exact-name match), accumulating forever. Now the issued archive writes to the deterministic Vydane/YYYY/MM/.pdf and overwrites (same model as the offers manager), and cleanIssued also sweeps stale _N.pdf duplicates so existing junk self-heals on the next save/delete. Document numbers are digit-only (never contain _), so the suffix match cannot hit a different document. uniquePath remains for received-invoice uploads where name collisions are legitimate. Co-Authored-By: Claude Fable 5 --- src/__tests__/nas-document-manager.test.ts | 25 ++++++++++++++++++ src/services/nas-financials-manager.ts | 30 +++++++++++++++++----- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/__tests__/nas-document-manager.test.ts b/src/__tests__/nas-document-manager.test.ts index 02b1ba0..6b975e5 100644 --- a/src/__tests__/nas-document-manager.test.ts +++ b/src/__tests__/nas-document-manager.test.ts @@ -53,4 +53,29 @@ describe("NasDocumentManager issued-PDF round-trip", () => { blank.saveIssuedPdf("25P0003", 2026, 6, Buffer.from("x")), ).toBeNull(); }); + + it("re-saving the same number OVERWRITES in place — never a _1 duplicate", () => { + const first = nas.saveIssuedPdf("25P0004", 2026, 6, Buffer.from("v1")); + const second = nas.saveIssuedPdf("25P0004", 2026, 6, Buffer.from("v2")); + + // Same canonical path both times, newest content wins. + expect(second).toBe(first); + const dir = path.join(tmpBase, "Vydané", "2026", "06"); + expect(fs.readdirSync(dir)).toEqual(["25P0004.pdf"]); + expect(nas.readIssued(second!)!.data.toString()).toBe("v2"); + }); + + it("cleanIssued also sweeps stale _N duplicates left by the old racy save", () => { + nas.saveIssuedPdf("25P0005", 2026, 6, Buffer.from("canonical")); + // Simulate duplicates the pre-fix uniquePath save created during races. + const dir = path.join(tmpBase, "Vydané", "2026", "06"); + fs.writeFileSync(path.join(dir, "25P0005_1.pdf"), "stale"); + fs.writeFileSync(path.join(dir, "25P0005_2.pdf"), "stale"); + // A DIFFERENT number must not be touched by the suffix match. + nas.saveIssuedPdf("25P0006", 2026, 6, Buffer.from("other")); + + nas.cleanIssued("25P0005"); + + expect(fs.readdirSync(dir).sort()).toEqual(["25P0006.pdf"]); + }); }); diff --git a/src/services/nas-financials-manager.ts b/src/services/nas-financials-manager.ts index 430d173..88e6951 100644 --- a/src/services/nas-financials-manager.ts +++ b/src/services/nas-financials-manager.ts @@ -52,10 +52,20 @@ export class NasDocumentManager { // ── Created (issued) documents ─────────────────────────────────── - /** Remove any existing PDF for this document number across all year/month folders */ + /** + * Remove any existing PDF for this document number across all year/month + * folders — including stale `_N.pdf` duplicates that the old + * uniquePath-based save left behind when two archive calls raced (the + * save-after-edit fire-and-forget vs. the /file fallback). Document + * numbers are digit-only and never contain "_", so the suffix match + * cannot hit a different document. + */ cleanIssued(documentNumber: string): void { if (!this.basePath) return; - const safeName = this.sanitizeFilename(documentNumber) + ".pdf"; + const safeBase = this.sanitizeFilename(documentNumber); + const stalePattern = new RegExp( + `^${safeBase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(_\\d+)?\\.pdf$`, + ); const issuedDir = path.join(this.basePath, DIR_ISSUED); try { if (!fs.existsSync(issuedDir)) return; @@ -74,9 +84,10 @@ export class NasDocumentManager { ); continue; } - const filePath = path.join(monthPath, safeName); - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); + for (const file of fs.readdirSync(monthPath)) { + if (stalePattern.test(file)) { + fs.unlinkSync(path.join(monthPath, file)); + } } } } @@ -103,12 +114,17 @@ export class NasDocumentManager { const dir = this.ensureDir(DIR_ISSUED, year, month); if (!dir) return null; + // Deterministic path, OVERWRITE in place (same model as the offers + // manager): the archive of a numbered document has exactly one canonical + // file. uniquePath here used to spawn `_1.pdf` duplicates + // whenever two archive calls raced — those are user-visible junk on the + // NAS and invisible to the reader/cleaner, which match the exact name. const safeName = this.sanitizeFilename(documentNumber) + ".pdf"; - const fullPath = this.uniquePath(dir, safeName); + const fullPath = path.join(dir, safeName); try { fs.writeFileSync(fullPath, pdfBuffer); - return `${DIR_ISSUED}/${year}/${this.pad(month)}/${path.basename(fullPath)}`; + return `${DIR_ISSUED}/${year}/${this.pad(month)}/${safeName}`; } catch (err) { console.error( "[nas-financials-manager] write failed for issued document:",