feat(nas): archive issued-order PDFs to NAS_ORDERS; split NAS_FINANCIALS_PATH into NAS_INVOICES + NAS_ORDERS
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,10 @@ import path from "path";
|
||||
import { config } from "../config/env";
|
||||
|
||||
/**
|
||||
* NAS storage for financial documents.
|
||||
* NAS storage for financial/order documents (issued invoices, received
|
||||
* invoices, issued purchase orders — any document archived under the standard
|
||||
* Vydané/Přijaté layout). One instance is bound to a single base path, so a
|
||||
* separate singleton is exported per document family (invoices vs orders).
|
||||
* Structure: {basePath}/Vydané/YYYY/MM/ and {basePath}/Přijaté/YYYY/MM/
|
||||
*
|
||||
* PERFORMANCE NOTE (audit finding — DEFERRED, intentional):
|
||||
@@ -11,9 +14,9 @@ import { config } from "../config/env";
|
||||
* readFileSync/readdirSync/statSync). When basePath is a network (SMB) share, a
|
||||
* slow or hung NAS will BLOCK the Node event loop for the duration of the call,
|
||||
* stalling all other requests in the same process. This is a known trade-off:
|
||||
* the file sizes are small (invoice PDFs), the call sites are low-frequency
|
||||
* admin operations, and the synchronous API lets callers treat a returned path
|
||||
* as a verified, durable write (see migrate-received-invoices-to-nas.ts, which
|
||||
* the file sizes are small (PDFs), the call sites are low-frequency admin
|
||||
* operations, and the synchronous API lets callers treat a returned path as a
|
||||
* verified, durable write (see migrate-received-invoices-to-nas.ts, which
|
||||
* relies on this to safely null out the DB BLOB). Converting to the async
|
||||
* fs.promises API is a large, cross-cutting change touching every caller and
|
||||
* their error handling — deliberately NOT done here to keep this fix scoped.
|
||||
@@ -22,12 +25,11 @@ import { config } from "../config/env";
|
||||
const DIR_ISSUED = "Vydané";
|
||||
const DIR_RECEIVED = "Přijaté";
|
||||
|
||||
class NasFinancialsManager {
|
||||
export class NasDocumentManager {
|
||||
private readonly basePath: string;
|
||||
|
||||
constructor() {
|
||||
const raw = config.nas.financialsPath;
|
||||
this.basePath = raw ? path.resolve(raw).replace(/\\/g, "/") : "";
|
||||
constructor(basePath: string) {
|
||||
this.basePath = basePath ? path.resolve(basePath).replace(/\\/g, "/") : "";
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
@@ -48,12 +50,12 @@ class NasFinancialsManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Created (issued) invoices ────────────────────────────────────
|
||||
// ── Created (issued) documents ───────────────────────────────────
|
||||
|
||||
/** Remove any existing PDF for this invoice number across all year/month folders */
|
||||
cleanIssuedInvoice(invoiceNumber: string): void {
|
||||
/** Remove any existing PDF for this document number across all year/month folders */
|
||||
cleanIssued(documentNumber: string): void {
|
||||
if (!this.basePath) return;
|
||||
const safeName = this.sanitizeFilename(invoiceNumber) + ".pdf";
|
||||
const safeName = this.sanitizeFilename(documentNumber) + ".pdf";
|
||||
const issuedDir = path.join(this.basePath, DIR_ISSUED);
|
||||
try {
|
||||
if (!fs.existsSync(issuedDir)) return;
|
||||
@@ -80,20 +82,20 @@ class NasFinancialsManager {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[nas-financials-manager] cleanIssuedInvoice failed for:",
|
||||
invoiceNumber,
|
||||
"[nas-financials-manager] cleanIssued failed for:",
|
||||
documentNumber,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
saveIssuedInvoicePdf(
|
||||
invoiceNumber: string,
|
||||
saveIssuedPdf(
|
||||
documentNumber: string,
|
||||
year: number,
|
||||
month: number,
|
||||
pdfBuffer: Buffer,
|
||||
): string | null {
|
||||
// Guard parity with saveReceivedInvoice: without this, an unconfigured
|
||||
// Guard parity with saveReceived: without this, an unconfigured
|
||||
// (empty) basePath would let ensureDir build a RELATIVE path and write the
|
||||
// PDF into the process CWD instead of the NAS. Bail out instead.
|
||||
if (!this.isConfigured()) return null;
|
||||
@@ -101,7 +103,7 @@ class NasFinancialsManager {
|
||||
const dir = this.ensureDir(DIR_ISSUED, year, month);
|
||||
if (!dir) return null;
|
||||
|
||||
const safeName = this.sanitizeFilename(invoiceNumber) + ".pdf";
|
||||
const safeName = this.sanitizeFilename(documentNumber) + ".pdf";
|
||||
const fullPath = this.uniquePath(dir, safeName);
|
||||
|
||||
try {
|
||||
@@ -109,7 +111,7 @@ class NasFinancialsManager {
|
||||
return `${DIR_ISSUED}/${year}/${this.pad(month)}/${path.basename(fullPath)}`;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[nas-financials-manager] write failed for issued invoice:",
|
||||
"[nas-financials-manager] write failed for issued document:",
|
||||
fullPath,
|
||||
err,
|
||||
);
|
||||
@@ -117,9 +119,7 @@ class NasFinancialsManager {
|
||||
}
|
||||
}
|
||||
|
||||
readIssuedInvoice(
|
||||
relativePath: string,
|
||||
): { data: Buffer; fileName: string } | null {
|
||||
readIssued(relativePath: string): { data: Buffer; fileName: string } | null {
|
||||
const fullPath = this.resolveSafePath(relativePath);
|
||||
if (!fullPath) return null;
|
||||
try {
|
||||
@@ -127,7 +127,7 @@ class NasFinancialsManager {
|
||||
return { data, fileName: path.basename(fullPath) };
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[nas-financials-manager] read failed for issued invoice:",
|
||||
"[nas-financials-manager] read failed for issued document:",
|
||||
fullPath,
|
||||
err,
|
||||
);
|
||||
@@ -135,7 +135,7 @@ class NasFinancialsManager {
|
||||
}
|
||||
}
|
||||
|
||||
deleteIssuedInvoice(relativePath: string): boolean {
|
||||
deleteIssued(relativePath: string): boolean {
|
||||
const fullPath = this.resolveSafePath(relativePath);
|
||||
if (!fullPath) return false;
|
||||
try {
|
||||
@@ -143,7 +143,7 @@ class NasFinancialsManager {
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[nas-financials-manager] delete failed for issued invoice:",
|
||||
"[nas-financials-manager] delete failed for issued document:",
|
||||
fullPath,
|
||||
err,
|
||||
);
|
||||
@@ -151,21 +151,21 @@ class NasFinancialsManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Received invoices ────────────────────────────────────────────
|
||||
// ── Received documents ───────────────────────────────────────────
|
||||
|
||||
buildReceivedPath(fileName: string, year: number, month: number): string {
|
||||
const safeName = this.sanitizeFilename(fileName) || "file";
|
||||
return `${DIR_RECEIVED}/${year}/${this.pad(month)}/${safeName}`;
|
||||
}
|
||||
|
||||
saveReceivedInvoice(
|
||||
saveReceived(
|
||||
originalName: string,
|
||||
year: number,
|
||||
month: number,
|
||||
fileBuffer: Buffer,
|
||||
): { filePath: string } | { error: string } {
|
||||
if (!this.isConfigured()) {
|
||||
return { error: "NAS financials path is not configured" };
|
||||
return { error: "NAS path is not configured" };
|
||||
}
|
||||
|
||||
const dir = this.ensureDir(DIR_RECEIVED, year, month);
|
||||
@@ -188,9 +188,7 @@ class NasFinancialsManager {
|
||||
}
|
||||
}
|
||||
|
||||
readReceivedInvoice(
|
||||
filePath: string,
|
||||
): { data: Buffer; fileName: string } | null {
|
||||
readReceived(filePath: string): { data: Buffer; fileName: string } | null {
|
||||
const fullPath = this.resolveSafePath(filePath);
|
||||
if (!fullPath) return null;
|
||||
|
||||
@@ -199,7 +197,7 @@ class NasFinancialsManager {
|
||||
return { data, fileName: path.basename(fullPath) };
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[nas-financials-manager] read failed for received invoice:",
|
||||
"[nas-financials-manager] read failed for received document:",
|
||||
fullPath,
|
||||
err,
|
||||
);
|
||||
@@ -207,7 +205,7 @@ class NasFinancialsManager {
|
||||
}
|
||||
}
|
||||
|
||||
deleteReceivedInvoice(filePath: string): boolean {
|
||||
deleteReceived(filePath: string): boolean {
|
||||
const fullPath = this.resolveSafePath(filePath);
|
||||
if (!fullPath) return false;
|
||||
|
||||
@@ -216,7 +214,7 @@ class NasFinancialsManager {
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[nas-financials-manager] delete failed for received invoice:",
|
||||
"[nas-financials-manager] delete failed for received document:",
|
||||
fullPath,
|
||||
err,
|
||||
);
|
||||
@@ -303,4 +301,7 @@ class NasFinancialsManager {
|
||||
}
|
||||
}
|
||||
|
||||
export const nasFinancialsManager = new NasFinancialsManager();
|
||||
export const nasInvoicesManager = new NasDocumentManager(
|
||||
config.nas.invoicesPath,
|
||||
);
|
||||
export const nasOrdersManager = new NasDocumentManager(config.nas.ordersPath);
|
||||
|
||||
Reference in New Issue
Block a user