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 <number>_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/<number>.pdf and overwrites (same model as the offers manager), and cleanIssued also sweeps stale <number>_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 <noreply@anthropic.com>
This commit is contained in:
@@ -53,4 +53,29 @@ describe("NasDocumentManager issued-PDF round-trip", () => {
|
|||||||
blank.saveIssuedPdf("25P0003", 2026, 6, Buffer.from("x")),
|
blank.saveIssuedPdf("25P0003", 2026, 6, Buffer.from("x")),
|
||||||
).toBeNull();
|
).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"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,10 +52,20 @@ export class NasDocumentManager {
|
|||||||
|
|
||||||
// ── Created (issued) documents ───────────────────────────────────
|
// ── 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 `<number>_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 {
|
cleanIssued(documentNumber: string): void {
|
||||||
if (!this.basePath) return;
|
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);
|
const issuedDir = path.join(this.basePath, DIR_ISSUED);
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(issuedDir)) return;
|
if (!fs.existsSync(issuedDir)) return;
|
||||||
@@ -74,9 +84,10 @@ export class NasDocumentManager {
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const filePath = path.join(monthPath, safeName);
|
for (const file of fs.readdirSync(monthPath)) {
|
||||||
if (fs.existsSync(filePath)) {
|
if (stalePattern.test(file)) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(path.join(monthPath, file));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,12 +114,17 @@ export class NasDocumentManager {
|
|||||||
const dir = this.ensureDir(DIR_ISSUED, year, month);
|
const dir = this.ensureDir(DIR_ISSUED, year, month);
|
||||||
if (!dir) return null;
|
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 `<number>_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 safeName = this.sanitizeFilename(documentNumber) + ".pdf";
|
||||||
const fullPath = this.uniquePath(dir, safeName);
|
const fullPath = path.join(dir, safeName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(fullPath, pdfBuffer);
|
fs.writeFileSync(fullPath, pdfBuffer);
|
||||||
return `${DIR_ISSUED}/${year}/${this.pad(month)}/${path.basename(fullPath)}`;
|
return `${DIR_ISSUED}/${year}/${this.pad(month)}/${safeName}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
"[nas-financials-manager] write failed for issued document:",
|
"[nas-financials-manager] write failed for issued document:",
|
||||||
|
|||||||
Reference in New Issue
Block a user