From 40d069b5aa41e347c055ffdfc033e03e9216d45b Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Thu, 6 Aug 2026 11:48:21 +0200 Subject: [PATCH] docs(plan): add 15% VAT to supplier PO PDF implementation plan Co-authored-by: Orca --- .../plans/2026-08-06-supplier-po-pdf-vat.md | 651 ++++++++++++++++++ 1 file changed, 651 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-06-supplier-po-pdf-vat.md diff --git a/docs/superpowers/plans/2026-08-06-supplier-po-pdf-vat.md b/docs/superpowers/plans/2026-08-06-supplier-po-pdf-vat.md new file mode 100644 index 0000000..6d81b39 --- /dev/null +++ b/docs/superpowers/plans/2026-08-06-supplier-po-pdf-vat.md @@ -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 (``), 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 `` 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 `` 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. */} + + + Subtotal: + {formatZAR(subtotal)} + + + + Total: + {formatZAR(total)} + + +``` + +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. */} + + + Subtotal: + {formatZAR(subtotal)} + + + VAT (15%): + {formatZAR(vat)} + + + + Total: + {formatZAR(totalInclVat)} + + +``` + +- [ ] **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`. \ No newline at end of file