@burdenoff/microfe-movethewheels 2026.1016.3 → 2026.1016.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +10 -0
  2. package/dist/MoveTheWheelsRoot.js +5 -0
  3. package/dist/MoveTheWheelsRoutes.js +25 -21
  4. package/dist/components/ContainerEvidencePicker.js +55 -55
  5. package/dist/components/FleetAlertsPanel.js +2 -2
  6. package/dist/components/freight-invoice/InvoiceForm.js +366 -0
  7. package/dist/components/freight-invoice/InvoiceResults.js +240 -0
  8. package/dist/generated/consolidation-operations.d.ts +195 -265
  9. package/dist/generated/container-condition-operations.d.ts +124 -187
  10. package/dist/generated/container-operations.d.ts +59 -85
  11. package/dist/generated/empty-reuse-operations.d.ts +165 -271
  12. package/dist/generated/free-time-operations.d.ts +120 -184
  13. package/dist/generated/freight-cost-operations.d.ts +307 -476
  14. package/dist/generated/freight-invoice-operations.d.ts +1447 -0
  15. package/dist/generated/logistics-procedure-operations.d.ts +100 -156
  16. package/dist/generated/pending-pick-operations.d.ts +769 -1025
  17. package/dist/generated/shipment-status-authority-operations.d.ts +82 -103
  18. package/dist/generated/stock-binding-operations.d.ts +188 -260
  19. package/dist/generated/stock-observation-operations.d.ts +103 -138
  20. package/dist/generated/tracking-quality-operations.d.ts +27 -41
  21. package/dist/generated/trip-utilization-operations.d.ts +137 -185
  22. package/dist/generated/wspace-operations.d.ts +628 -2272
  23. package/dist/generated/wspace-types.d.ts +4 -23
  24. package/dist/hooks/useInvoiceComparisonRecovery.js +118 -0
  25. package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
  26. package/dist/movethewheels/components/freight-invoice/InvoiceForm.d.ts +19 -0
  27. package/dist/movethewheels/components/freight-invoice/InvoiceResults.d.ts +9 -0
  28. package/dist/movethewheels/hooks/useFleet.d.ts +1 -1
  29. package/dist/movethewheels/hooks/useFleetIntelligence.d.ts +1 -1
  30. package/dist/movethewheels/hooks/useInvoiceComparisonRecovery.d.ts +22 -0
  31. package/dist/movethewheels/hooks/useProcedureSelection.d.ts +1 -3
  32. package/dist/movethewheels/pages/FreightInvoicePage.d.ts +10 -0
  33. package/dist/movethewheels/utils/driverLicenseExpiry.d.ts +6 -0
  34. package/dist/movethewheels/utils/invoiceComparison.d.ts +32 -0
  35. package/dist/pages/DriverDetailsPage.js +306 -282
  36. package/dist/pages/FreightEstimatePage.js +1 -0
  37. package/dist/pages/FreightInvoicePage.js +346 -0
  38. package/dist/src/generated/consolidation-operations.js +3961 -503
  39. package/dist/src/generated/container-condition-operations.js +2968 -206
  40. package/dist/src/generated/container-operations.js +997 -99
  41. package/dist/src/generated/empty-reuse-operations.js +4811 -747
  42. package/dist/src/generated/free-time-operations.js +2824 -233
  43. package/dist/src/generated/freight-cost-operations.js +4852 -812
  44. package/dist/src/generated/freight-invoice-operations.js +4894 -0
  45. package/dist/src/generated/logistics-procedure-operations.js +2707 -360
  46. package/dist/src/generated/pending-pick-operations.js +8244 -1196
  47. package/dist/src/generated/shipment-status-authority-operations.js +1157 -84
  48. package/dist/src/generated/stock-binding-operations.js +2779 -173
  49. package/dist/src/generated/stock-observation-operations.js +1274 -91
  50. package/dist/src/generated/tracking-quality-operations.js +407 -42
  51. package/dist/src/generated/trip-utilization-operations.js +1765 -138
  52. package/dist/src/generated/wspace-operations.js +428 -706
  53. package/dist/utils/driverLicenseExpiry.js +14 -0
  54. package/dist/utils/invoiceComparison.js +272 -0
  55. package/package.json +29 -29
  56. package/src/generated/consolidation-operations.ts +60 -559
  57. package/src/generated/container-condition-operations.ts +63 -265
  58. package/src/generated/container-operations.ts +37 -132
  59. package/src/generated/empty-reuse-operations.ts +83 -824
  60. package/src/generated/free-time-operations.ts +55 -284
  61. package/src/generated/freight-cost-operations.ts +42 -848
  62. package/src/generated/freight-invoice-operations.ts +830 -0
  63. package/src/generated/logistics-procedure-operations.ts +60 -414
  64. package/src/generated/pending-pick-operations.ts +48 -1239
  65. package/src/generated/shipment-status-authority-operations.ts +33 -113
  66. package/src/generated/stock-binding-operations.ts +29 -198
  67. package/src/generated/stock-observation-operations.ts +24 -111
  68. package/src/generated/tracking-quality-operations.ts +19 -57
  69. package/src/generated/trip-utilization-operations.ts +32 -166
  70. package/src/generated/wspace-operations.ts +284 -2906
  71. package/src/generated/wspace-types.ts +2 -8
  72. package/src/movethewheels/MoveTheWheelsRoot.tsx +6 -0
  73. package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
  74. package/src/movethewheels/components/ContainerEvidencePicker.tsx +12 -4
  75. package/src/movethewheels/components/FleetAlertsPanel.tsx +4 -2
  76. package/src/movethewheels/components/freight-invoice/InvoiceForm.tsx +412 -0
  77. package/src/movethewheels/components/freight-invoice/InvoiceResults.tsx +162 -0
  78. package/src/movethewheels/hooks/useFleet.ts +1 -1
  79. package/src/movethewheels/hooks/useFleetIntelligence.ts +5 -1
  80. package/src/movethewheels/hooks/useInvoiceComparisonRecovery.ts +227 -0
  81. package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
  82. package/src/movethewheels/pages/FreightEstimatePage.tsx +1 -0
  83. package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
  84. package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
  85. package/src/movethewheels/utils/invoiceComparison.ts +520 -0
  86. package/src/operations/freight-invoice/comparison.graphql +40 -0
  87. package/src/operations/wspace/freight-invoice-evidence.graphql +6 -0
  88. package/patches/braces@3.0.3.patch +0 -87
