Rebuild the Classic template on the engine with A4/Letter and a template harness
Classic now renders through buildRenderModel, computeFrame, the template and renderCore, with S.No and HSN/SAC columns, GST slots only through checked Slot components, taxable value, reverse-charge line, cancelled tag, continuation band and Page n of N. Letter absorbs its height deficit in elastic gaps. A harness runs 15 fixtures on A4 and Letter plus an item sweep and asserts zero audit errors, repeated headers, no split blocks, rupee extraction and deterministic output. The legacy InvoiceDocument is removed.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const TEMPLATES_DIR = path.join(ROOT, "src/pdf/templates");
|
||||
|
||||
/**
|
||||
* Model fields that carry regulated data. Templates must not read them: the data may leave the model
|
||||
* only through the Slot components, so the contract test can see every place it is printed.
|
||||
*/
|
||||
const REGULATED = [
|
||||
/\.gstin\b/,
|
||||
/\.showGstin\b/,
|
||||
/\.posLabel\b/,
|
||||
/\.hsnSac\b/,
|
||||
/\.taxRows\b/,
|
||||
/\.amountInWords\b/,
|
||||
/\.reverseCharge\b/,
|
||||
/\.signature\b/,
|
||||
/\.slots\b/,
|
||||
];
|
||||
|
||||
function sources(dir: string): string[] {
|
||||
return readdirSync(dir).flatMap((name) => {
|
||||
const full = path.join(dir, name);
|
||||
if (statSync(full).isDirectory()) return sources(full);
|
||||
return /\.(ts|tsx)$/.test(name) && !/\.test\.|harness\.ts$/.test(name) ? [full] : [];
|
||||
});
|
||||
}
|
||||
|
||||
describe("templates read regulated data only through Slot.*", () => {
|
||||
it("scans the classic template", () => {
|
||||
expect(sources(TEMPLATES_DIR).some((f) => f.endsWith("classic/Layout.tsx"))).toBe(true);
|
||||
});
|
||||
|
||||
it("finds no direct access to the regulated model fields", () => {
|
||||
const hits: string[] = [];
|
||||
for (const file of sources(TEMPLATES_DIR)) {
|
||||
readFileSync(file, "utf8")
|
||||
.split("\n")
|
||||
.forEach((line, i) => {
|
||||
if (/^\s*(\/\/|\*|\/\*)/.test(line)) return;
|
||||
// The slot API itself is the sanctioned way in: Slot.TaxRows, slotHeights.amountInWords, ...
|
||||
const code = line.replace(/\b(Slot|slotHeights)\.\w+/g, "");
|
||||
for (const re of REGULATED) if (re.test(code)) hits.push(`${path.relative(ROOT, file)}:${i + 1} ${line.trim()}`);
|
||||
});
|
||||
}
|
||||
expect(hits).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user