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

assignment-fields.test.ts

Language TypeScript7.2 KB201 lines

packages/plinth-client/test/assignment-fields.test.ts

  1. /**
  2. * Plinth sends `production_owner` and `account_owner` on the orders.fetch
  3. * response, at order level and per product line.
  4. *
  5. * These tests pin the three decisions that make that path safe:
  6. *
  7. * 1. `--` is not a person. Treated as one, the board grows "a designer named
  8. * -- holding dozens of order lines".
  9. * 2. A per-line value beats the order-level one, including when the per-line
  10. * value is the sentinel. Assignment in Plinth is per product line; an order
  11. * naming someone does not make each of its lines theirs.
  12. * 3. Presence is counted, not assumed. A field that arrived by vendor change
  13. * can leave the same way, and a vanished field makes every line read
  14. * unassigned, indistinguishable from a team with nothing to do unless
  15. * somebody counted.
  16. */
  17. import { describe, expect, it } from 'vitest'
  18. import { OrdersResource } from '../src/resources/orders.js'
  19. import type { PlinthClient } from '../src/http.js'
  20. interface Assign {
  21. /** Omit a key entirely to model Plinth not sending it; it does that per record. */
  22. production_owner?: string
  23. account_owner?: string
  24. }
  25. /** One order carrying `lines` product rows, with assignment fields where given. */
  26. function order(id: number, lines: number, product: Assign = {}, orderLevel: Assign = {}) {
  27. const products: Record<string, unknown> = {}
  28. for (let i = 0; i < lines; i++) {
  29. products[String(i)] = {
  30. line_ref: id * 100 + i,
  31. item_ref: 1,
  32. item_name: 'Mesh Banner',
  33. qty: 1,
  34. unit_price: '10.00',
  35. line_status: 'Delivered',
  36. ...product,
  37. }
  38. }
  39. return {
  40. order_ref: id,
  41. status_label: 'Delivered',
  42. status_code: 189,
  43. closed_on: '2026-07-01 10:00:00',
  44. first_name: 'A',
  45. last_name: 'B',
  46. grand_total: '10.00',
  47. lines: products,
  48. ...orderLevel,
  49. }
  50. }
  51. function page(orders: ReturnType<typeof order>[]) {
  52. const order_map: Record<string, unknown> = {}
  53. for (const o of orders) order_map[String(o.order_ref)] = o
  54. return { order_map }
  55. }
  56. function fake(pages: unknown[]) {
  57. const calls: Record<string, unknown>[] = []
  58. const client = {
  59. async call(_m: string, params: Record<string, unknown>) {
  60. calls.push(params)
  61. return pages[calls.length - 1] ?? { order_map: {} }
  62. },
  63. } as unknown as PlinthClient
  64. return { client, calls }
  65. }
  66. const fetchLines = async (pages: unknown[]) => {
  67. const { client } = fake(pages)
  68. return new OrdersResource(client).linesInWindow('2026-07-01', '2026-07-31')
  69. }
  70. describe('the unassigned sentinel never becomes a person', () => {
  71. // A truthiness check would turn `--` into a designer with dozens of order
  72. // lines. "Nobody holds this" and
  73. // "somebody called -- holds this" lead to opposite actions.
  74. it.each(['--', '---', '-', '', ' ', 'N/A', 'none'])(
  75. 'reads %j as nobody',
  76. async (sentinel) => {
  77. const w = await fetchLines([page([order(1, 1, { production_owner: sentinel })])])
  78. expect(w.lines[0]!.productionOwner).toBeNull()
  79. },
  80. )
  81. it('does the same for the client success field', async () => {
  82. const w = await fetchLines([page([order(1, 1, { account_owner: '--' })])])
  83. expect(w.lines[0]!.accountOwner).toBeNull()
  84. })
  85. })
  86. describe('account labels pass through unmapped', () => {
  87. it('leaves the raw Plinth label alone', async () => {
  88. // Mapping label -> roster name is collect.ts's job, the same division of
  89. // labour the CSV parser had. A client package has no roster.
  90. const w = await fetchLines([
  91. page([
  92. order(1, 1, {
  93. production_owner: 'Marco-Acct7-Desk',
  94. account_owner: 'Hazel (Desk 2)',
  95. }),
  96. ]),
  97. ])
  98. expect(w.lines[0]!.productionOwner).toBe('Marco-Acct7-Desk')
  99. expect(w.lines[0]!.accountOwner).toBe('Hazel (Desk 2)')
  100. })
  101. })
  102. describe('per-line assignment beats the order level', () => {
  103. it('falls back to the order value when the product record lacks the key', async () => {
  104. const w = await fetchLines([
  105. page([order(1, 2, {}, { production_owner: 'Sam-Accounts-Desk' })]),
  106. ])
  107. expect(w.lines.map((l) => l.productionOwner)).toEqual([
  108. 'Sam-Accounts-Desk',
  109. 'Sam-Accounts-Desk',
  110. ])
  111. })
  112. it('keeps a per-line sentinel even when the order names somebody', async () => {
  113. // The more specific source wins. An order assigned to Sam can still hold a
  114. // line nobody has picked up, and crediting it to him would overstate his load
  115. // and hide the line from whoever should claim it.
  116. const w = await fetchLines([
  117. page([order(1, 1, { production_owner: '--' }, { production_owner: 'Sam-Accounts-Desk' })]),
  118. ])
  119. expect(w.lines[0]!.productionOwner).toBeNull()
  120. })
  121. it('prefers the per-line name over a different order-level name', async () => {
  122. const w = await fetchLines([
  123. page([
  124. order(
  125. 1,
  126. 1,
  127. { production_owner: 'Marco-Acct7-Desk' },
  128. { production_owner: 'Rafael-Prod-Desk' },
  129. ),
  130. ]),
  131. ])
  132. expect(w.lines[0]!.productionOwner).toBe('Marco-Acct7-Desk')
  133. })
  134. })
  135. describe('field coverage is counted, not assumed', () => {
  136. it('reports full coverage when every line carries both fields', async () => {
  137. const w = await fetchLines([
  138. page([
  139. order(1, 2, { production_owner: 'Marco-Acct7-Desk', account_owner: '--' }),
  140. order(2, 1, { production_owner: '--', account_owner: 'Sam-Accounts-Desk' }),
  141. ]),
  142. ])
  143. // Presence, not value: `--` is a present field saying "nobody", which is a
  144. // different fact from Plinth not sending the field at all.
  145. expect(w.assignmentFields).toEqual({ production: 3, accountOwner: 3, total: 3 })
  146. })
  147. it('reports zero coverage with a non-zero total when the fields are absent', async () => {
  148. // The regression that matters. Zero here must be distinguishable from "three
  149. // lines, nobody assigned"; the caller turns it into MISSING, not free time.
  150. const w = await fetchLines([page([order(1, 3)])])
  151. expect(w.assignmentFields).toEqual({ production: 0, accountOwner: 0, total: 3 })
  152. })
  153. it('counts an order-level field as covering each of its lines', async () => {
  154. const w = await fetchLines([
  155. page([order(1, 3, {}, { production_owner: 'Sam-Accounts-Desk' })]),
  156. ])
  157. expect(w.assignmentFields.production).toBe(3)
  158. expect(w.assignmentFields.accountOwner).toBe(0)
  159. })
  160. it('counts partial coverage across a mixed window', async () => {
  161. const w = await fetchLines([
  162. page([
  163. order(1, 2, { production_owner: 'Marco-Acct7-Desk' }),
  164. order(2, 2),
  165. ]),
  166. ])
  167. expect(w.assignmentFields).toEqual({ production: 2, accountOwner: 0, total: 4 })
  168. })
  169. it('accumulates across pages rather than reporting only the last one', async () => {
  170. // 5 lines per page is the short-page signal at this limit, so both pages are
  171. // fetched; coverage has to survive the loop.
  172. const wide = Array.from({ length: 100 }, (_, i) =>
  173. order(i + 1, 5, { production_owner: 'Marco-Acct7-Desk' }),
  174. )
  175. const w = await fetchLines([page(wide), page([order(999, 2)])])
  176. expect(w.assignmentFields.total).toBe(502)
  177. expect(w.assignmentFields.production).toBe(500)
  178. })
  179. it('reports an empty window as zero of zero, which says nothing either way', async () => {
  180. const w = await fetchLines([{ order_map: {} }])
  181. expect(w.assignmentFields).toEqual({ production: 0, accountOwner: 0, total: 0 })
  182. })
  183. })