@@ -0,0 +1,520 @@
1
+ import { freightMoney } from './freightDraft';
2
+ import type {
3
+ CompareMtwFreightInvoiceInput,
4
+ InvoiceEstimatePinFragment,
5
+ InvoiceReceiptFragment,
6
+ InvoiceStructuralPreviewQuery,
7
+ MtwFreightInvoiceFactsInput,
8
+ } from '../../generated/freight-invoice-operations';
9
+
10
+ export const INVOICE_RECOVERY_STORAGE = 'movethewheels.freight-invoice.unresolved.v1';
11
+ export interface InvoiceScope {
12
+ actorId: string;
13
+ workspaceId: string;
14
+ tenantId: string | null;
15
+ organizationId: string;
16
+ projectId: string | null;
17
+ gateway: string;
18
+ }
19
+ export interface InvoiceIntent {
20
+ version: 1;
21
+ scope: InvoiceScope;
22
+ input: CompareMtwFreightInvoiceInput;
23
+ estimate: InvoiceEstimatePinFragment;
24
+ }
25
+ function fail(message: string): never {
26
+ throw new Error(message);
27
+ }
28
+ function object(value: unknown, allowed: readonly string[]): Record<string, unknown> {
29
+ if (
30
+ !value ||
31
+ typeof value !== 'object' ||
32
+ Array.isArray(value) ||
33
+ Object.keys(value).some((key) => !allowed.includes(key))
34
+ )
35
+ fail('Invoice fields are not supported. Nothing was sent.');
36
+ return Object.fromEntries(Object.entries(value));
37
+ }
38
+ function text(value: unknown, maximum: number): string {
39
+ if (typeof value !== 'string' || !value.trim() || value.trim().length > maximum)
40
+ fail('Complete the bounded invoice source fields. Nothing was sent.');
41
+ return value.trim();
42
+ }
43
+ function optional(value: unknown, maximum: number): string | null {
44
+ return value == null || value === '' ? null : text(value, maximum);
45
+ }
46
+ function integer(value: unknown, max: number): number {
47
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > max)
48
+ fail('Invoice numeric convention is invalid. Nothing was sent.');
49
+ return value;
50
+ }
51
+ function currency(value: unknown) {
52
+ const v = object(value, ['code', 'minorUnitDigits']),
53
+ code = text(v.code, 3);
54
+ if (!/^[A-Z]{3}$/.test(code)) fail('Declare an uppercase three-letter invoice currency.');
55
+ return { code, minorUnitDigits: integer(v.minorUnitDigits, 4) };
56
+ }
57
+ export function invoiceCanonical(value: unknown): string {
58
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
59
+ return JSON.stringify(value);
60
+ if (typeof value === 'number' && Number.isFinite(value)) return JSON.stringify(value);
61
+ if (Array.isArray(value)) return '[' + value.map(invoiceCanonical).join(',') + ']';
62
+ if (value && typeof value === 'object') {
63
+ const v = Object.fromEntries(Object.entries(value)),
64
+ keys = Object.keys(v).filter((key) => v[key] !== undefined);
65
+ keys.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
66
+ return (
67
+ '{' + keys.map((key) => JSON.stringify(key) + ':' + invoiceCanonical(v[key])).join(',') + '}'
68
+ );
69
+ }
70
+ return fail('Cannot safely retain this invoice intent. Nothing was sent.');
71
+ }
72
+ export function invoiceFactsInput(value: unknown): MtwFreightInvoiceFactsInput {
73
+ const v = object(value, [
74
+ 'schemaVersion',
75
+ 'supplierReference',
76
+ 'supplierLabel',
77
+ 'invoiceReference',
78
+ 'sourceReference',
79
+ 'sourceRevision',
80
+ 'invoiceDate',
81
+ 'currency',
82
+ 'rows',
83
+ ]);
84
+ if (v.schemaVersion !== 1 || !Array.isArray(v.rows) || v.rows.length < 1 || v.rows.length > 100)
85
+ fail('Provide 1–100 reported invoice rows.');
86
+ const money = currency(v.currency),
87
+ date = optional(v.invoiceDate, 10);
88
+ if (date !== null) {
89
+ const d = new Date(date + 'T00:00:00.000Z');
90
+ if (
91
+ !/^\d{4}-\d{2}-\d{2}$/.test(date) ||
92
+ date.startsWith('0000') ||
93
+ !Number.isFinite(d.getTime()) ||
94
+ d.toISOString().slice(0, 10) !== date
95
+ )
96
+ fail('Invoice date must be an exact source date or left unknown.');
97
+ }
98
+ const rows = v.rows.map((item) => {
99
+ const r = object(item, [
100
+ 'id',
101
+ 'description',
102
+ 'estimateLineId',
103
+ 'currency',
104
+ 'payerReference',
105
+ 'payerLabel',
106
+ 'quantity',
107
+ 'unit',
108
+ 'unitRateMajor',
109
+ 'amountMinor',
110
+ 'zeroReason',
111
+ ]);
112
+ const row = {
113
+ id: text(r.id, 120),
114
+ description: text(r.description, 1000),
115
+ estimateLineId: optional(r.estimateLineId, 120),
116
+ currency: currency(r.currency),
117
+ payerReference: optional(r.payerReference, 255),
118
+ payerLabel: optional(r.payerLabel, 255),
119
+ quantity: optional(r.quantity, 40),
120
+ unit: optional(r.unit, 255),
121
+ unitRateMajor: optional(r.unitRateMajor, 40),
122
+ amountMinor: optional(r.amountMinor, 38),
123
+ zeroReason: optional(r.zeroReason, 255),
124
+ };
125
+ if (invoiceCanonical(row.currency) !== invoiceCanonical(money))
126
+ fail('Every row must declare the same invoice currency and scale.');
127
+ if ((row.payerReference === null) !== (row.payerLabel === null))
128
+ fail('Supply both payer reference and label, or leave both unknown.');
129
+ for (const decimal of [row.quantity, row.unitRateMajor])
130
+ if (decimal !== null && !/^(0|[1-9]\d{0,19})(\.\d{1,18})?$/.test(decimal))
131
+ fail(
132
+ 'Quantity and rate require nonnegative decimal strings; no credits or exponent notation.'
133
+ );
134
+ if (row.amountMinor !== null && !/^(0|[1-9]\d{0,37})$/.test(row.amountMinor))
135
+ fail('Amount requires a nonnegative integer minor-unit string or unknown.');
136
+ if (row.amountMinor === '0' && row.zeroReason === null)
137
+ fail('Record the source reason for an explicit zero charge.');
138
+ return row;
139
+ });
140
+ rows.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
141
+ if (new Set(rows.map((r) => r.id)).size !== rows.length) fail('Invoice row IDs must be unique.');
142
+ return {
143
+ schemaVersion: 1,
144
+ supplierReference: optional(v.supplierReference, 255),
145
+ supplierLabel: optional(v.supplierLabel, 255),
146
+ invoiceReference: optional(v.invoiceReference, 120),
147
+ sourceReference: text(v.sourceReference, 255),
148
+ sourceRevision: text(v.sourceRevision, 255),
149
+ invoiceDate: date,
150
+ currency: money,
151
+ rows,
152
+ };
153
+ }
154
+ export function invoiceRecordInput(value: unknown): CompareMtwFreightInvoiceInput {
155
+ const v = object(value, [
156
+ 'shipmentId',
157
+ 'estimateVersionId',
158
+ 'facts',
159
+ 'evidence',
160
+ 'expectedPreviousComparisonId',
161
+ 'expectedRevision',
162
+ 'correctionReason',
163
+ 'idempotencyKey',
164
+ ]);
165
+ const facts = invoiceFactsInput(v.facts);
166
+ if (!facts.supplierReference || !facts.invoiceReference)
167
+ fail('Source stable supplier and invoice references before recording.');
168
+ if (!Array.isArray(v.evidence) || v.evidence.length < 1 || v.evidence.length > 10)
169
+ fail('Select 1–10 current exact evidence versions.');
170
+ const evidence = v.evidence.map((item) => {
171
+ const r = object(item, ['fileId', 'versionId']);
172
+ return { fileId: text(r.fileId, 255), versionId: text(r.versionId, 255) };
173
+ });
174
+ evidence.sort((a, b) => {
175
+ const x = JSON.stringify([a.fileId, a.versionId]),
176
+ y = JSON.stringify([b.fileId, b.versionId]);
177
+ return x < y ? -1 : x > y ? 1 : 0;
178
+ });
179
+ if (new Set(evidence.map((e) => e.versionId)).size !== evidence.length)
180
+ fail('Select distinct exact evidence versions.');
181
+ const expectedRevision = integer(v.expectedRevision, 2147483646),
182
+ previous = optional(v.expectedPreviousComparisonId, 255),
183
+ reason = optional(v.correctionReason, 1000);
184
+ if (
185
+ (expectedRevision === 0) !== (previous === null) ||
186
+ (expectedRevision === 0) !== (reason === null)
187
+ )
188
+ fail('A correction needs the exact previous revision and a reason.');
189
+ const result = {
190
+ shipmentId: text(v.shipmentId, 255),
191
+ estimateVersionId: text(v.estimateVersionId, 255),
192
+ facts,
193
+ evidence,
194
+ expectedPreviousComparisonId: previous,
195
+ expectedRevision,
196
+ correctionReason: reason,
197
+ idempotencyKey: text(v.idempotencyKey, 120),
198
+ };
199
+ if (new TextEncoder().encode(invoiceCanonical(result)).byteLength > 131072)
200
+ fail('This normalized invoice request exceeds 131072 bytes.');
201
+ return result;
202
+ }
203
+ export function sameInvoiceScope(a: InvoiceScope, b: InvoiceScope | null): boolean {
204
+ return b !== null && invoiceCanonical(a) === invoiceCanonical(b);
205
+ }
206
+ export function decodeInvoiceIntent(raw: string | null): InvoiceIntent | null {
207
+ if (!raw || new TextEncoder().encode(raw).byteLength > 200000) return null;
208
+ try {
209
+ const v = object(JSON.parse(raw), ['version', 'scope', 'input', 'estimate']);
210
+ if (v.version !== 1) return null;
211
+ const s = object(v.scope, [
212
+ 'actorId',
213
+ 'workspaceId',
214
+ 'tenantId',
215
+ 'organizationId',
216
+ 'projectId',
217
+ 'gateway',
218
+ ]);
219
+ const scope = {
220
+ actorId: text(s.actorId, 255),
221
+ workspaceId: text(s.workspaceId, 255),
222
+ tenantId: optional(s.tenantId, 255),
223
+ organizationId: text(s.organizationId, 255),
224
+ projectId: optional(s.projectId, 255),
225
+ gateway: text(s.gateway, 2048),
226
+ };
227
+ const input = invoiceRecordInput(v.input),
228
+ e = object(v.estimate, [
229
+ 'id',
230
+ 'estimateReference',
231
+ 'version',
232
+ 'kind',
233
+ 'sourceReference',
234
+ 'sourceRevision',
235
+ 'quotedAt',
236
+ 'validFrom',
237
+ 'validUntil',
238
+ 'acceptance',
239
+ ]);
240
+ if (
241
+ (e.kind !== 'ESTIMATE' && e.kind !== 'QUOTE') ||
242
+ e.acceptance !== 'NOT_ESTABLISHED' ||
243
+ e.id !== input.estimateVersionId
244
+ )
245
+ return null;
246
+ const version = integer(e.version, 2147483647);
247
+ if (version < 1) return null;
248
+ const estimate: InvoiceEstimatePinFragment = {
249
+ id: text(e.id, 255),
250
+ estimateReference: text(e.estimateReference, 120),
251
+ version,
252
+ kind: e.kind,
253
+ sourceReference: text(e.sourceReference, 255),
254
+ sourceRevision: text(e.sourceRevision, 255),
255
+ quotedAt: optional(e.quotedAt, 32),
256
+ validFrom: optional(e.validFrom, 32),
257
+ validUntil: optional(e.validUntil, 32),
258
+ acceptance: e.acceptance,
259
+ };
260
+ return { version: 1, scope, input, estimate };
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+ export function encodeInvoiceIntent(value: InvoiceIntent): string {
266
+ const raw = JSON.stringify(value);
267
+ if (!decodeInvoiceIntent(raw))
268
+ fail('Cannot retain the original invoice intent. Nothing was sent.');
269
+ return raw;
270
+ }
271
+ function instant(value: unknown): value is string {
272
+ return (
273
+ typeof value === 'string' &&
274
+ Number.isFinite(new Date(value).getTime()) &&
275
+ new Date(value).toISOString() === value
276
+ );
277
+ }
278
+ const comparisonFlags = new Set([
279
+ 'MATCHED',
280
+ 'RATE_CHANGED',
281
+ 'QUANTITY_CHANGED',
282
+ 'AMOUNT_CHANGED',
283
+ 'CURRENCY_CHANGED',
284
+ 'PAYER_CHANGED',
285
+ 'UNMATCHED_INVOICE_LINE',
286
+ 'MISSING_EXPECTED_LINE',
287
+ 'DUPLICATE_MATCH',
288
+ 'EXCLUDED_CHARGE_APPEARED',
289
+ 'UNKNOWN_COMPARISON',
290
+ ]);
291
+ const comparisonReasons = new Set([
292
+ 'MISSING_AMOUNT',
293
+ 'MISSING_PAYER',
294
+ 'MISSING_UNIT',
295
+ 'UNIT_MISMATCH',
296
+ 'MISSING_QUANTITY',
297
+ 'MISSING_RATE',
298
+ 'SCALE_MISMATCH',
299
+ 'UNKNOWN_BASIS',
300
+ 'UNKNOWN_DISPOSITION',
301
+ 'INCLUDED_CHILD_BILLED_SEPARATELY',
302
+ 'UNKNOWN_EXPECTED_CHARGE_COVERAGE',
303
+ ]);
304
+ function enumList(value: unknown, allowed: Set<string>, nonempty = false): value is string[] {
305
+ return (
306
+ Array.isArray(value) &&
307
+ value.length <= allowed.size &&
308
+ (!nonempty || value.length > 0) &&
309
+ value.every((v) => typeof v === 'string' && allowed.has(v)) &&
310
+ new Set(value).size === value.length
311
+ );
312
+ }
313
+ function outcomeMatches(
314
+ receipt: Pick<
315
+ InvoiceReceiptFragment,
316
+ 'outcome' | 'estimateDeclaredSubtotals' | 'invoiceDeclaredSubtotals'
317
+ >,
318
+ expected: MtwFreightInvoiceFactsInput['rows']
319
+ ): boolean {
320
+ const o = receipt.outcome;
321
+ if (
322
+ !o ||
323
+ typeof o.allLinesMatched !== 'boolean' ||
324
+ !enumList(o.flags, comparisonFlags, true) ||
325
+ !Array.isArray(o.rows) ||
326
+ o.rows.length !== expected.length ||
327
+ new Set(o.rows.map((r) => r.invoiceLineId)).size !== expected.length ||
328
+ !Array.isArray(o.missingExpectedLineIds) ||
329
+ o.missingExpectedLineIds.length > 100 ||
330
+ o.missingExpectedLineIds.some((id) => typeof id !== 'string' || !id || id.length > 120) ||
331
+ new Set(o.missingExpectedLineIds).size !== o.missingExpectedLineIds.length ||
332
+ !Array.isArray(o.unknownExpectedCoverage) ||
333
+ o.unknownExpectedCoverage.length > 100
334
+ )
335
+ return false;
336
+ if (
337
+ o.rows.some(
338
+ (r) =>
339
+ !expected.some(
340
+ (e) => e.id === r.invoiceLineId && (e.estimateLineId ?? null) === r.estimateLineId
341
+ ) ||
342
+ !enumList(r.flags, comparisonFlags, true) ||
343
+ !enumList(r.reasons, comparisonReasons) ||
344
+ (r.deltaMinor !== null &&
345
+ (typeof r.deltaMinor !== 'string' || !/^(-?[1-9]\d{0,39}|0)$/.test(r.deltaMinor)))
346
+ )
347
+ )
348
+ return false;
349
+ if (
350
+ o.unknownExpectedCoverage.some(
351
+ (r) =>
352
+ typeof r.estimateLineId !== 'string' ||
353
+ !r.estimateLineId ||
354
+ r.estimateLineId.length > 120 ||
355
+ r.flag !== 'UNKNOWN_COMPARISON' ||
356
+ r.reason !== 'UNKNOWN_EXPECTED_CHARGE_COVERAGE'
357
+ )
358
+ )
359
+ return false;
360
+ if (
361
+ o.allLinesMatched &&
362
+ (o.flags.length !== 1 ||
363
+ o.flags[0] !== 'MATCHED' ||
364
+ o.missingExpectedLineIds.length > 0 ||
365
+ o.unknownExpectedCoverage.length > 0 ||
366
+ o.rows.some((r) => r.flags.length !== 1 || r.flags[0] !== 'MATCHED'))
367
+ )
368
+ return false;
369
+ if (!Array.isArray(o.comparableSubtotals) || o.comparableSubtotals.length > 100) return false;
370
+ const comparableRows: string[] = [];
371
+ for (const group of o.comparableSubtotals) {
372
+ currency(group.currency);
373
+ text(group.payerReference, 255);
374
+ for (const minor of [group.estimateMinor, group.invoiceMinor])
375
+ if (typeof minor !== 'string' || !/^(0|[1-9]\d{0,39})$/.test(minor)) return false;
376
+ if (
377
+ typeof group.deltaMinor !== 'string' ||
378
+ !/^(-?[1-9]\d{0,39}|0)$/.test(group.deltaMinor) ||
379
+ !Array.isArray(group.invoiceLineIds) ||
380
+ !group.invoiceLineIds.length ||
381
+ group.invoiceLineIds.some((id) => !expected.some((r) => r.id === id))
382
+ )
383
+ return false;
384
+ comparableRows.push(...group.invoiceLineIds);
385
+ }
386
+ if (new Set(comparableRows).size !== comparableRows.length) return false;
387
+ for (const groups of [receipt.estimateDeclaredSubtotals, receipt.invoiceDeclaredSubtotals]) {
388
+ if (!Array.isArray(groups) || groups.length > 100) return false;
389
+ for (const group of groups) {
390
+ currency(group.currency);
391
+ optional(group.payerReference, 255);
392
+ optional(group.payerLabel, 255);
393
+ if (
394
+ typeof group.knownAmountMinor !== 'string' ||
395
+ !/^(0|[1-9]\d{0,39})$/.test(group.knownAmountMinor) ||
396
+ (group.totalAmountMinor !== null &&
397
+ (typeof group.totalAmountMinor !== 'string' ||
398
+ !/^(0|[1-9]\d{0,39})$/.test(group.totalAmountMinor)))
399
+ )
400
+ return false;
401
+ }
402
+ }
403
+ return true;
404
+ }
405
+ /** Server owns full trusted context and fingerprint; validate visible business facts, never invent its hash. */
406
+ export function matchesInvoiceReceipt(
407
+ receipt: InvoiceReceiptFragment | null | undefined,
408
+ intent: InvoiceIntent
409
+ ): boolean {
410
+ try {
411
+ if (
412
+ !receipt ||
413
+ typeof receipt.id !== 'string' ||
414
+ !/^([a-f0-9]{8}-){1}[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(receipt.id) ||
415
+ typeof receipt.invoiceIdentityId !== 'string' ||
416
+ !receipt.invoiceIdentityId ||
417
+ receipt.invoiceIdentityId.length > 255 ||
418
+ receipt.invoiceIdentityId !== receipt.invoiceIdentityId.trim() ||
419
+ receipt.shipmentId !== intent.input.shipmentId ||
420
+ receipt.idempotencyKey !== intent.input.idempotencyKey ||
421
+ typeof receipt.requestFingerprint !== 'string' ||
422
+ !/^([a-f0-9]{64})$/.test(receipt.requestFingerprint) ||
423
+ receipt.methodVersion !== 1 ||
424
+ receipt.revision !== intent.input.expectedRevision + 1 ||
425
+ receipt.previousComparisonId !== (intent.input.expectedPreviousComparisonId ?? null) ||
426
+ receipt.correctionReason !== (intent.input.correctionReason ?? null) ||
427
+ receipt.createdBy !== intent.scope.actorId ||
428
+ receipt.actorProvenance !== 'direct_human' ||
429
+ (receipt.onBehalfOf !== null && receipt.onBehalfOf !== intent.scope.actorId) ||
430
+ !instant(receipt.recordedAt)
431
+ )
432
+ return false;
433
+ if (
434
+ invoiceCanonical(receipt.estimate) !== invoiceCanonical(intent.estimate) ||
435
+ invoiceCanonical(invoiceFactsInput(receipt.facts)) !== invoiceCanonical(intent.input.facts)
436
+ )
437
+ return false;
438
+ if (!outcomeMatches(receipt, intent.input.facts.rows)) return false;
439
+ const requested = intent.input.evidence.map((e) => JSON.stringify([e.fileId, e.versionId]));
440
+ const actual = receipt.evidence.map((e) => JSON.stringify([e.fileId, e.versionId]));
441
+ if (
442
+ actual.length !== requested.length ||
443
+ new Set(actual).size !== actual.length ||
444
+ actual.some((key, index) => key !== requested[index])
445
+ )
446
+ return false;
447
+ return receipt.evidence.every(
448
+ (e) =>
449
+ typeof e.sha256Hash === 'string' &&
450
+ /^[a-f0-9]{64}$/.test(e.sha256Hash) &&
451
+ instant(e.checkedAt) &&
452
+ new Date(e.checkedAt) <= new Date(receipt.recordedAt) &&
453
+ new Date(receipt.recordedAt).getTime() - new Date(e.checkedAt).getTime() <= 60000
454
+ );
455
+ } catch {
456
+ return false;
457
+ }
458
+ }
459
+
460
+ /** Structural preview must match the selected exact version and current draft; it grants no save authority. */
461
+ export function matchesInvoicePreview(
462
+ value: InvoiceStructuralPreviewQuery['previewMtwFreightInvoice'],
463
+ facts: MtwFreightInvoiceFactsInput,
464
+ estimate: InvoiceEstimatePinFragment
465
+ ): boolean {
466
+ try {
467
+ return (
468
+ !!value &&
469
+ value.methodVersion === 1 &&
470
+ typeof value.recordable === 'boolean' &&
471
+ Array.isArray(value.blockers) &&
472
+ value.blockers.length <= 4 &&
473
+ value.recordable === (value.blockers.length === 0) &&
474
+ value.blockers.every(
475
+ (blocker) =>
476
+ blocker === 'SUPPLIER_UNKNOWN' ||
477
+ blocker === 'INVOICE_REFERENCE_UNKNOWN' ||
478
+ blocker === 'EVIDENCE_REQUIRED' ||
479
+ blocker === 'SHIPMENT_NOT_LIVE'
480
+ ) &&
481
+ invoiceCanonical(value.estimate) === invoiceCanonical(estimate) &&
482
+ invoiceCanonical(invoiceFactsInput(value.facts)) ===
483
+ invoiceCanonical(invoiceFactsInput(facts)) &&
484
+ outcomeMatches(value, facts.rows)
485
+ );
486
+ } catch {
487
+ return false;
488
+ }
489
+ }
490
+
491
+ /** Exact decimal display including signed differences; money never enters Number. */
492
+ export function invoiceMoney(
493
+ minor: string | null | undefined,
494
+ currency: { code: string; minorUnitDigits: number }
495
+ ): string {
496
+ if (minor == null) return 'Unknown';
497
+ const negative = minor.startsWith('-');
498
+ return `${currency.code} ${negative ? '-' : ''}${freightMoney(negative ? minor.slice(1) : minor, currency.minorUnitDigits)}`;
499
+ }
500
+ export function invoiceRowDifference(
501
+ value: Pick<InvoiceReceiptFragment, 'facts' | 'outcome'>,
502
+ rowId: string
503
+ ): string {
504
+ const source = value.facts.rows.find((row) => row.id === rowId),
505
+ result = value.outcome.rows.find((row) => row.invoiceLineId === rowId);
506
+ const group = value.outcome.comparableSubtotals.find((group) =>
507
+ group.invoiceLineIds.includes(rowId)
508
+ );
509
+ if (
510
+ !source ||
511
+ !result ||
512
+ result.deltaMinor === null ||
513
+ !group ||
514
+ source.currency.code !== group.currency.code ||
515
+ source.currency.minorUnitDigits !== group.currency.minorUnitDigits ||
516
+ source.payerReference !== group.payerReference
517
+ )
518
+ return 'Not comparable';
519
+ return invoiceMoney(result.deltaMinor, source.currency);
520
+ }
@@ -0,0 +1,40 @@
1
+ fragment InvoiceSourceFacts on MtwFreightInvoiceFacts {
2
+ schemaVersion supplierReference supplierLabel invoiceReference sourceReference sourceRevision invoiceDate
3
+ currency { code minorUnitDigits }
4
+ rows { id description estimateLineId currency { code minorUnitDigits } payerReference payerLabel quantity unit unitRateMajor amountMinor zeroReason }
5
+ }
6
+ fragment InvoiceOutcome on MtwFreightInvoiceOutcome {
7
+ flags allLinesMatched missingExpectedLineIds
8
+ unknownExpectedCoverage { estimateLineId flag reason }
9
+ rows { invoiceLineId estimateLineId flags reasons deltaMinor }
10
+ comparableSubtotals { currency { code minorUnitDigits } payerReference estimateMinor invoiceMinor deltaMinor invoiceLineIds }
11
+ }
12
+ fragment InvoiceDeclaredSubtotal on MtwFreightSubtotal { currency { code minorUnitDigits } payerReference payerLabel knownAmountMinor totalAmountMinor }
13
+ fragment InvoiceEstimatePin on MtwFreightInvoiceEstimatePin { id estimateReference version kind sourceReference sourceRevision quotedAt validFrom validUntil acceptance }
14
+ fragment InvoiceReceipt on MtwFreightInvoiceComparison {
15
+ id shipmentId invoiceIdentityId revision previousComparisonId correctionReason methodVersion idempotencyKey requestFingerprint
16
+ estimate { ...InvoiceEstimatePin } facts { ...InvoiceSourceFacts } outcome { ...InvoiceOutcome }
17
+ estimateDeclaredSubtotals { ...InvoiceDeclaredSubtotal } invoiceDeclaredSubtotals { ...InvoiceDeclaredSubtotal }
18
+ evidence { fileId versionId sha256Hash checkedAt }
19
+ recordedAt createdBy actorProvenance onBehalfOf
20
+ }
21
+ query InvoiceShipments($pagination: PaginationInput) { shipments(pagination: $pagination) { id shipmentNumber primaryMode status } shipmentsCount }
22
+ query InvoiceEstimatePage($shipmentId: ID!, $pagination: MtwContainerPagination) {
23
+ mtwFreightEstimatePage(shipmentId: $shipmentId, pagination: $pagination) { items { id shipmentId estimateReference version facts { title kind } } total limit offset hasMore nextOffset }
24
+ }
25
+ query InvoiceEstimateVersion($shipmentId: ID!, $id: ID!) {
26
+ mtwFreightEstimateVersion(shipmentId: $shipmentId, id: $id) {
27
+ id shipmentId estimateReference version validityAtRead
28
+ facts { title kind sourceReference sourceRevision quotedAt validFrom validUntil lines { id description disposition amountBasis quantity unit unitRateMajor amountMinor currency { code minorUnitDigits } payerReference payerLabel inclusionParentLineId } }
29
+ totals { completeRelativeToDeclarations lines { id nativeAmountMinor } }
30
+ }
31
+ }
32
+ query InvoiceStructuralPreview($input: PreviewMtwFreightInvoiceInput!) {
33
+ previewMtwFreightInvoice(input: $input) { methodVersion recordable blockers estimate { ...InvoiceEstimatePin } facts { ...InvoiceSourceFacts } outcome { ...InvoiceOutcome } estimateDeclaredSubtotals { ...InvoiceDeclaredSubtotal } invoiceDeclaredSubtotals { ...InvoiceDeclaredSubtotal } }
34
+ }
35
+ mutation RecordInvoiceComparison($input: CompareMtwFreightInvoiceInput!) { compareMtwFreightInvoice(input: $input) { ...InvoiceReceipt } }
36
+ query InvoiceComparisonExact($shipmentId: ID!, $id: ID!) { mtwFreightInvoiceComparison(shipmentId: $shipmentId, id: $id) { ...InvoiceReceipt } }
37
+ query InvoiceComparisonHistory($shipmentId: ID!, $invoiceIdentityId: ID!, $pagination: MtwContainerPagination) {
38
+ mtwFreightInvoiceComparisonPage(shipmentId: $shipmentId, invoiceIdentityId: $invoiceIdentityId, pagination: $pagination) { items { ...InvoiceReceipt } total limit offset hasMore nextOffset }
39
+ }
40
+ query RecoverInvoiceComparison($shipmentId: ID!, $requestKey: String!) { mtwFreightInvoiceComparisonByRequestKey(shipmentId: $shipmentId, requestKey: $requestKey) { ...InvoiceReceipt } }
@@ -0,0 +1,6 @@
1
+ query InvoiceEvidenceVersions($fileId: ID!, $pagination: PaginationInput) {
2
+ getFileVersions(fileId: $fileId, pagination: $pagination) {
3
+ versions { id fileId name versionNumber versionLabel sha256Hash createdAt isCurrentVersion }
4
+ hasMore
5
+ }
6
+ }
@@ -1,87 +0,0 @@
1
- diff --git a/lib/compile.js b/lib/compile.js
2
- --- a/lib/compile.js
3
- +++ b/lib/compile.js
4
- @@ -1,0 +2,2 @@
5
- +
6
- +const validateAst = require('./validate-ast');
7
- @@ -6,0 +9 @@
8
- + validateAst(ast);
9
- diff --git a/lib/expand.js b/lib/expand.js
10
- --- a/lib/expand.js
11
- +++ b/lib/expand.js
12
- @@ -1,0 +2,2 @@
13
- +
14
- +const validateAst = require('./validate-ast');
15
- @@ -33,0 +36 @@
16
- + validateAst(ast);
17
- diff --git a/lib/parse.js b/lib/parse.js
18
- --- a/lib/parse.js
19
- +++ b/lib/parse.js
20
- @@ -1,0 +2,2 @@
21
- +
22
- +const validateAst = require('./validate-ast');
23
- @@ -146,0 +149 @@
24
- + if (stack.length >= 128) throw new RangeError('Braces nesting limit exceeded');
25
- @@ -212,0 +216 @@
26
- + if (stack.length >= 128) throw new RangeError('Braces nesting limit exceeded');
27
- @@ -327,0 +332 @@
28
- + validateAst(ast);
29
- diff --git a/lib/stringify.js b/lib/stringify.js
30
- --- a/lib/stringify.js
31
- +++ b/lib/stringify.js
32
- @@ -1,0 +2,2 @@
33
- +
34
- +const validateAst = require('./validate-ast');
35
- @@ -5,0 +8 @@
36
- + validateAst(ast);
37
- diff --git a/lib/validate-ast.js b/lib/validate-ast.js
38
- new file mode 100644
39
- --- /dev/null
40
- +++ b/lib/validate-ast.js
41
- @@ -0,0 +1,46 @@
42
- +'use strict';
43
- +
44
- +// Local mitigation for GHSA-vfj7-8cjw-p6xm. Validate iteratively before any
45
- +// recursive AST processing. Parent/prev links are metadata, not child edges.
46
- +const MAX_DEPTH = 128;
47
- +const MAX_NODES = 65536;
48
- +
49
- +module.exports = ast => {
50
- + const pending = [{ node: ast, depth: 0, exit: false }];
51
- + const active = new WeakSet();
52
- + let visits = 0;
53
- + while (pending.length > 0) {
54
- + const item = pending.pop();
55
- + const node = item.node;
56
- + if (item.exit) {
57
- + active.delete(node);
58
- + continue;
59
- + }
60
- + if (node === null || typeof node !== 'object') throw new TypeError('Invalid braces AST');
61
- + if (++visits > MAX_NODES) throw new RangeError('Braces AST node limit exceeded');
62
- + if (item.depth > MAX_DEPTH) throw new RangeError('Braces nesting limit exceeded');
63
- + if (active.has(node)) throw new RangeError('Cyclic braces AST');
64
- + // expand() also follows parent links. Reject malformed parent chains before
65
- + // its loop can hang; normal parser parents lead to the root.
66
- + let parent = node.parent;
67
- + const parents = new WeakSet();
68
- + let parentDepth = 0;
69
- + while (parent) {
70
- + if (typeof parent !== 'object' || parents.has(parent) || ++parentDepth > MAX_DEPTH) {
71
- + throw new RangeError('Invalid braces AST parent chain');
72
- + }
73
- + parents.add(parent);
74
- + parent = parent.parent;
75
- + }
76
- + if (node.nodes !== undefined) {
77
- + if (!Array.isArray(node.nodes)) throw new TypeError('Invalid braces AST children');
78
- + if (node.nodes.length > MAX_NODES - visits) throw new RangeError('Braces AST node limit exceeded');
79
- + active.add(node);
80
- + pending.push({ node, depth: item.depth, exit: true });
81
- + for (let index = node.nodes.length - 1; index >= 0; index--) {
82
- + pending.push({ node: node.nodes[index], depth: item.depth + 1, exit: false });
83
- + }
84
- + if (pending.length > 2 * MAX_NODES) throw new RangeError('Braces AST node limit exceeded');
85
- + }
86
- + }
87
- +};