@crewhaus/tool-money 0.7.0

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.
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Payment identifier validation.
3
+ *
4
+ * Every one of these carries a checksum precisely so a typo is caught before
5
+ * money moves, and every one of them is arithmetic — there is nothing for a
6
+ * model to judge. Getting it wrong is expensive in a way that most wrong
7
+ * answers are not, so each algorithm here is the published one and the tests
8
+ * use published vectors.
9
+ *
10
+ * A valid checksum means the identifier is well-formed. It does not mean the
11
+ * account exists, is open, or belongs to who you think. Nothing here reaches
12
+ * a network, and the result says as much.
13
+ */
14
+ export const IDENTIFIER_KINDS = ["iban", "bic", "aba", "card", "sortcode"];
15
+ /**
16
+ * IBAN length by country. The length is part of the standard, so a string
17
+ * that passes mod-97 at the wrong length is still not an IBAN — and mod-97
18
+ * alone accepts plenty of those.
19
+ */
20
+ const IBAN_LENGTHS = new Map(Object.entries({
21
+ AD: 24,
22
+ AE: 23,
23
+ AL: 28,
24
+ AT: 20,
25
+ AZ: 28,
26
+ BA: 20,
27
+ BE: 16,
28
+ BG: 22,
29
+ BH: 22,
30
+ BR: 29,
31
+ BY: 28,
32
+ CH: 21,
33
+ CR: 22,
34
+ CY: 28,
35
+ CZ: 24,
36
+ DE: 22,
37
+ DK: 18,
38
+ DO: 28,
39
+ EE: 20,
40
+ EG: 29,
41
+ ES: 24,
42
+ FI: 18,
43
+ FO: 18,
44
+ FR: 27,
45
+ GB: 22,
46
+ GE: 22,
47
+ GI: 23,
48
+ GL: 18,
49
+ GR: 27,
50
+ GT: 28,
51
+ HR: 21,
52
+ HU: 28,
53
+ IE: 22,
54
+ IL: 23,
55
+ IQ: 23,
56
+ IS: 26,
57
+ IT: 27,
58
+ JO: 30,
59
+ KW: 30,
60
+ KZ: 20,
61
+ LB: 28,
62
+ LC: 32,
63
+ LI: 21,
64
+ LT: 20,
65
+ LU: 20,
66
+ LV: 21,
67
+ LY: 25,
68
+ MC: 27,
69
+ MD: 24,
70
+ ME: 22,
71
+ MK: 19,
72
+ MR: 27,
73
+ MT: 31,
74
+ MU: 30,
75
+ NL: 18,
76
+ NO: 15,
77
+ PK: 24,
78
+ PL: 28,
79
+ PS: 29,
80
+ PT: 25,
81
+ QA: 29,
82
+ RO: 24,
83
+ RS: 22,
84
+ SA: 24,
85
+ SC: 31,
86
+ SD: 18,
87
+ SE: 24,
88
+ SI: 19,
89
+ SK: 24,
90
+ SM: 27,
91
+ ST: 25,
92
+ SV: 28,
93
+ TL: 23,
94
+ TN: 24,
95
+ TR: 26,
96
+ UA: 29,
97
+ VA: 22,
98
+ VG: 24,
99
+ XK: 20,
100
+ }).map(([k, v]) => [k, v]));
101
+ const strip = (raw) => raw.replace(/[\s-]/g, "").toUpperCase();
102
+ /**
103
+ * ISO 7064 MOD-97-10, computed a chunk at a time.
104
+ *
105
+ * The whole rearranged IBAN as one integer is up to 36 digits, past what a
106
+ * JS number holds exactly — so it is folded 7 digits at a time, which is the
107
+ * standard's own suggestion and stays inside the safe integer range.
108
+ */
109
+ function mod97(digits) {
110
+ let remainder = 0;
111
+ for (let i = 0; i < digits.length; i += 7) {
112
+ remainder = Number(`${remainder}${digits.slice(i, i + 7)}`) % 97;
113
+ }
114
+ return remainder;
115
+ }
116
+ export function validateIban(raw) {
117
+ const normalized = strip(raw);
118
+ const base = {
119
+ kind: "iban",
120
+ normalized,
121
+ formatted: (normalized.match(/.{1,4}/g) ?? []).join(" "),
122
+ parts: {},
123
+ };
124
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(normalized)) {
125
+ return {
126
+ ...base,
127
+ valid: false,
128
+ reason: "not in IBAN form: two letters, two digits, then alphanumerics",
129
+ };
130
+ }
131
+ const country = normalized.slice(0, 2);
132
+ const expected = IBAN_LENGTHS.get(country);
133
+ if (expected === undefined) {
134
+ return { ...base, valid: false, reason: `"${country}" is not a country that issues IBANs` };
135
+ }
136
+ if (normalized.length !== expected) {
137
+ return {
138
+ ...base,
139
+ valid: false,
140
+ reason: `${country} IBANs are ${expected} characters; this is ${normalized.length}`,
141
+ };
142
+ }
143
+ // Move the first four characters to the end, then letters become numbers.
144
+ const rearranged = normalized.slice(4) + normalized.slice(0, 4);
145
+ const digits = [...rearranged]
146
+ .map((ch) => (/[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch))
147
+ .join("");
148
+ const ok = mod97(digits) === 1;
149
+ return {
150
+ ...base,
151
+ valid: ok,
152
+ reason: ok ? "" : "the check digits do not match the rest of the IBAN",
153
+ parts: { country, checkDigits: normalized.slice(2, 4), bban: normalized.slice(4) },
154
+ };
155
+ }
156
+ export function validateBic(raw) {
157
+ const normalized = strip(raw);
158
+ const match = /^([A-Z]{4})([A-Z]{2})([A-Z0-9]{2})([A-Z0-9]{3})?$/.exec(normalized);
159
+ if (!match) {
160
+ return {
161
+ kind: "bic",
162
+ valid: false,
163
+ normalized,
164
+ formatted: normalized,
165
+ reason: "a BIC is 8 or 11 characters: 4 bank, 2 country, 2 location, optionally 3 branch",
166
+ parts: {},
167
+ };
168
+ }
169
+ return {
170
+ kind: "bic",
171
+ valid: true,
172
+ normalized,
173
+ formatted: normalized,
174
+ reason: "",
175
+ parts: {
176
+ bank: match[1],
177
+ country: match[2],
178
+ location: match[3],
179
+ branch: match[4] ?? "",
180
+ },
181
+ };
182
+ }
183
+ /** ABA routing number: a weighted sum over nine digits, 3-7-1 repeating. */
184
+ export function validateAba(raw) {
185
+ const normalized = raw.replace(/[\s-]/g, "");
186
+ const base = { kind: "aba", normalized, formatted: normalized, parts: {} };
187
+ if (!/^\d{9}$/.test(normalized)) {
188
+ return { ...base, valid: false, reason: "a routing number is exactly nine digits" };
189
+ }
190
+ const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1];
191
+ const sum = [...normalized].reduce((total, digit, i) => total + Number(digit) * weights[i], 0);
192
+ const ok = sum % 10 === 0;
193
+ return {
194
+ ...base,
195
+ valid: ok,
196
+ reason: ok ? "" : "the checksum does not match",
197
+ parts: {
198
+ federalReserve: normalized.slice(0, 4),
199
+ institution: normalized.slice(4, 8),
200
+ check: normalized.slice(8),
201
+ },
202
+ };
203
+ }
204
+ /** Luhn, as used by payment cards. */
205
+ export function luhnOk(digits) {
206
+ let sum = 0;
207
+ let double = false;
208
+ for (let i = digits.length - 1; i >= 0; i--) {
209
+ let value = Number(digits[i]);
210
+ if (double) {
211
+ value *= 2;
212
+ if (value > 9)
213
+ value -= 9;
214
+ }
215
+ sum += value;
216
+ double = !double;
217
+ }
218
+ return sum % 10 === 0;
219
+ }
220
+ /** Card brands, by the prefixes and lengths the networks publish. */
221
+ function cardBrand(digits) {
222
+ if (/^4\d{12}(\d{3})?(\d{3})?$/.test(digits))
223
+ return "visa";
224
+ if (/^(5[1-5]\d{14}|2(2[2-9]\d{12}|[3-6]\d{13}|7[01]\d{12}|720\d{12}))$/.test(digits))
225
+ return "mastercard";
226
+ if (/^3[47]\d{13}$/.test(digits))
227
+ return "amex";
228
+ if (/^(6011\d{12}|65\d{14}|64[4-9]\d{13})$/.test(digits))
229
+ return "discover";
230
+ if (/^3(0[0-5]|[68]\d)\d{11}$/.test(digits))
231
+ return "diners";
232
+ if (/^35(2[89]|[3-8]\d)\d{12}$/.test(digits))
233
+ return "jcb";
234
+ return "unknown";
235
+ }
236
+ /**
237
+ * Validate a card number's checksum.
238
+ *
239
+ * Only the last four digits are ever echoed. A validator that returned the
240
+ * number it was given would take a value the caller has to protect and copy
241
+ * it into a log, a transcript and a model's context.
242
+ */
243
+ export function validateCard(raw) {
244
+ const digits = raw.replace(/[\s-]/g, "");
245
+ const masked = digits.length >= 4 ? `${"*".repeat(Math.max(0, digits.length - 4))}${digits.slice(-4)}` : "";
246
+ const base = { kind: "card", normalized: masked, formatted: masked, parts: {} };
247
+ if (!/^\d{12,19}$/.test(digits)) {
248
+ return { ...base, valid: false, reason: "a card number is 12 to 19 digits" };
249
+ }
250
+ const ok = luhnOk(digits);
251
+ return {
252
+ ...base,
253
+ valid: ok,
254
+ reason: ok ? "" : "the Luhn checksum does not match",
255
+ parts: { brand: cardBrand(digits), last4: digits.slice(-4), length: String(digits.length) },
256
+ };
257
+ }
258
+ /** UK sort code: six digits, and no checksum exists to check. */
259
+ export function validateSortCode(raw) {
260
+ const normalized = raw.replace(/[\s-]/g, "");
261
+ const ok = /^\d{6}$/.test(normalized);
262
+ return {
263
+ kind: "sortcode",
264
+ valid: ok,
265
+ normalized,
266
+ formatted: ok ? (normalized.match(/.{2}/g) ?? []).join("-") : normalized,
267
+ // Said plainly: a well-formed sort code is not a verified one, and a
268
+ // caller that reads `valid` as "this bank exists" would be wrong.
269
+ reason: ok ? "" : "a sort code is exactly six digits",
270
+ parts: ok ? { note: "a sort code carries no checksum; only its shape was checked" } : {},
271
+ };
272
+ }
273
+ /** Guess which kind an identifier is, for callers that do not know. */
274
+ export function detectKind(raw) {
275
+ const normalized = strip(raw);
276
+ if (/^[A-Z]{2}\d{2}[A-Z0-9]{10,30}$/.test(normalized) &&
277
+ IBAN_LENGTHS.has(normalized.slice(0, 2))) {
278
+ return "iban";
279
+ }
280
+ if (/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/.test(normalized))
281
+ return "bic";
282
+ if (/^\d{9}$/.test(normalized))
283
+ return "aba";
284
+ if (/^\d{6}$/.test(normalized))
285
+ return "sortcode";
286
+ if (/^\d{12,19}$/.test(normalized))
287
+ return "card";
288
+ return null;
289
+ }
290
+ export function validateIdentifier(raw, kind) {
291
+ switch (kind) {
292
+ case "iban":
293
+ return validateIban(raw);
294
+ case "bic":
295
+ return validateBic(raw);
296
+ case "aba":
297
+ return validateAba(raw);
298
+ case "card":
299
+ return validateCard(raw);
300
+ case "sortcode":
301
+ return validateSortCode(raw);
302
+ }
303
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Lot consumption and realized gain.
3
+ *
4
+ * Which lots a disposal consumes is a policy choice with a tax consequence,
5
+ * and once the policy is chosen the arithmetic is fixed. A model asked to do
6
+ * this will produce a plausible number that cannot be reproduced next
7
+ * quarter, which is the one property a cost-basis figure must have.
8
+ *
9
+ * Cost is integer minor units. Quantity is a decimal, because a lot can be
10
+ * 0.37 of a share or of a coin — but the cost attached to a lot is tracked
11
+ * as an integer remainder and the final consumption takes whatever is left,
12
+ * so a fully consumed lot always accounts for exactly its cost with no
13
+ * rounding drift.
14
+ */
15
+ export declare const LOT_METHODS: readonly ["fifo", "lifo", "hifo", "specific"];
16
+ export type LotMethod = (typeof LOT_METHODS)[number];
17
+ export type Lot = {
18
+ readonly id: string;
19
+ /** ISO-8601 with an offset. Acquisition time. */
20
+ readonly acquiredAt: string;
21
+ readonly quantity: number;
22
+ /** What the whole lot cost, in minor units. */
23
+ readonly costMinor: number;
24
+ };
25
+ export type Disposal = {
26
+ readonly id: string;
27
+ readonly disposedAt: string;
28
+ readonly quantity: number;
29
+ /** What the whole disposal realized, in minor units. */
30
+ readonly proceedsMinor: number;
31
+ /** `specific` only: which lots to consume, in order. */
32
+ readonly lotIds?: ReadonlyArray<string>;
33
+ };
34
+ export type Consumption = {
35
+ readonly lotId: string;
36
+ readonly quantity: number;
37
+ readonly costMinor: number;
38
+ readonly acquiredAt: string;
39
+ /** True when held over a year, which most jurisdictions treat differently. */
40
+ readonly longTerm: boolean;
41
+ };
42
+ export type DisposalResult = {
43
+ readonly id: string;
44
+ readonly quantity: number;
45
+ readonly proceedsMinor: number;
46
+ readonly costMinor: number;
47
+ readonly gainMinor: number;
48
+ readonly shortTermGainMinor: number;
49
+ readonly longTermGainMinor: number;
50
+ readonly consumed: ReadonlyArray<Consumption>;
51
+ };
52
+ export type CostBasisResult = {
53
+ readonly method: LotMethod;
54
+ readonly disposals: ReadonlyArray<DisposalResult>;
55
+ readonly realizedGainMinor: number;
56
+ readonly remainingLots: ReadonlyArray<Lot>;
57
+ readonly remainingQuantity: number;
58
+ readonly remainingCostMinor: number;
59
+ };
60
+ export declare function computeCostBasis(lots: ReadonlyArray<Lot>, disposals: ReadonlyArray<Disposal>, method: LotMethod): CostBasisResult;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Lot consumption and realized gain.
3
+ *
4
+ * Which lots a disposal consumes is a policy choice with a tax consequence,
5
+ * and once the policy is chosen the arithmetic is fixed. A model asked to do
6
+ * this will produce a plausible number that cannot be reproduced next
7
+ * quarter, which is the one property a cost-basis figure must have.
8
+ *
9
+ * Cost is integer minor units. Quantity is a decimal, because a lot can be
10
+ * 0.37 of a share or of a coin — but the cost attached to a lot is tracked
11
+ * as an integer remainder and the final consumption takes whatever is left,
12
+ * so a fully consumed lot always accounts for exactly its cost with no
13
+ * rounding drift.
14
+ */
15
+ export const LOT_METHODS = ["fifo", "lifo", "hifo", "specific"];
16
+ const YEAR_MS = 365 * 24 * 60 * 60 * 1000;
17
+ function instant(value, what) {
18
+ if (!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(value.trim())) {
19
+ throw new Error(`${what} ("${value}") has no UTC offset — an offset-less timestamp means local time and would put a holding period on the wrong side of a year boundary depending on the machine`);
20
+ }
21
+ const parsed = Date.parse(value);
22
+ if (Number.isNaN(parsed))
23
+ throw new Error(`${what} ("${value}") is not a valid ISO-8601 instant`);
24
+ return parsed;
25
+ }
26
+ export function computeCostBasis(lots, disposals, method) {
27
+ const seen = new Set();
28
+ for (const lot of lots) {
29
+ if (seen.has(lot.id))
30
+ throw new Error(`two lots share the id "${lot.id}"`);
31
+ seen.add(lot.id);
32
+ if (lot.quantity <= 0)
33
+ throw new Error(`lot "${lot.id}" has quantity ${lot.quantity}`);
34
+ if (!Number.isInteger(lot.costMinor))
35
+ throw new Error(`lot "${lot.id}" cost must be minor units`);
36
+ instant(lot.acquiredAt, `lot "${lot.id}" acquiredAt`);
37
+ }
38
+ // Working copies: remaining cost is an integer so a lot cannot drift.
39
+ const open = lots.map((lot) => ({
40
+ ...lot,
41
+ remainingQuantity: lot.quantity,
42
+ remainingCostMinor: lot.costMinor,
43
+ acquiredMs: instant(lot.acquiredAt, "acquiredAt"),
44
+ }));
45
+ const results = [];
46
+ for (const disposal of disposals) {
47
+ const disposedMs = instant(disposal.disposedAt, `disposal "${disposal.id}" disposedAt`);
48
+ if (disposal.quantity <= 0) {
49
+ throw new Error(`disposal "${disposal.id}" has quantity ${disposal.quantity}`);
50
+ }
51
+ let order = open.filter((l) => l.remainingQuantity > 0);
52
+ if (method === "specific") {
53
+ const wanted = disposal.lotIds;
54
+ if (!wanted || wanted.length === 0) {
55
+ throw new Error(`disposal "${disposal.id}" uses the specific-identification method but names no lots`);
56
+ }
57
+ const index = new Map(order.map((l) => [l.id, l]));
58
+ order = wanted.map((id) => {
59
+ const lot = index.get(id);
60
+ if (!lot) {
61
+ throw new Error(`disposal "${disposal.id}" names lot "${id}", which is not open`);
62
+ }
63
+ return lot;
64
+ });
65
+ }
66
+ else if (method === "fifo") {
67
+ order = [...order].sort((a, b) => a.acquiredMs - b.acquiredMs || (a.id < b.id ? -1 : 1));
68
+ }
69
+ else if (method === "lifo") {
70
+ order = [...order].sort((a, b) => b.acquiredMs - a.acquiredMs || (a.id < b.id ? -1 : 1));
71
+ }
72
+ else {
73
+ // Highest cost per unit first, which realizes the smallest gain.
74
+ order = [...order].sort((a, b) => b.remainingCostMinor / b.remainingQuantity - a.remainingCostMinor / a.remainingQuantity ||
75
+ (a.id < b.id ? -1 : 1));
76
+ }
77
+ let toConsume = disposal.quantity;
78
+ const consumed = [];
79
+ let costMinor = 0;
80
+ for (const lot of order) {
81
+ if (toConsume <= 0)
82
+ break;
83
+ const take = Math.min(toConsume, lot.remainingQuantity);
84
+ // Taking the whole remainder consumes the whole remaining cost. That
85
+ // is what keeps a lot's costs summing to exactly what it cost, however
86
+ // many partial disposals came before.
87
+ const takeCost = take === lot.remainingQuantity
88
+ ? lot.remainingCostMinor
89
+ : Math.round((lot.remainingCostMinor * take) / lot.remainingQuantity);
90
+ lot.remainingQuantity -= take;
91
+ lot.remainingCostMinor -= takeCost;
92
+ toConsume -= take;
93
+ costMinor += takeCost;
94
+ consumed.push({
95
+ lotId: lot.id,
96
+ quantity: take,
97
+ costMinor: takeCost,
98
+ acquiredAt: lot.acquiredAt,
99
+ longTerm: disposedMs - lot.acquiredMs > YEAR_MS,
100
+ });
101
+ }
102
+ if (toConsume > 1e-9) {
103
+ throw new Error(`disposal "${disposal.id}" needs ${disposal.quantity} but only ${disposal.quantity - toConsume} was open — a disposal cannot exceed the lots held`);
104
+ }
105
+ // Proceeds follow the units, so the split between short and long term is
106
+ // allocated by quantity rather than by cost.
107
+ const gainMinor = disposal.proceedsMinor - costMinor;
108
+ let longTermProceeds = 0;
109
+ let longTermCost = 0;
110
+ for (const entry of consumed) {
111
+ if (!entry.longTerm)
112
+ continue;
113
+ longTermProceeds += Math.round((disposal.proceedsMinor * entry.quantity) / disposal.quantity);
114
+ longTermCost += entry.costMinor;
115
+ }
116
+ const longTermGainMinor = longTermProceeds - longTermCost;
117
+ results.push({
118
+ id: disposal.id,
119
+ quantity: disposal.quantity,
120
+ proceedsMinor: disposal.proceedsMinor,
121
+ costMinor,
122
+ gainMinor,
123
+ longTermGainMinor,
124
+ shortTermGainMinor: gainMinor - longTermGainMinor,
125
+ consumed,
126
+ });
127
+ }
128
+ const remaining = open.filter((l) => l.remainingQuantity > 0);
129
+ return {
130
+ method,
131
+ disposals: results,
132
+ realizedGainMinor: results.reduce((s, r) => s + r.gainMinor, 0),
133
+ remainingLots: remaining.map((l) => ({
134
+ id: l.id,
135
+ acquiredAt: l.acquiredAt,
136
+ quantity: l.remainingQuantity,
137
+ costMinor: l.remainingCostMinor,
138
+ })),
139
+ remainingQuantity: remaining.reduce((s, l) => s + l.remainingQuantity, 0),
140
+ remainingCostMinor: remaining.reduce((s, l) => s + l.remainingCostMinor, 0),
141
+ };
142
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Three-way match: invoice against purchase order against goods receipt.
3
+ *
4
+ * This is the standard accounts-payable control, and it is entirely
5
+ * mechanical — pair the lines, compare price and quantity, and report what
6
+ * falls outside tolerance. A model reading two documents to decide whether
7
+ * 4,800 matches 4,750 is being paid to do subtraction, and it will sometimes
8
+ * get it wrong on an invoice nobody re-reads.
9
+ */
10
+ export type MatchLine = {
11
+ readonly id: string;
12
+ /** Explicit link to a PO line, when the document carries one. */
13
+ readonly poLineId?: string;
14
+ readonly sku?: string;
15
+ readonly description?: string;
16
+ readonly quantity: number;
17
+ readonly unitPriceMinor: number;
18
+ };
19
+ export type Tolerance = {
20
+ /** Allowed unit-price variance, in basis points. 100 is 1%. */
21
+ readonly pricePercentBps?: number;
22
+ /** Allowed variance in absolute minor units, whichever is larger. */
23
+ readonly priceAbsoluteMinor?: number;
24
+ readonly quantityPercentBps?: number;
25
+ readonly quantityAbsolute?: number;
26
+ };
27
+ export declare const MATCH_STATUSES: readonly ["matched", "price-variance", "quantity-variance", "both-variance", "over-receipt", "not-received"];
28
+ export type MatchStatus = (typeof MATCH_STATUSES)[number];
29
+ export type MatchPair = {
30
+ readonly invoiceLineId: string;
31
+ readonly poLineId: string;
32
+ readonly matchedBy: "reference" | "sku" | "description";
33
+ readonly status: MatchStatus;
34
+ readonly invoiceUnitPriceMinor: number;
35
+ readonly poUnitPriceMinor: number;
36
+ readonly priceDeltaMinor: number;
37
+ readonly invoiceQuantity: number;
38
+ readonly poQuantity: number;
39
+ readonly receivedQuantity: number | null;
40
+ readonly quantityDelta: number;
41
+ /** What the variance costs, positive when the invoice asks for more. */
42
+ readonly exposureMinor: number;
43
+ readonly reasons: ReadonlyArray<string>;
44
+ };
45
+ export type MatchReport = {
46
+ readonly pairs: ReadonlyArray<MatchPair>;
47
+ readonly unmatchedInvoiceLines: ReadonlyArray<string>;
48
+ readonly unmatchedPoLines: ReadonlyArray<string>;
49
+ readonly ok: boolean;
50
+ readonly exceptions: number;
51
+ /** Net amount the invoice claims above the order, across every exception. */
52
+ readonly totalExposureMinor: number;
53
+ readonly threeWay: boolean;
54
+ };
55
+ export declare function matchInvoiceToPurchaseOrder(invoiceLines: ReadonlyArray<MatchLine>, poLines: ReadonlyArray<MatchLine>, receiptLines?: ReadonlyArray<{
56
+ readonly poLineId: string;
57
+ readonly quantity: number;
58
+ }>, tolerance?: Tolerance): MatchReport;