mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

orders-pagination.test.ts

Language TypeScript6.5 KB177 lines

packages/plinth-client/test/orders-pagination.test.ts

  1. /**
  2. * Plinth pagination counts product lines, not orders.
  3. *
  4. * `limit` and `offset` on orders.fetch are measured in product lines. Against
  5. * the live API:
  6. *
  7. * limit=100 offset=0 -> 61 orders, 100 lines
  8. * limit=100 offset=100 -> 57 orders, 100 lines
  9. * limit=500 offset=0 -> 297 orders, 500 lines
  10. *
  11. * A loop that stops on `batch.length < limit`, comparing orders against a line
  12. * budget, stops after the first page every time. Because Plinth returns orders
  13. * oldest first, the surviving slice is the oldest days of the window, mostly
  14. * terminal, with the design queue reading zero: nothing looks broken, and the
  15. * board describes a different fortnight while labelling it as this one.
  16. *
  17. * These tests count in the unit the API counts in. A fake that returned a page
  18. * of orders rather than a page of lines would let that mistake pass, so the
  19. * fakes below deliberately model many-lines-per-order.
  20. */
  21. import { describe, expect, it } from 'vitest'
  22. import { OrdersResource } from '../src/resources/orders.js'
  23. import type { PlinthClient } from '../src/http.js'
  24. /** One order carrying `lines` product rows. */
  25. function order(id: number, lines: number) {
  26. const product: Record<string, unknown> = {}
  27. for (let i = 0; i < lines; i++) {
  28. product[String(i)] = {
  29. line_ref: id * 100 + i,
  30. item_ref: 1,
  31. item_name: 'Mesh Banner',
  32. qty: 1,
  33. unit_price: '10.00',
  34. line_status: 'Delivered',
  35. }
  36. }
  37. return {
  38. order_ref: id,
  39. status_label: 'Delivered',
  40. status_code: 189,
  41. closed_on: '2026-07-01 10:00:00',
  42. first_name: 'A',
  43. last_name: 'B',
  44. grand_total: '10.00',
  45. lines: product,
  46. }
  47. }
  48. /** Builds a page holding exactly `lines` product rows, spread over few orders. */
  49. function pageOfLines(startId: number, lines: number, linesPerOrder = 5) {
  50. const order_map: Record<string, unknown> = {}
  51. let made = 0
  52. let id = startId
  53. while (made < lines) {
  54. const n = Math.min(linesPerOrder, lines - made)
  55. order_map[String(id)] = order(id, n)
  56. made += n
  57. id++
  58. }
  59. return { order_map }
  60. }
  61. /** Records the params of every call and replays scripted pages. */
  62. function fake(pages: unknown[]) {
  63. const calls: Record<string, unknown>[] = []
  64. const client = {
  65. async call(_m: string, params: Record<string, unknown>) {
  66. calls.push(params)
  67. return pages[calls.length - 1] ?? { order_map: {} }
  68. },
  69. } as unknown as PlinthClient
  70. return { client, calls }
  71. }
  72. describe('the page break compares lines, not orders', () => {
  73. it('keeps paging when a full page of lines came back from few orders', async () => {
  74. // 100 lines across 20 orders. The old condition (20 orders < 100) stopped
  75. // here and returned a twelfth of the window.
  76. const { client, calls } = fake([
  77. pageOfLines(1, 100),
  78. pageOfLines(100, 100),
  79. pageOfLines(200, 30), // short in lines -> genuine end
  80. ])
  81. const res = await new OrdersResource(client).linesInWindow(
  82. '2026-06-26', '2026-08-25', 100,
  83. )
  84. expect(calls).toHaveLength(3)
  85. expect(res.lines).toHaveLength(230)
  86. expect(res.pages).toBe(3)
  87. expect(res.truncated).toBe(false)
  88. })
  89. it('stops on the first page only when that page is short in LINES', async () => {
  90. const { client, calls } = fake([pageOfLines(1, 40)])
  91. const res = await new OrdersResource(client).linesInWindow(
  92. '2026-06-26', '2026-08-25', 100,
  93. )
  94. expect(calls).toHaveLength(1)
  95. expect(res.lines).toHaveLength(40)
  96. expect(res.truncated).toBe(false)
  97. })
  98. it('does not stop on a page of exactly limit lines held by one order', async () => {
  99. // The pathological shape: 1 order, 100 lines. Counting orders, 1 < 100.
  100. const { client, calls } = fake([
  101. { order_map: { '1': order(1, 100) } },
  102. pageOfLines(2, 10),
  103. ])
  104. const res = await new OrdersResource(client).linesInWindow(
  105. '2026-06-26', '2026-08-25', 100,
  106. )
  107. expect(calls).toHaveLength(2)
  108. expect(res.lines).toHaveLength(110)
  109. })
  110. it('advances offset by the line limit, not by the order count', async () => {
  111. const { client, calls } = fake([
  112. pageOfLines(1, 100),
  113. pageOfLines(100, 100),
  114. pageOfLines(200, 1),
  115. ])
  116. await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25', 100)
  117. expect(calls.map((c) => c.offset)).toEqual([0, 100, 200])
  118. expect(calls.map((c) => c.limit)).toEqual([100, 100, 100])
  119. })
  120. })
  121. describe('a capped sweep says so', () => {
  122. it('reports truncated when maxPages runs out before a short page', async () => {
  123. // Every page full: the window is wider than this can fetch. The counts are a
  124. // floor, and the caller has to be able to know that.
  125. const { client } = fake([pageOfLines(1, 50), pageOfLines(50, 50), pageOfLines(100, 50)])
  126. const res = await new OrdersResource(client).linesInWindow(
  127. '2026-06-26', '2026-08-25', 50, 3,
  128. )
  129. expect(res.pages).toBe(3)
  130. expect(res.truncated).toBe(true)
  131. })
  132. it('reports not-truncated when it reached the end of the window', async () => {
  133. const { client } = fake([pageOfLines(1, 50), pageOfLines(50, 20)])
  134. const res = await new OrdersResource(client).linesInWindow(
  135. '2026-06-26', '2026-08-25', 50, 3,
  136. )
  137. expect(res.truncated).toBe(false)
  138. })
  139. it('treats an empty first page as a complete empty window, not a truncated one', async () => {
  140. // Plinth returns null for "Invalid request", which is also how it says zero
  141. // results. Zero and truncated must not look the same.
  142. const { client } = fake([{ order_map: undefined }])
  143. const res = await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25')
  144. expect(res.lines).toEqual([])
  145. expect(res.truncated).toBe(false)
  146. expect(res.pages).toBe(1)
  147. })
  148. })
  149. describe('the window it actually asks for', () => {
  150. it('sends the dates it was given, converted to Plinth format', async () => {
  151. const { client, calls } = fake([pageOfLines(1, 1)])
  152. await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25')
  153. // toPlinthDate turns ISO into yyyymmdd; assert the shape, not the algorithm.
  154. expect(String(calls[0]!.since)).toMatch(/^\d{8}$/)
  155. expect(String(calls[0]!.until)).toMatch(/^\d{8}$/)
  156. })
  157. it('defaults to a limit that fetches a 60-day window in few calls', async () => {
  158. // 60 days is ~780 lines live. At a page size of 100 that is 8 requests
  159. // against a 10-per-10-second rate limit.
  160. const { client, calls } = fake([pageOfLines(1, 10)])
  161. await new OrdersResource(client).linesInWindow('2026-06-26', '2026-08-25')
  162. expect(Number(calls[0]!.limit)).toBeGreaterThanOrEqual(500)
  163. })
  164. })