WFDASH-WEBAPP/docs/superpowers/plans/2026-08-06-supplier-po-pdf-vat.md
SJCoetz 40d069b5aa docs(plan): add 15% VAT to supplier PO PDF implementation plan
Co-authored-by: Orca <help@stably.ai>
2026-08-06 11:48:21 +02:00

24 KiB
Raw Blame History

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:

import {
  formatZAR,
  formatDateLong,
  addDays,
  quoteSubtotal,
  vatAmount,
  poSubtotal,
  QUOTE_VALID_DAYS,
  effectiveQuoteDate,
  effectiveValidityDays,
} from "./format";

Then append at the bottom of the file:

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:

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):

/**
 * 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:

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
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):

import { formatZAR, formatDateLong } from "./format";

Replace with:

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):

  // 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:

  // 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):

        {/* 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:

        {/* 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:

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:

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
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:

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:

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:

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
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:

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:

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:

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:

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.