@burdenoff/microfe-movethewheels 2026.1016.2 → 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 (113) 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-cost/FreightCommercialEditor.js +217 -0
  7. package/dist/components/freight-cost/FreightCommercialFactEditor.js +217 -0
  8. package/dist/components/freight-cost/FreightCommercialHistory.js +115 -0
  9. package/dist/components/freight-cost/FreightDraftForm.js +106 -91
  10. package/dist/components/freight-cost/FreightHistory.js +42 -40
  11. package/dist/components/freight-invoice/InvoiceForm.js +366 -0
  12. package/dist/components/freight-invoice/InvoiceResults.js +240 -0
  13. package/dist/generated/consolidation-operations.d.ts +195 -265
  14. package/dist/generated/container-condition-operations.d.ts +124 -187
  15. package/dist/generated/container-operations.d.ts +59 -85
  16. package/dist/generated/empty-reuse-operations.d.ts +165 -271
  17. package/dist/generated/free-time-operations.d.ts +120 -184
  18. package/dist/generated/freight-cost-operations.d.ts +955 -165
  19. package/dist/generated/freight-invoice-operations.d.ts +1447 -0
  20. package/dist/generated/logistics-procedure-operations.d.ts +100 -156
  21. package/dist/generated/pending-pick-operations.d.ts +769 -1025
  22. package/dist/generated/shipment-status-authority-operations.d.ts +82 -103
  23. package/dist/generated/stock-binding-operations.d.ts +188 -260
  24. package/dist/generated/stock-observation-operations.d.ts +103 -138
  25. package/dist/generated/tracking-quality-operations.d.ts +27 -41
  26. package/dist/generated/trip-utilization-operations.d.ts +137 -185
  27. package/dist/generated/wspace-operations.d.ts +628 -2272
  28. package/dist/generated/wspace-types.d.ts +4 -23
  29. package/dist/hooks/useFreightRecoveryAction.js +20 -20
  30. package/dist/hooks/useInvoiceComparisonRecovery.js +118 -0
  31. package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
  32. package/dist/movethewheels/components/freight-cost/FreightCommercialEditor.d.ts +8 -0
  33. package/dist/movethewheels/components/freight-cost/FreightCommercialFactEditor.d.ts +8 -0
  34. package/dist/movethewheels/components/freight-cost/FreightCommercialHistory.d.ts +4 -0
  35. package/dist/movethewheels/components/freight-cost/FreightDraftForm.d.ts +4 -2
  36. package/dist/movethewheels/components/freight-cost/FreightHistory.d.ts +2 -2
  37. package/dist/movethewheels/components/freight-invoice/InvoiceForm.d.ts +19 -0
  38. package/dist/movethewheels/components/freight-invoice/InvoiceResults.d.ts +9 -0
  39. package/dist/movethewheels/hooks/useFleet.d.ts +1 -1
  40. package/dist/movethewheels/hooks/useFleetIntelligence.d.ts +1 -1
  41. package/dist/movethewheels/hooks/useInvoiceComparisonRecovery.d.ts +22 -0
  42. package/dist/movethewheels/hooks/useProcedureSelection.d.ts +1 -3
  43. package/dist/movethewheels/pages/FreightInvoicePage.d.ts +10 -0
  44. package/dist/movethewheels/utils/driverLicenseExpiry.d.ts +6 -0
  45. package/dist/movethewheels/utils/freightCommercial.d.ts +44 -0
  46. package/dist/movethewheels/utils/invoiceComparison.d.ts +32 -0
  47. package/dist/pages/DriverDetailsPage.js +306 -282
  48. package/dist/pages/FreightEstimatePage.js +97 -92
  49. package/dist/pages/FreightInvoicePage.js +346 -0
  50. package/dist/src/generated/consolidation-operations.js +3961 -503
  51. package/dist/src/generated/container-condition-operations.js +2968 -206
  52. package/dist/src/generated/container-operations.js +997 -99
  53. package/dist/src/generated/empty-reuse-operations.js +4811 -747
  54. package/dist/src/generated/free-time-operations.js +2824 -233
  55. package/dist/src/generated/freight-cost-operations.js +4852 -323
  56. package/dist/src/generated/freight-invoice-operations.js +4894 -0
  57. package/dist/src/generated/logistics-procedure-operations.js +2707 -360
  58. package/dist/src/generated/pending-pick-operations.js +8244 -1196
  59. package/dist/src/generated/shipment-status-authority-operations.js +1157 -84
  60. package/dist/src/generated/stock-binding-operations.js +2779 -173
  61. package/dist/src/generated/stock-observation-operations.js +1274 -91
  62. package/dist/src/generated/tracking-quality-operations.js +407 -42
  63. package/dist/src/generated/trip-utilization-operations.js +1765 -138
  64. package/dist/src/generated/wspace-operations.js +428 -706
  65. package/dist/utils/driverLicenseExpiry.js +14 -0
  66. package/dist/utils/freightAcknowledgement.js +6 -5
  67. package/dist/utils/freightCommercial.js +194 -0
  68. package/dist/utils/freightRecovery.js +22 -22
  69. package/dist/utils/invoiceComparison.js +272 -0
  70. package/package.json +30 -29
  71. package/src/generated/consolidation-operations.ts +60 -559
  72. package/src/generated/container-condition-operations.ts +63 -265
  73. package/src/generated/container-operations.ts +37 -132
  74. package/src/generated/empty-reuse-operations.ts +83 -824
  75. package/src/generated/free-time-operations.ts +55 -284
  76. package/src/generated/freight-cost-operations.ts +373 -349
  77. package/src/generated/freight-invoice-operations.ts +830 -0
  78. package/src/generated/logistics-procedure-operations.ts +60 -414
  79. package/src/generated/pending-pick-operations.ts +48 -1239
  80. package/src/generated/shipment-status-authority-operations.ts +33 -113
  81. package/src/generated/stock-binding-operations.ts +29 -198
  82. package/src/generated/stock-observation-operations.ts +24 -111
  83. package/src/generated/tracking-quality-operations.ts +19 -57
  84. package/src/generated/trip-utilization-operations.ts +32 -166
  85. package/src/generated/wspace-operations.ts +284 -2906
  86. package/src/generated/wspace-types.ts +2 -8
  87. package/src/movethewheels/MoveTheWheelsRoot.tsx +6 -0
  88. package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
  89. package/src/movethewheels/components/ContainerEvidencePicker.tsx +12 -4
  90. package/src/movethewheels/components/FleetAlertsPanel.tsx +4 -2
  91. package/src/movethewheels/components/freight-cost/FreightCommercialEditor.tsx +236 -0
  92. package/src/movethewheels/components/freight-cost/FreightCommercialFactEditor.tsx +258 -0
  93. package/src/movethewheels/components/freight-cost/FreightCommercialHistory.tsx +90 -0
  94. package/src/movethewheels/components/freight-cost/FreightDraftForm.tsx +33 -4
  95. package/src/movethewheels/components/freight-cost/FreightHistory.tsx +4 -2
  96. package/src/movethewheels/components/freight-invoice/InvoiceForm.tsx +412 -0
  97. package/src/movethewheels/components/freight-invoice/InvoiceResults.tsx +162 -0
  98. package/src/movethewheels/hooks/useFleet.ts +1 -1
  99. package/src/movethewheels/hooks/useFleetIntelligence.ts +5 -1
  100. package/src/movethewheels/hooks/useFreightRecoveryAction.ts +2 -1
  101. package/src/movethewheels/hooks/useInvoiceComparisonRecovery.ts +227 -0
  102. package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
  103. package/src/movethewheels/pages/FreightEstimatePage.tsx +15 -7
  104. package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
  105. package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
  106. package/src/movethewheels/utils/freightAcknowledgement.ts +3 -1
  107. package/src/movethewheels/utils/freightCommercial.ts +388 -0
  108. package/src/movethewheels/utils/freightRecovery.ts +6 -2
  109. package/src/movethewheels/utils/invoiceComparison.ts +520 -0
  110. package/src/operations/freight-cost/commercial.graphql +483 -0
  111. package/src/operations/freight-invoice/comparison.graphql +40 -0
  112. package/src/operations/wspace/freight-invoice-evidence.graphql +6 -0
  113. 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
+ }