orders-pagination.test.ts
Language TypeScript6.5 KB177 lines
/*** Plinth pagination counts product lines, not orders.** `limit` and `offset` on orders.fetch are measured in product lines. Against* the live API:** limit=100 offset=0 -> 61 orders, 100 lines* limit=100 offset=100 -> 57 orders, 100 lines* limit=500 offset=0 -> 297 orders, 500 lines** A loop that stops on `batch.length < limit`, comparing orders against a line* budget, stops after the first page every time. Because Plinth returns orders* oldest first, the surviving slice is the oldest days of the window, mostly* terminal, with the design queue reading zero: nothing looks broken, and the* board describes a different fortnight while labelling it as this one.** These tests count in the unit the API counts in. A fake that returned a page* of orders rather than a page of lines would let that mistake pass, so the* fakes below deliberately model many-lines-per-order.*/import { describe, expect, it } from 'vitest'import { OrdersResource } from '../src/resources/orders.js'import type { PlinthClient } from '../src/http.js'/** One order carrying `lines` product rows. */function order(id: number, lines: number) {const product: Record<string, unknown> = {}for (let i = 0; i < lines; i++) {product[String(i)] = {line_ref: id * 100 + i,item_ref: 1,item_name: 'Mesh Banner',qty: 1,unit_price: '10.00',line_status: 'Delivered',}}return {order_ref: id,status_label: 'Delivered',status_code: 189,closed_on: '2026-07-01 10:00:00',first_name: 'A',last_name: 'B',grand_total: '10.00',lines: product,}}/** Builds a page holding exactly `lines` product rows, spread over few orders. */function pageOfLines(startId: number, lines: number, linesPerOrder = 5) {const order_map: Record<string, unknown> = {}let made = 0let id = startIdwhile (made < lines) {const n = Math.min(linesPerOrder, lines - made)order_map[String(id)] = order(id, n)made += nid++}return { order_map }}/** Records the params of every call and replays scripted pages. */function fake(pages: unknown[]) {const calls: Record<string, unknown>[] = []const client = {async call(_m: string, params: Record<string, unknown>) {calls.push(params)return pages[calls.length - 1] ?? { order_map: {} }},} as unknown as PlinthClientreturn { client, calls }}describe('the page break compares lines, not orders', () => {it('keeps paging when a full page of lines came back from few orders', async () => {// 100 lines across 20 orders. The old condition (20 orders < 100) stopped// here and returned a twelfth of the window.const { client, calls } = fake([pageOfLines(1, 100),pageOfLines(100, 100),pageOfLines(200, 30), // short in lines -> genuine end])const res = await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25', 100,)expect(calls).toHaveLength(3)expect(res.lines).toHaveLength(230)expect(res.pages).toBe(3)expect(res.truncated).toBe(false)})it('stops on the first page only when that page is short in LINES', async () => {const { client, calls } = fake([pageOfLines(1, 40)])const res = await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25', 100,)expect(calls).toHaveLength(1)expect(res.lines).toHaveLength(40)expect(res.truncated).toBe(false)})it('does not stop on a page of exactly limit lines held by one order', async () => {// The pathological shape: 1 order, 100 lines. Counting orders, 1 < 100.const { client, calls } = fake([{ order_map: { '1': order(1, 100) } },pageOfLines(2, 10),])const res = await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25', 100,)expect(calls).toHaveLength(2)expect(res.lines).toHaveLength(110)})it('advances offset by the line limit, not by the order count', async () => {const { client, calls } = fake([pageOfLines(1, 100),pageOfLines(100, 100),pageOfLines(200, 1),])await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25', 100)expect(calls.map((c) => c.offset)).toEqual([0, 100, 200])expect(calls.map((c) => c.limit)).toEqual([100, 100, 100])})})describe('a capped sweep says so', () => {it('reports truncated when maxPages runs out before a short page', async () => {// Every page full: the window is wider than this can fetch. The counts are a// floor, and the caller has to be able to know that.const { client } = fake([pageOfLines(1, 50), pageOfLines(50, 50), pageOfLines(100, 50)])const res = await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25', 50, 3,)expect(res.pages).toBe(3)expect(res.truncated).toBe(true)})it('reports not-truncated when it reached the end of the window', async () => {const { client } = fake([pageOfLines(1, 50), pageOfLines(50, 20)])const res = await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25', 50, 3,)expect(res.truncated).toBe(false)})it('treats an empty first page as a complete empty window, not a truncated one', async () => {// Plinth returns null for "Invalid request", which is also how it says zero// results. Zero and truncated must not look the same.const { client } = fake([{ order_map: undefined }])const res = await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25')expect(res.lines).toEqual([])expect(res.truncated).toBe(false)expect(res.pages).toBe(1)})})describe('the window it actually asks for', () => {it('sends the dates it was given, converted to Plinth format', async () => {const { client, calls } = fake([pageOfLines(1, 1)])await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25')// toPlinthDate turns ISO into yyyymmdd; assert the shape, not the algorithm.expect(String(calls[0]!.since)).toMatch(/^\d{8}$/)expect(String(calls[0]!.until)).toMatch(/^\d{8}$/)})it('defaults to a limit that fetches a 60-day window in few calls', async () => {// 60 days is ~780 lines live. At a page size of 100 that is 8 requests// against a 10-per-10-second rate limit.const { client, calls } = fake([pageOfLines(1, 10)])await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25')expect(Number(calls[0]!.limit)).toBeGreaterThanOrEqual(500)})})