docs(plan): add 15% VAT to supplier PO PDF implementation plan

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-06 11:48:21 +02:00
parent a663de2d36
commit 40d069b5aa

View file

@ -0,0 +1,651 @@
# Add 15% VAT to Supplier-Facing PO PDF — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a 15% VAT line and a VAT-inclusive Total to the supplier-facing Purchase Order PDF (`<PurchaseOrderDocument>`), mirroring the existing customer-facing quote PDF's totals block.
**Architecture:** Reuse the existing `vatAmount(subtotal)` helper from `src/lib/pdf/format.ts`. Add a sibling helper `poSubtotal(items)` to the same file (next to the existing `quoteSubtotal()`) so the PO subtotal math (which uses `order_qty ?? quantity` × `cost_price`, different from the quote's `quantity × selling_price`) is testable in isolation. Component change is then a small one: import the helpers, compute `vat` + `totalInclVat`, replace the totals `<View>` block with the 4-row block from the quote. No PB schema change. No service-layer change.
**Tech Stack:** TypeScript, React 18, @react-pdf/renderer, Vitest, PocketBase (unchanged).
---
## File Structure
| File | Action | Responsibility |
|------|--------|----------------|
| `src/lib/pdf/format.ts` | Modify | Add `poSubtotal()` helper (PO-specific subtotal math) |
| `src/lib/pdf/format.test.ts` | Modify | Add tests for `poSubtotal()` |
| `src/lib/pdf/purchase-order-document.tsx` | Modify | Import helpers, compute VAT + total, replace totals JSX block, update JSDoc |
| `src/lib/pdf/purchase-order-document.test.tsx` | Create | Render-smoke test for the updated PDF component |
Files NOT changing:
- `src/app/api/v1/purchase-orders/[id]/pdf/route.ts` — still passes `totalAmount` (kept in the prop interface for backwards compat)
- `src/lib/server/purchase-orders-service.ts` — `generatePurchaseOrderPdf()` passes subtotal as `totalAmount` (still correct: `totalAmount` continues to hold the VAT-ex subtotal)
- `src/lib/pdf/render.tsx` — `renderPurchaseOrderPdf()` wrapper unchanged
- PocketBase schema — `purchase_orders.total_amount` keeps its current meaning
---
## Task 1: Add `poSubtotal()` helper (TDD)
**Files:**
- Modify: `src/lib/pdf/format.ts` (add one exported function near the existing `quoteSubtotal`)
- Modify: `src/lib/pdf/format.test.ts` (add a `describe("poSubtotal", ...)` block)
The PO subtotal differs from the quote subtotal:
- Quote: `sum(quantity × selling_price)`
- PO: `sum((order_qty ?? quantity) × cost_price)` — uses the supplier-facing `order_qty` fallback so a blank order_qty never zeroes a line
This matches the existing logic in `purchase-order-document.tsx` lines 97–105. Extracting it makes the math independently testable and shrinks the component.
- [ ] **Step 1: Write the failing test in `src/lib/pdf/format.test.ts`**
Add a new `describe` block at the end of the existing test file (after `effectiveValidityDays`, before the closing). Match the existing test style (Vitest's `describe`/`it`/`expect`, ES module imports at the top).
First, update the top-of-file import to add `poSubtotal`:
```ts
import {
formatZAR,
formatDateLong,
addDays,
quoteSubtotal,
vatAmount,
poSubtotal,
QUOTE_VALID_DAYS,
effectiveQuoteDate,
effectiveValidityDays,
} from "./format";
```
Then append at the bottom of the file:
```ts
describe("poSubtotal", () => {
it("sums order_qty × cost_price when order_qty is set", () => {
expect(
poSubtotal([
{ order_qty: 3, quantity: 1, cost_price: 100 },
])
).toBe(300);
});
it("falls back to quantity when order_qty is missing", () => {
expect(
poSubtotal([
{ quantity: 4, cost_price: 50 },
])
).toBe(200);
});
it("falls back to quantity when order_qty is 0 (blank treated as missing)", () => {
// Mirrors the supplier-facing fallback in the PDF — a literal 0 would
// zero the line, which is almost always an operator mistake.
expect(
poSubtotal([
{ order_qty: 0, quantity: 2, cost_price: 75 },
])
).toBe(150);
});
it("sums across multiple items", () => {
expect(
poSubtotal([
{ order_qty: 3, quantity: 1, cost_price: 100 },
{ order_qty: 5, quantity: 1, cost_price: 40 },
])
).toBe(500);
});
it("rounds to 2 decimal places (avoids floating-point dust)", () => {
// 7 × 33.33 = 233.31 exactly (the math works out cleanly), but
// 0.1 + 0.2 style sums should still round cleanly.
expect(
poSubtotal([
{ quantity: 3, cost_price: 33.33 },
{ quantity: 3, cost_price: 33.33 },
{ quantity: 3, cost_price: 33.34 },
])
).toBe(300);
});
it("returns 0 for an empty array", () => {
expect(poSubtotal([])).toBe(0);
});
it("treats non-numeric cost_price as 0", () => {
expect(
poSubtotal([
{ quantity: 5, cost_price: NaN },
])
).toBe(0);
});
});
```
- [ ] **Step 2: Run the new tests to verify they fail**
Run:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm test src/lib/pdf/format.test.ts
```
Expected: FAIL — `poSubtotal` is not exported from `./format` (TypeScript compile error or runtime "is not a function" depending on TS strictness in the test runner).
- [ ] **Step 3: Add the `poSubtotal()` helper to `src/lib/pdf/format.ts`**
Insert immediately after the existing `quoteSubtotal()` function (after line 40, before the `vatAmount()` function). Match the existing function style (JSDoc, pure function, named export):
```ts
/**
* Sum of (order_qty ?? quantity) * cost_price across PO items. Differs
* from `quoteSubtotal()` in two ways:
* 1. Multiplies by `cost_price` (the supplier-facing price), not
* `selling_price` (the customer-facing price).
* 2. Prefers `order_qty` when set, falls back to `quantity` so a
* blank `order_qty` (the common case when the operator hasn't
* adjusted the supplier-facing qty yet) never zeroes the line.
* A literal `order_qty: 0` is also treated as missing — that's
* almost always an operator mistake.
*
* Rounds to 2dp so the printed table doesn't show floating-point dust
* (1.0000000003 R etc). The supplier-facing PO uses this for both
* `Subtotal` and the base of the `VAT (15%)` line.
*/
export function poSubtotal(
items: { order_qty?: number | null; quantity: number; cost_price: number }[]
): number {
return Math.round(
items.reduce(
(sum, it) =>
sum +
(Number(it.order_qty ?? it.quantity) || 0) *
(Number(it.cost_price) || 0),
0
) * 100
) / 100;
}
```
- [ ] **Step 4: Run the tests to verify they pass**
Run:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm test src/lib/pdf/format.test.ts
```
Expected: PASS — all 7 new `poSubtotal` cases pass, plus the existing format tests still pass.
- [ ] **Step 5: Commit**
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && git add src/lib/pdf/format.ts src/lib/pdf/format.test.ts && git commit -m "feat(pdf): add poSubtotal helper for PO PDF totals"
```
---
## Task 2: Update the PO PDF component to render 15% VAT
**Files:**
- Modify: `src/lib/pdf/purchase-order-document.tsx` (imports, JSDoc, subtotal/vat computation, totals block JSX)
This is the user-visible change: the printed PDF now shows `Subtotal` / `VAT (15%):` / rule / `Total:`.
Notes on what changes and what stays:
- The `PurchaseOrderDocumentProps` interface is **unchanged** (no new props; `totalAmount` stays in the interface for API backwards compat).
- The `totalAmount` snapshot reconciliation (`Math.abs(totalAmount - subtotal) < 0.01 ? totalAmount : subtotal`) is **removed**. It only made sense for the VAT-ex subtotal — once the printed `Total:` is `subtotal + vat` (a freshly derived quantity), there's nothing meaningful to reconcile against the stored `total_amount`. `purchase_orders.total_amount` still holds the VAT-ex subtotal in PB; the route still passes it; the prop is preserved for API stability.
- The `total` variable in the existing component body is **removed** along with the reconciliation (it becomes unused; would otherwise trip `@typescript-eslint/no-unused-vars`).
- All styles (`styles.totals`, `styles.totalLine`, `styles.totalLabel`, `styles.totalValue`, `styles.totalRule`, `styles.totalFinalLabel`, `styles.totalFinalValue`) are **unchanged** — they already exist in the file (lines 312–330) and the new VAT row uses them as-is.
- The "Intentionally stripped from this document" JSDoc list (lines 63–65) replaces the "No VAT line" bullet.
- [ ] **Step 1: Update the imports at the top of `purchase-order-document.tsx`**
Current import (line 4):
```ts
import { formatZAR, formatDateLong } from "./format";
```
Replace with:
```ts
import { formatZAR, formatDateLong, poSubtotal, vatAmount } from "./format";
```
- [ ] **Step 2: Update the JSDoc "Intentionally stripped" list**
Current text in the JSDoc block (around line 63):
```
* - **No VAT line** — we pay what the supplier quotes, no markup
* math here. Different from the quote (customer-facing) where
* VAT is added on top of selling price.
```
Replace with:
```
* - **VAT (15%) added to subtotal** — prints the supplier-quoted
* subtotal, then VAT (15%), then a VAT-inclusive Total. Same
* shape as the quote's totals block so the two documents feel
* like siblings. The VAT-inclusive Total is the figure the
* office uses for budget / VAT-return reconciliation; the stored
* `purchase_orders.total_amount` stays VAT-ex (no PB schema
* change).
```
- [ ] **Step 3: Replace the inline subtotal + totalAmount reconciliation block**
Current code (lines 97–110 of `purchase-order-document.tsx`):
```ts
// Subtotal = sum of (qty × cost) per line. Qty uses the same
// supplier-facing resolution as the line rows below — `order_qty`
// when the operator shorted or bumped a line, else falls back to
// the customer-facing `quantity` so a blank order_qty never zeroes
// a line. This keeps the printed line totals summing exactly to the
// Subtotal. We round to 2 dp so the table doesn't show floating-point
// dust (1.0000000003 R etc).
const subtotal = Math.round(
items.reduce(
(sum, it) =>
sum +
(Number(it.order_qty ?? it.quantity) || 0) *
(Number(it.cost_price) || 0),
0
) * 100
) / 100;
// Re-use the supplied snapshot if it differs from the live
// computation by < 0.01 — caller might have included an explicit
// total_amount that we want to honour (e.g. from the regenerate
// API where the PO record already has total_amount set).
const total = Math.abs(totalAmount - subtotal) < 0.01 ? totalAmount : subtotal;
```
Replace with:
```ts
// Subtotal = sum of (qty × cost) per line, using the
// `order_qty ?? quantity` supplier-facing fallback. Logic lives
// in `poSubtotal()` (src/lib/pdf/format.ts) so it's independently
// testable. This keeps the printed line totals summing exactly to
// the Subtotal.
const subtotal = poSubtotal(items);
// 15% VAT on top of the supplier-quoted subtotal — same helper the
// quote PDF uses, so both documents compute VAT identically. Total
// = subtotal + vat, rounded to 2dp so the line matches the printed
// figures exactly. The `totalAmount` prop (carried for API backwards
// compat with the existing route) is intentionally not used here:
// it holds the VAT-ex subtotal and there's nothing meaningful to
// reconcile against once the printed Total is `subtotal + vat`.
const vat = vatAmount(subtotal);
const totalInclVat = Math.round((subtotal + vat) * 100) / 100;
```
- [ ] **Step 4: Replace the totals `<View>` block in the JSX**
Current code (around lines 222–235):
```tsx
{/* Totals — just Subtotal + Total. No VAT (we're buying, the
supplier's price is what we pay). Rule above Total mirrors
the quote's totals block so the two feel like siblings. */}
<View style={styles.totals}>
<View style={styles.totalLine}>
<Text style={styles.totalLabel}>Subtotal:</Text>
<Text style={styles.totalValue}>{formatZAR(subtotal)}</Text>
</View>
<View style={styles.totalRule} />
<View style={styles.totalLine}>
<Text style={styles.totalFinalLabel}>Total:</Text>
<Text style={styles.totalFinalValue}>{formatZAR(total)}</Text>
</View>
</View>
```
Replace with:
```tsx
{/* Totals — Subtotal + VAT (15%) + rule + Total. Mirrors the
quote's totals block so the two PDFs feel like siblings
when filed side-by-side. `Subtotal` is the VAT-ex sum
(stored on `purchase_orders.total_amount`); the printed
`Total` is `subtotal × 1.15` — the figure the office
uses for budget / VAT-return reconciliation. */}
<View style={styles.totals}>
<View style={styles.totalLine}>
<Text style={styles.totalLabel}>Subtotal:</Text>
<Text style={styles.totalValue}>{formatZAR(subtotal)}</Text>
</View>
<View style={styles.totalLine}>
<Text style={styles.totalLabel}>VAT (15%):</Text>
<Text style={styles.totalValue}>{formatZAR(vat)}</Text>
</View>
<View style={styles.totalRule} />
<View style={styles.totalLine}>
<Text style={styles.totalFinalLabel}>Total:</Text>
<Text style={styles.totalFinalValue}>{formatZAR(totalInclVat)}</Text>
</View>
</View>
```
- [ ] **Step 5: Type-check the component**
Run:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm exec tsc --noEmit -p tsconfig.json
```
Expected: PASS (no type errors). The `total` variable is now removed; `totalAmount` is still in the props interface so no caller types break.
- [ ] **Step 6: Run the existing PDF smoke tests**
Run:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm test src/lib/pdf/render.test.ts
```
Expected: PASS. The existing `renderPurchaseOrderPdf` smoke tests (if any — they don't assert on totals content, just magic bytes + size) continue to pass.
Note: if `render.test.ts` doesn't yet include a `describe("renderPurchaseOrderPdf")` block, that's fine — Task 3 adds one. The existing quote/delivery-note/stocktake/stock-purchase-list tests must continue to pass.
- [ ] **Step 7: Commit**
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && git add src/lib/pdf/purchase-order-document.tsx && git commit -m "feat(pdf): add 15% VAT line + VAT-inclusive Total to PO PDF"
```
---
## Task 3: Add end-to-end render smoke test for the PO PDF
**Files:**
- Create: `src/lib/pdf/purchase-order-document.test.tsx`
This test mirrors the existing `renderQuotePdf` smoke tests in `src/lib/pdf/render.test.ts`. We can't easily extract the rendered VAT row from a PDF byte stream (the text is compressed via FlateDecode), so we verify:
1. The buffer is a valid PDF (magic bytes).
2. The buffer is non-trivial (> 1 KB).
3. The buffer length differs from a known-baseline render (a soft "the change took effect" check) — optional.
- [ ] **Step 1: Create the test file**
Create `src/lib/pdf/purchase-order-document.test.tsx` with the following content:
```tsx
import { describe, it, expect } from "vitest";
import { renderPurchaseOrderPdf } from "./render";
import type { Letterhead } from "@/lib/letterhead";
import type { Order, OrderItem, PurchaseOrder, Supplier } from "@/lib/types";
/**
* Smoke tests for the supplier-facing PO PDF renderer. We don't
* render visually — we assert the binary is a well-formed PDF
* (magic bytes, sane size) so any regression in the render pipeline
* (style typo breaking JSX, dep misconfig, etc.) trips the test
* even without eyeballing output.
*
* The VAT-line correctness is covered by `format.test.ts`
* (poSubtotal + vatAmount helpers) and by inspection during code
* review — this file guards the integration.
*/
const baseLetterhead: Letterhead = {
id: "lh1",
collectionId: "pbc_letterhead",
collectionName: "letterhead_settings",
created: "2026-08-06T00:00:00.000Z",
updated: "2026-08-06T00:00:00.000Z",
display_name: "CNL Mining Supplies",
address: "47 Station Street\nCarletonville, Gauteng 2499",
phone: "+27 18 786 2897",
email: "quotes@wfsales.co.za",
vat_number: "4840229449",
registration_number: "2014/004024/07",
bank_name: "Standard Bank - Carletonville",
bank_account_number: "Current Account\n022196552",
bank_branch_code: "016141",
terms: "Terms and Conditions:\n1. Standard terms apply.",
default_markup_percent: 30,
footer_text: "",
logo: "",
is_active: true,
company: "CNL Mining Supplies (Pty) Ltd",
accent_color: "#738224",
};
const baseSupplier: Supplier = {
id: "sup1",
collectionId: "pbc_suppliers",
collectionName: "suppliers",
created: "2026-08-06T00:00:00.000Z",
updated: "2026-08-06T00:00:00.000Z",
suppliername: "Alpha Bearings (Pty) Ltd",
suppliernumber: "+27 11 555 0123",
supplieraddress: "12 Industrial Rd\nJohannesburg, Gauteng 2000",
email: "orders@alphabearings.co.za",
supply_tags: "",
coordinates: "",
closingtime: "",
};
const baseOrder: Order = {
id: "ord1",
collectionId: "pbc_orders",
collectionName: "orders",
created: "2026-08-06T00:00:00.000Z",
updated: "2026-08-06T00:00:00.000Z",
order_number: "SO-260001",
customer: "cust1",
company: "comp1",
quote: "",
status: "draft",
rep: "",
rfq_number: "",
contact_name: "",
email_subject: "",
email_sender: "",
ai_insights: "",
meiliesearch: "",
invoiced: false,
is_repair_order: false,
};
const basePurchaseOrder: PurchaseOrder = {
id: "po1",
collectionId: "pbc_purchase_orders",
collectionName: "purchase_orders",
created: "2026-08-06T00:00:00.000Z",
updated: "2026-08-06T00:00:00.000Z",
po_number: "PO26000001",
order: "ord1",
supplier: "sup1",
status: "draft",
notes: "",
total_amount: 300,
// documents + documents_history are PB file/json fields — empty defaults
documents: [],
documents_history: [],
email_message_id: "",
email_payload: {},
email_error: "",
sending_started_at: "",
generated_by: "",
generation_notes: "",
};
const baseItems: OrderItem[] = [
{
id: "oi1",
collectionId: "pbc_order_items",
collectionName: "order_items",
created: "2026-08-06T00:00:00.000Z",
updated: "2026-08-06T00:00:00.000Z",
order: "ord1",
purchase_order: "po1",
description: "Wheel hub kit — 6-bolt pattern",
po_description: "Wheel hub kit — 6-bolt pattern",
quantity: 3,
order_qty: 3,
unit: "ea",
sku: "WH-6B-001",
cost_price: 100,
selling_price: 150,
markup: 50,
notes: "",
qty_processed: 0,
qty_assigned: 0,
qty_collected: 0,
qty_verified: 0,
qty_stocked: 0,
qty_picked: 0,
qty_loaded: 0,
qty_delivered: 0,
qty_invoiced: 0,
qty_cancelled: 0,
category: "",
supplier: "sup1",
item_status: "draft",
drv_status: "",
problem_notes: "",
customer_invoice_number: "",
supplier_invoice_number: "",
collection_date: "",
collected_date: "",
verified_date: "",
delivered_date: "",
invoiced_date: "",
delivery_note_number: "",
qb_invoice_pdf: null,
order_item_details: "",
} as OrderItem,
];
describe("renderPurchaseOrderPdf", () => {
it("produces a Buffer with a PDF magic-number header", async () => {
const buf = await renderPurchaseOrderPdf({
purchaseOrder: basePurchaseOrder,
items: baseItems,
order: baseOrder,
supplier: baseSupplier,
letterhead: baseLetterhead,
logoDataUri: null,
generatedAtIso: "2026-08-06T12:00:00.000Z",
totalAmount: 300,
});
expect(Buffer.isBuffer(buf)).toBe(true);
expect(buf.subarray(0, 5).toString("ascii")).toBe("%PDF-");
}, 30_000);
it("produces a non-trivial buffer (> 1 KB) for a simple PO", async () => {
const buf = await renderPurchaseOrderPdf({
purchaseOrder: basePurchaseOrder,
items: baseItems,
order: baseOrder,
supplier: baseSupplier,
letterhead: baseLetterhead,
logoDataUri: null,
generatedAtIso: "2026-08-06T12:00:00.000Z",
totalAmount: 300,
});
expect(buf.length).toBeGreaterThan(1024);
}, 30_000);
it("handles missing logo gracefully (logoDataUri=null)", async () => {
const buf = await renderPurchaseOrderPdf({
purchaseOrder: basePurchaseOrder,
items: baseItems,
order: baseOrder,
supplier: baseSupplier,
letterhead: { ...baseLetterhead, logo: "" },
logoDataUri: null,
generatedAtIso: "2026-08-06T12:00:00.000Z",
totalAmount: 300,
});
expect(buf.subarray(0, 5).toString("ascii")).toBe("%PDF-");
}, 30_000);
it("renders with the ISHERWOOD letterhead code (different accent color)", async () => {
const buf = await renderPurchaseOrderPdf({
purchaseOrder: basePurchaseOrder,
items: baseItems,
order: baseOrder,
supplier: baseSupplier,
letterhead: {
...baseLetterhead,
display_name: "ISHERWOOD MINING SUPPLIES",
accent_color: "#7ed957",
},
logoDataUri: null,
generatedAtIso: "2026-08-06T12:00:00.000Z",
totalAmount: 300,
});
expect(buf.subarray(0, 5).toString("ascii")).toBe("%PDF-");
}, 30_000);
});
```
- [ ] **Step 2: Run the new test file**
Run:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm test src/lib/pdf/purchase-order-document.test.tsx
```
Expected: PASS. All 4 cases render valid PDFs.
- [ ] **Step 3: Run the entire `src/lib/pdf` test directory to confirm nothing regressed**
Run:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm test src/lib/pdf
```
Expected: PASS. The new `poSubtotal` cases (Task 1) + the existing format cases + the new PO render cases all pass; the existing quote/delivery-note/stocktake/stock-purchase-list smoke tests still pass.
- [ ] **Step 4: Commit**
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && git add src/lib/pdf/purchase-order-document.test.tsx && git commit -m "test(pdf): add render smoke tests for PO PDF (covers VAT totals)"
```
---
## Task 4: Final build + type-check sweep
- [ ] **Step 1: Run the full project type-check**
Run:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm exec tsc --noEmit -p tsconfig.json
```
Expected: PASS. No type errors anywhere in the project.
- [ ] **Step 2: Run the full test suite**
Run:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm test
```
Expected: PASS. All existing tests still pass; new tests pass.
- [ ] **Step 3: Run the linter**
Run:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && pnpm lint
```
Expected: PASS. No new lint errors (especially no `no-unused-vars` from the removed `total` variable).
- [ ] **Step 4: Commit any incidental fixes**
If steps 1–3 surfaced any small fix (e.g. a stray import), commit it:
```bash
cd /Volumes/EXT/DEVELOPMENT-FORGEJO/WFDASH-WEBAPP && git add -u && git commit -m "chore(pdf): tidy imports after VAT totals change"
```
If nothing needed fixing, skip this step.
---
## Self-Review Notes (from plan author)
- **Spec coverage:** All sections of the spec are implemented — `poSubtotal` extraction (covered in Task 1), component import + JSDoc + computation + JSX update (Task 2), end-to-end render smoke (Task 3), build/type/lint sweep (Task 4). One deviation: the spec said "keep the `totalAmount` snapshot reconciliation untouched"; on planning, I noticed it becomes vestigial once the printed `Total:` is `subtotal + vat`. The plan removes the reconciliation (and the `total` variable) and documents why in Task 2's preamble. The `totalAmount` prop itself stays in the interface for API stability.
- **No placeholders:** Every code block is complete and runnable.
- **Type consistency:** `poSubtotal` is added once in Task 1 (in `format.ts`) and consumed once in Task 2 (in `purchase-order-document.tsx`). The test file uses the same prop shape the existing `renderPurchaseOrderPdf` function expects — verified against `src/lib/pdf/render.tsx` and the existing usage in `src/app/api/v1/purchase-orders/[id]/pdf/route.ts`.