@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.
- package/README.md +10 -0
- package/dist/MoveTheWheelsRoot.js +5 -0
- package/dist/MoveTheWheelsRoutes.js +25 -21
- package/dist/components/ContainerEvidencePicker.js +55 -55
- package/dist/components/FleetAlertsPanel.js +2 -2
- package/dist/components/freight-cost/FreightCommercialEditor.js +217 -0
- package/dist/components/freight-cost/FreightCommercialFactEditor.js +217 -0
- package/dist/components/freight-cost/FreightCommercialHistory.js +115 -0
- package/dist/components/freight-cost/FreightDraftForm.js +106 -91
- package/dist/components/freight-cost/FreightHistory.js +42 -40
- package/dist/components/freight-invoice/InvoiceForm.js +366 -0
- package/dist/components/freight-invoice/InvoiceResults.js +240 -0
- package/dist/generated/consolidation-operations.d.ts +195 -265
- package/dist/generated/container-condition-operations.d.ts +124 -187
- package/dist/generated/container-operations.d.ts +59 -85
- package/dist/generated/empty-reuse-operations.d.ts +165 -271
- package/dist/generated/free-time-operations.d.ts +120 -184
- package/dist/generated/freight-cost-operations.d.ts +955 -165
- package/dist/generated/freight-invoice-operations.d.ts +1447 -0
- package/dist/generated/logistics-procedure-operations.d.ts +100 -156
- package/dist/generated/pending-pick-operations.d.ts +769 -1025
- package/dist/generated/shipment-status-authority-operations.d.ts +82 -103
- package/dist/generated/stock-binding-operations.d.ts +188 -260
- package/dist/generated/stock-observation-operations.d.ts +103 -138
- package/dist/generated/tracking-quality-operations.d.ts +27 -41
- package/dist/generated/trip-utilization-operations.d.ts +137 -185
- package/dist/generated/wspace-operations.d.ts +628 -2272
- package/dist/generated/wspace-types.d.ts +4 -23
- package/dist/hooks/useFreightRecoveryAction.js +20 -20
- package/dist/hooks/useInvoiceComparisonRecovery.js +118 -0
- package/dist/movethewheels/components/ContainerEvidencePicker.d.ts +2 -1
- package/dist/movethewheels/components/freight-cost/FreightCommercialEditor.d.ts +8 -0
- package/dist/movethewheels/components/freight-cost/FreightCommercialFactEditor.d.ts +8 -0
- package/dist/movethewheels/components/freight-cost/FreightCommercialHistory.d.ts +4 -0
- package/dist/movethewheels/components/freight-cost/FreightDraftForm.d.ts +4 -2
- package/dist/movethewheels/components/freight-cost/FreightHistory.d.ts +2 -2
- package/dist/movethewheels/components/freight-invoice/InvoiceForm.d.ts +19 -0
- package/dist/movethewheels/components/freight-invoice/InvoiceResults.d.ts +9 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +1 -1
- package/dist/movethewheels/hooks/useFleetIntelligence.d.ts +1 -1
- package/dist/movethewheels/hooks/useInvoiceComparisonRecovery.d.ts +22 -0
- package/dist/movethewheels/hooks/useProcedureSelection.d.ts +1 -3
- package/dist/movethewheels/pages/FreightInvoicePage.d.ts +10 -0
- package/dist/movethewheels/utils/driverLicenseExpiry.d.ts +6 -0
- package/dist/movethewheels/utils/freightCommercial.d.ts +44 -0
- package/dist/movethewheels/utils/invoiceComparison.d.ts +32 -0
- package/dist/pages/DriverDetailsPage.js +306 -282
- package/dist/pages/FreightEstimatePage.js +97 -92
- package/dist/pages/FreightInvoicePage.js +346 -0
- package/dist/src/generated/consolidation-operations.js +3961 -503
- package/dist/src/generated/container-condition-operations.js +2968 -206
- package/dist/src/generated/container-operations.js +997 -99
- package/dist/src/generated/empty-reuse-operations.js +4811 -747
- package/dist/src/generated/free-time-operations.js +2824 -233
- package/dist/src/generated/freight-cost-operations.js +4852 -323
- package/dist/src/generated/freight-invoice-operations.js +4894 -0
- package/dist/src/generated/logistics-procedure-operations.js +2707 -360
- package/dist/src/generated/pending-pick-operations.js +8244 -1196
- package/dist/src/generated/shipment-status-authority-operations.js +1157 -84
- package/dist/src/generated/stock-binding-operations.js +2779 -173
- package/dist/src/generated/stock-observation-operations.js +1274 -91
- package/dist/src/generated/tracking-quality-operations.js +407 -42
- package/dist/src/generated/trip-utilization-operations.js +1765 -138
- package/dist/src/generated/wspace-operations.js +428 -706
- package/dist/utils/driverLicenseExpiry.js +14 -0
- package/dist/utils/freightAcknowledgement.js +6 -5
- package/dist/utils/freightCommercial.js +194 -0
- package/dist/utils/freightRecovery.js +22 -22
- package/dist/utils/invoiceComparison.js +272 -0
- package/package.json +30 -29
- package/src/generated/consolidation-operations.ts +60 -559
- package/src/generated/container-condition-operations.ts +63 -265
- package/src/generated/container-operations.ts +37 -132
- package/src/generated/empty-reuse-operations.ts +83 -824
- package/src/generated/free-time-operations.ts +55 -284
- package/src/generated/freight-cost-operations.ts +373 -349
- package/src/generated/freight-invoice-operations.ts +830 -0
- package/src/generated/logistics-procedure-operations.ts +60 -414
- package/src/generated/pending-pick-operations.ts +48 -1239
- package/src/generated/shipment-status-authority-operations.ts +33 -113
- package/src/generated/stock-binding-operations.ts +29 -198
- package/src/generated/stock-observation-operations.ts +24 -111
- package/src/generated/tracking-quality-operations.ts +19 -57
- package/src/generated/trip-utilization-operations.ts +32 -166
- package/src/generated/wspace-operations.ts +284 -2906
- package/src/generated/wspace-types.ts +2 -8
- package/src/movethewheels/MoveTheWheelsRoot.tsx +6 -0
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
- package/src/movethewheels/components/ContainerEvidencePicker.tsx +12 -4
- package/src/movethewheels/components/FleetAlertsPanel.tsx +4 -2
- package/src/movethewheels/components/freight-cost/FreightCommercialEditor.tsx +236 -0
- package/src/movethewheels/components/freight-cost/FreightCommercialFactEditor.tsx +258 -0
- package/src/movethewheels/components/freight-cost/FreightCommercialHistory.tsx +90 -0
- package/src/movethewheels/components/freight-cost/FreightDraftForm.tsx +33 -4
- package/src/movethewheels/components/freight-cost/FreightHistory.tsx +4 -2
- package/src/movethewheels/components/freight-invoice/InvoiceForm.tsx +412 -0
- package/src/movethewheels/components/freight-invoice/InvoiceResults.tsx +162 -0
- package/src/movethewheels/hooks/useFleet.ts +1 -1
- package/src/movethewheels/hooks/useFleetIntelligence.ts +5 -1
- package/src/movethewheels/hooks/useFreightRecoveryAction.ts +2 -1
- package/src/movethewheels/hooks/useInvoiceComparisonRecovery.ts +227 -0
- package/src/movethewheels/pages/DriverDetailsPage.tsx +35 -9
- package/src/movethewheels/pages/FreightEstimatePage.tsx +15 -7
- package/src/movethewheels/pages/FreightInvoicePage.tsx +477 -0
- package/src/movethewheels/utils/driverLicenseExpiry.ts +25 -0
- package/src/movethewheels/utils/freightAcknowledgement.ts +3 -1
- package/src/movethewheels/utils/freightCommercial.ts +388 -0
- package/src/movethewheels/utils/freightRecovery.ts +6 -2
- package/src/movethewheels/utils/invoiceComparison.ts +520 -0
- package/src/operations/freight-cost/commercial.graphql +483 -0
- package/src/operations/freight-invoice/comparison.graphql +40 -0
- package/src/operations/wspace/freight-invoice-evidence.graphql +6 -0
- 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
|
+
}
|