assignment-fields.test.ts
Language TypeScript7.2 KB201 lines
/*** Plinth sends `production_owner` and `account_owner` on the orders.fetch* response, at order level and per product line.** These tests pin the three decisions that make that path safe:** 1. `--` is not a person. Treated as one, the board grows "a designer named* -- holding dozens of order lines".* 2. A per-line value beats the order-level one, including when the per-line* value is the sentinel. Assignment in Plinth is per product line; an order* naming someone does not make each of its lines theirs.* 3. Presence is counted, not assumed. A field that arrived by vendor change* can leave the same way, and a vanished field makes every line read* unassigned, indistinguishable from a team with nothing to do unless* somebody counted.*/import { describe, expect, it } from 'vitest'import { OrdersResource } from '../src/resources/orders.js'import type { PlinthClient } from '../src/http.js'interface Assign {/** Omit a key entirely to model Plinth not sending it; it does that per record. */production_owner?: stringaccount_owner?: string}/** One order carrying `lines` product rows, with assignment fields where given. */function order(id: number, lines: number, product: Assign = {}, orderLevel: Assign = {}) {const products: Record<string, unknown> = {}for (let i = 0; i < lines; i++) {products[String(i)] = {line_ref: id * 100 + i,item_ref: 1,item_name: 'Mesh Banner',qty: 1,unit_price: '10.00',line_status: 'Delivered',...product,}}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: products,...orderLevel,}}function page(orders: ReturnType<typeof order>[]) {const order_map: Record<string, unknown> = {}for (const o of orders) order_map[String(o.order_ref)] = oreturn { order_map }}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 }}const fetchLines = async (pages: unknown[]) => {const { client } = fake(pages)return new OrdersResource(client).linesInWindow('2026-07-01', '2026-07-31')}describe('the unassigned sentinel never becomes a person', () => {// A truthiness check would turn `--` into a designer with dozens of order// lines. "Nobody holds this" and// "somebody called -- holds this" lead to opposite actions.it.each(['--', '---', '-', '', ' ', 'N/A', 'none'])('reads %j as nobody',async (sentinel) => {const w = await fetchLines([page([order(1, 1, { production_owner: sentinel })])])expect(w.lines[0]!.productionOwner).toBeNull()},)it('does the same for the client success field', async () => {const w = await fetchLines([page([order(1, 1, { account_owner: '--' })])])expect(w.lines[0]!.accountOwner).toBeNull()})})describe('account labels pass through unmapped', () => {it('leaves the raw Plinth label alone', async () => {// Mapping label -> roster name is collect.ts's job, the same division of// labour the CSV parser had. A client package has no roster.const w = await fetchLines([page([order(1, 1, {production_owner: 'Marco-Acct7-Desk',account_owner: 'Hazel (Desk 2)',}),]),])expect(w.lines[0]!.productionOwner).toBe('Marco-Acct7-Desk')expect(w.lines[0]!.accountOwner).toBe('Hazel (Desk 2)')})})describe('per-line assignment beats the order level', () => {it('falls back to the order value when the product record lacks the key', async () => {const w = await fetchLines([page([order(1, 2, {}, { production_owner: 'Sam-Accounts-Desk' })]),])expect(w.lines.map((l) => l.productionOwner)).toEqual(['Sam-Accounts-Desk','Sam-Accounts-Desk',])})it('keeps a per-line sentinel even when the order names somebody', async () => {// The more specific source wins. An order assigned to Sam can still hold a// line nobody has picked up, and crediting it to him would overstate his load// and hide the line from whoever should claim it.const w = await fetchLines([page([order(1, 1, { production_owner: '--' }, { production_owner: 'Sam-Accounts-Desk' })]),])expect(w.lines[0]!.productionOwner).toBeNull()})it('prefers the per-line name over a different order-level name', async () => {const w = await fetchLines([page([order(1,1,{ production_owner: 'Marco-Acct7-Desk' },{ production_owner: 'Rafael-Prod-Desk' },),]),])expect(w.lines[0]!.productionOwner).toBe('Marco-Acct7-Desk')})})describe('field coverage is counted, not assumed', () => {it('reports full coverage when every line carries both fields', async () => {const w = await fetchLines([page([order(1, 2, { production_owner: 'Marco-Acct7-Desk', account_owner: '--' }),order(2, 1, { production_owner: '--', account_owner: 'Sam-Accounts-Desk' }),]),])// Presence, not value: `--` is a present field saying "nobody", which is a// different fact from Plinth not sending the field at all.expect(w.assignmentFields).toEqual({ production: 3, accountOwner: 3, total: 3 })})it('reports zero coverage with a non-zero total when the fields are absent', async () => {// The regression that matters. Zero here must be distinguishable from "three// lines, nobody assigned"; the caller turns it into MISSING, not free time.const w = await fetchLines([page([order(1, 3)])])expect(w.assignmentFields).toEqual({ production: 0, accountOwner: 0, total: 3 })})it('counts an order-level field as covering each of its lines', async () => {const w = await fetchLines([page([order(1, 3, {}, { production_owner: 'Sam-Accounts-Desk' })]),])expect(w.assignmentFields.production).toBe(3)expect(w.assignmentFields.accountOwner).toBe(0)})it('counts partial coverage across a mixed window', async () => {const w = await fetchLines([page([order(1, 2, { production_owner: 'Marco-Acct7-Desk' }),order(2, 2),]),])expect(w.assignmentFields).toEqual({ production: 2, accountOwner: 0, total: 4 })})it('accumulates across pages rather than reporting only the last one', async () => {// 5 lines per page is the short-page signal at this limit, so both pages are// fetched; coverage has to survive the loop.const wide = Array.from({ length: 100 }, (_, i) =>order(i + 1, 5, { production_owner: 'Marco-Acct7-Desk' }),)const w = await fetchLines([page(wide), page([order(999, 2)])])expect(w.assignmentFields.total).toBe(502)expect(w.assignmentFields.production).toBe(500)})it('reports an empty window as zero of zero, which says nothing either way', async () => {const w = await fetchLines([{ order_map: {} }])expect(w.assignmentFields).toEqual({ production: 0, accountOwner: 0, total: 0 })})})