docs(plan): add 15% VAT to supplier PO PDF implementation plan
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
a663de2d36
commit
40d069b5aa
1 changed files with 651 additions and 0 deletions
651
docs/superpowers/plans/2026-08-06-supplier-po-pdf-vat.md
Normal file
651
docs/superpowers/plans/2026-08-06-supplier-po-pdf-vat.md
Normal 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`.
|
||||
Loading…
Reference in a new issue