@gabrielerandelli/minus-tracker 0.6.0 → 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.
- package/dist/cli/index.js +572 -171
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +295 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +295 -21
- package/dist/index.js.map +1 -1
- package/package.json +10 -3
package/dist/index.cjs
CHANGED
|
@@ -152,6 +152,10 @@ function warningToEnglish(w) {
|
|
|
152
152
|
return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} \u2014 skipped`;
|
|
153
153
|
case "QUANTITY_ZERO":
|
|
154
154
|
return `Row ${w.row}: quantity is 0 \u2014 skipped`;
|
|
155
|
+
case "MISSING_ISIN_INCOME":
|
|
156
|
+
return `Row ${w.row}: blank ISIN on income row \u2014 skipped`;
|
|
157
|
+
case "ORPHAN_WITHHOLDING":
|
|
158
|
+
return `Withholding row for ${w.isin} on ${w.date}: no matching income row \u2014 skipped`;
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -167,6 +171,8 @@ var REQUIRED_COLUMNS = [
|
|
|
167
171
|
"Transaction costs currency",
|
|
168
172
|
"Product"
|
|
169
173
|
];
|
|
174
|
+
var INCOME_KEYWORDS = ["DIVIDEND", "COUPON", "INTEREST", "CEDOLA"];
|
|
175
|
+
var TAX_KEYWORDS = ["DIVIDEND TAX", "WITHHOLDING", "RITENUTA"];
|
|
170
176
|
function parseCSVRow(line) {
|
|
171
177
|
const fields = [];
|
|
172
178
|
let i = 0;
|
|
@@ -208,6 +214,7 @@ function parseDate(ddmmyyyy) {
|
|
|
208
214
|
var DEGIROParser = class {
|
|
209
215
|
_warningEntries = [];
|
|
210
216
|
_snapshot;
|
|
217
|
+
_incomeRows = [];
|
|
211
218
|
constructor(snapshot) {
|
|
212
219
|
this._snapshot = snapshot;
|
|
213
220
|
}
|
|
@@ -224,6 +231,7 @@ var DEGIROParser = class {
|
|
|
224
231
|
*/
|
|
225
232
|
parse(csv) {
|
|
226
233
|
this._warningEntries = [];
|
|
234
|
+
this._incomeRows = [];
|
|
227
235
|
if (typeof csv !== "string" || csv.includes("\0")) {
|
|
228
236
|
throw new ParseError("INVALID_CSV");
|
|
229
237
|
}
|
|
@@ -248,17 +256,70 @@ var DEGIROParser = class {
|
|
|
248
256
|
}
|
|
249
257
|
const snapshot = this._snapshot ?? getActiveSnapshot();
|
|
250
258
|
const transactions = [];
|
|
259
|
+
const incomeCandidates = [];
|
|
260
|
+
const withholdingCandidates = [];
|
|
251
261
|
for (let r = 1; r < rows.length; r++) {
|
|
252
262
|
const row = rows[r];
|
|
253
263
|
const rowIndex = r + 1;
|
|
254
264
|
if (row.every((cell) => cell.trim() === "")) continue;
|
|
255
265
|
const get = (col) => (row[colIndex[col]] ?? "").trim();
|
|
256
266
|
const isin = get("ISIN");
|
|
267
|
+
const product = get("Product");
|
|
268
|
+
const qtyStr = get("Quantity");
|
|
269
|
+
const rawQty = parseFloat(qtyStr);
|
|
270
|
+
const qtyIsZeroOrBlank = qtyStr === "" || isNaN(rawQty) || rawQty === 0;
|
|
271
|
+
if (qtyIsZeroOrBlank) {
|
|
272
|
+
const upperProduct = product.toUpperCase();
|
|
273
|
+
const isTaxKeyword = TAX_KEYWORDS.some((k) => upperProduct.includes(k));
|
|
274
|
+
const isIncomeKeyword = !isTaxKeyword && INCOME_KEYWORDS.some((k) => upperProduct.includes(k));
|
|
275
|
+
if (isTaxKeyword || isIncomeKeyword) {
|
|
276
|
+
const rawLocalValue = parseFloat(get("Local value"));
|
|
277
|
+
const localValue = isNaN(rawLocalValue) ? 0 : rawLocalValue;
|
|
278
|
+
const isoDate2 = parseDate(get("Date"));
|
|
279
|
+
if (isTaxKeyword && localValue < 0) {
|
|
280
|
+
if (!isin) {
|
|
281
|
+
this._warningEntries.push({
|
|
282
|
+
code: "MISSING_ISIN_INCOME",
|
|
283
|
+
row: rowIndex
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
withholdingCandidates.push({
|
|
288
|
+
isin,
|
|
289
|
+
date: isoDate2,
|
|
290
|
+
amount: Math.abs(localValue),
|
|
291
|
+
row: rowIndex
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (isIncomeKeyword && localValue > 0) {
|
|
296
|
+
if (!isin) {
|
|
297
|
+
this._warningEntries.push({
|
|
298
|
+
code: "MISSING_ISIN_INCOME",
|
|
299
|
+
row: rowIndex
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const incomeType = upperProduct.includes(
|
|
304
|
+
"DIVIDEND"
|
|
305
|
+
) ? "dividend" : "coupon";
|
|
306
|
+
incomeCandidates.push({
|
|
307
|
+
isin,
|
|
308
|
+
product,
|
|
309
|
+
date: isoDate2,
|
|
310
|
+
incomeType,
|
|
311
|
+
localValue,
|
|
312
|
+
currency: get("Local value currency"),
|
|
313
|
+
row: rowIndex
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
257
319
|
if (!isin) {
|
|
258
320
|
this._warningEntries.push({ code: "MISSING_ISIN", row: rowIndex });
|
|
259
321
|
continue;
|
|
260
322
|
}
|
|
261
|
-
const rawQty = parseFloat(get("Quantity"));
|
|
262
323
|
if (isNaN(rawQty) || rawQty === 0) {
|
|
263
324
|
this._warningEntries.push({ code: "QUANTITY_ZERO", row: rowIndex });
|
|
264
325
|
continue;
|
|
@@ -299,7 +360,6 @@ var DEGIROParser = class {
|
|
|
299
360
|
const rawFees = parseFloat(get("Transaction costs"));
|
|
300
361
|
const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);
|
|
301
362
|
const pricePerUnit = parseFloat(get("Price"));
|
|
302
|
-
const product = get("Product");
|
|
303
363
|
transactions.push({
|
|
304
364
|
isin,
|
|
305
365
|
product,
|
|
@@ -314,6 +374,73 @@ var DEGIROParser = class {
|
|
|
314
374
|
fxRate
|
|
315
375
|
});
|
|
316
376
|
}
|
|
377
|
+
const withholdingByKey = /* @__PURE__ */ new Map();
|
|
378
|
+
for (const w of withholdingCandidates) {
|
|
379
|
+
const key = `${w.isin}|${w.date}`;
|
|
380
|
+
const existing = withholdingByKey.get(key);
|
|
381
|
+
withholdingByKey.set(key, {
|
|
382
|
+
isin: w.isin,
|
|
383
|
+
date: w.date,
|
|
384
|
+
amount: (existing?.amount ?? 0) + w.amount
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const consumedKeys = /* @__PURE__ */ new Set();
|
|
388
|
+
const incomeRows = [];
|
|
389
|
+
for (const c of incomeCandidates) {
|
|
390
|
+
const key = `${c.isin}|${c.date}`;
|
|
391
|
+
const matchedWithholding = withholdingByKey.get(key);
|
|
392
|
+
let grossAmount;
|
|
393
|
+
let withholdingTax;
|
|
394
|
+
let fxRate;
|
|
395
|
+
if (c.currency === "EUR") {
|
|
396
|
+
grossAmount = Math.abs(c.localValue);
|
|
397
|
+
withholdingTax = matchedWithholding?.amount ?? 0;
|
|
398
|
+
fxRate = void 0;
|
|
399
|
+
} else {
|
|
400
|
+
const rate = lookupRate(c.currency, c.date, snapshot);
|
|
401
|
+
if (rate === null) {
|
|
402
|
+
if (snapshot[c.currency] === void 0) {
|
|
403
|
+
this._warningEntries.push({
|
|
404
|
+
code: "UNSUPPORTED_CURRENCY",
|
|
405
|
+
row: c.row,
|
|
406
|
+
currency: c.currency
|
|
407
|
+
});
|
|
408
|
+
} else {
|
|
409
|
+
this._warningEntries.push({
|
|
410
|
+
code: "NO_ECB_RATE",
|
|
411
|
+
row: c.row,
|
|
412
|
+
currency: c.currency,
|
|
413
|
+
date: c.date
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
grossAmount = Math.abs(c.localValue) / rate;
|
|
419
|
+
withholdingTax = matchedWithholding ? matchedWithholding.amount / rate : 0;
|
|
420
|
+
fxRate = rate;
|
|
421
|
+
}
|
|
422
|
+
if (matchedWithholding) consumedKeys.add(key);
|
|
423
|
+
incomeRows.push({
|
|
424
|
+
isin: c.isin,
|
|
425
|
+
product: c.product,
|
|
426
|
+
date: c.date,
|
|
427
|
+
incomeType: c.incomeType,
|
|
428
|
+
grossAmount,
|
|
429
|
+
withholdingTax,
|
|
430
|
+
currency: c.currency,
|
|
431
|
+
fxRate
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
for (const [key, w] of withholdingByKey) {
|
|
435
|
+
if (!consumedKeys.has(key)) {
|
|
436
|
+
this._warningEntries.push({
|
|
437
|
+
code: "ORPHAN_WITHHOLDING",
|
|
438
|
+
isin: w.isin,
|
|
439
|
+
date: w.date
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
this._incomeRows = incomeRows;
|
|
317
444
|
return transactions;
|
|
318
445
|
}
|
|
319
446
|
get warnings() {
|
|
@@ -322,12 +449,136 @@ var DEGIROParser = class {
|
|
|
322
449
|
get warningEntries() {
|
|
323
450
|
return this._warningEntries;
|
|
324
451
|
}
|
|
452
|
+
get incomeRows() {
|
|
453
|
+
return this._incomeRows;
|
|
454
|
+
}
|
|
325
455
|
};
|
|
326
456
|
|
|
327
|
-
// src/
|
|
457
|
+
// src/dichiarazione/engine.ts
|
|
458
|
+
var import_promises = require("fs/promises");
|
|
328
459
|
function roundHalfUp(x) {
|
|
329
460
|
return Math.sign(x) * Math.round(Math.abs(x) * 100) / 100;
|
|
330
461
|
}
|
|
462
|
+
function buildQuadroRT(bucketB, carryForward, taxYear) {
|
|
463
|
+
const plusvalenze = bucketB.plusvalenze;
|
|
464
|
+
const minusvalenze = bucketB.minusvalenze;
|
|
465
|
+
const differenza = roundHalfUp(plusvalenze - minusvalenze);
|
|
466
|
+
if (differenza > 0) {
|
|
467
|
+
const sorted = [...carryForward].sort((a, b) => a.year - b.year);
|
|
468
|
+
let remaining = differenza;
|
|
469
|
+
const carryForwardApplied = [];
|
|
470
|
+
for (const entry of sorted) {
|
|
471
|
+
if (taxYear - entry.year > 4) continue;
|
|
472
|
+
const consumed = Math.min(entry.amount, remaining);
|
|
473
|
+
if (consumed > 0) {
|
|
474
|
+
carryForwardApplied.push({
|
|
475
|
+
annoOrigine: entry.year,
|
|
476
|
+
importo: roundHalfUp(consumed)
|
|
477
|
+
});
|
|
478
|
+
remaining -= consumed;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const imponibileNetto = roundHalfUp(remaining);
|
|
482
|
+
const imposta = roundHalfUp(imponibileNetto * 0.26);
|
|
483
|
+
return {
|
|
484
|
+
plusvalenze,
|
|
485
|
+
minusvalenze,
|
|
486
|
+
differenza,
|
|
487
|
+
carryForwardApplied,
|
|
488
|
+
imponibileNetto,
|
|
489
|
+
imposta,
|
|
490
|
+
carryForwardRiportato: []
|
|
491
|
+
};
|
|
492
|
+
} else if (differenza < 0) {
|
|
493
|
+
return {
|
|
494
|
+
plusvalenze,
|
|
495
|
+
minusvalenze,
|
|
496
|
+
differenza,
|
|
497
|
+
carryForwardApplied: [],
|
|
498
|
+
imponibileNetto: 0,
|
|
499
|
+
imposta: 0,
|
|
500
|
+
carryForwardRiportato: [
|
|
501
|
+
{ annoOrigine: taxYear, importo: roundHalfUp(Math.abs(differenza)) }
|
|
502
|
+
]
|
|
503
|
+
};
|
|
504
|
+
} else {
|
|
505
|
+
return {
|
|
506
|
+
plusvalenze,
|
|
507
|
+
minusvalenze,
|
|
508
|
+
differenza: 0,
|
|
509
|
+
carryForwardApplied: [],
|
|
510
|
+
imponibileNetto: 0,
|
|
511
|
+
imposta: 0,
|
|
512
|
+
carryForwardRiportato: []
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function buildQuadroRM(bucketA, incomeRows, taxYear) {
|
|
517
|
+
const groups = bucketA?.groups ?? [];
|
|
518
|
+
const group26 = groups.find((g) => g.taxRate === 0.26);
|
|
519
|
+
const group125 = groups.find((g) => g.taxRate === 0.125);
|
|
520
|
+
const capitaleAliquota26 = {
|
|
521
|
+
plusvalenze: group26?.plusvalenze ?? 0,
|
|
522
|
+
imposta: group26?.imposta ?? 0
|
|
523
|
+
};
|
|
524
|
+
const capitaleAliquota125 = {
|
|
525
|
+
plusvalenze: group125?.plusvalenze ?? 0,
|
|
526
|
+
imposta: group125?.imposta ?? 0
|
|
527
|
+
};
|
|
528
|
+
const dividendiEsteri = incomeRows.filter((r) => r.incomeType === "dividend").map((r) => ({
|
|
529
|
+
isin: r.isin,
|
|
530
|
+
prodotto: r.product,
|
|
531
|
+
lordo: r.grossAmount,
|
|
532
|
+
rittenutaEstera: r.withholdingTax
|
|
533
|
+
}));
|
|
534
|
+
const cedole = incomeRows.filter((r) => r.incomeType === "coupon").map((r) => ({
|
|
535
|
+
isin: r.isin,
|
|
536
|
+
prodotto: r.product,
|
|
537
|
+
importo: r.grossAmount,
|
|
538
|
+
rittenutaEstera: r.withholdingTax
|
|
539
|
+
}));
|
|
540
|
+
return {
|
|
541
|
+
capitaleAliquota26,
|
|
542
|
+
capitaleAliquota125,
|
|
543
|
+
dividendiEsteri,
|
|
544
|
+
cedole
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function buildDichiarazioneReport(quadroRT, quadroRM, taxYear) {
|
|
548
|
+
const version = 1;
|
|
549
|
+
const modello = "Redditi PF";
|
|
550
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
551
|
+
return {
|
|
552
|
+
version,
|
|
553
|
+
annoImposta: taxYear,
|
|
554
|
+
modello,
|
|
555
|
+
generatedAt,
|
|
556
|
+
quadroRT,
|
|
557
|
+
quadroRM,
|
|
558
|
+
exportTo: async (path2) => {
|
|
559
|
+
await (0, import_promises.writeFile)(
|
|
560
|
+
path2,
|
|
561
|
+
JSON.stringify(
|
|
562
|
+
{
|
|
563
|
+
version,
|
|
564
|
+
annoImposta: taxYear,
|
|
565
|
+
modello,
|
|
566
|
+
generatedAt,
|
|
567
|
+
quadroRT,
|
|
568
|
+
quadroRM
|
|
569
|
+
},
|
|
570
|
+
null,
|
|
571
|
+
2
|
|
572
|
+
)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/calculator/index.ts
|
|
579
|
+
function roundHalfUp2(x) {
|
|
580
|
+
return Math.sign(x) * Math.round(Math.abs(x) * 100) / 100;
|
|
581
|
+
}
|
|
331
582
|
function inferTaxYear(transactions) {
|
|
332
583
|
const counts = {};
|
|
333
584
|
for (const t of transactions) {
|
|
@@ -419,9 +670,9 @@ var Calculator = class {
|
|
|
419
670
|
sellDate: tx.date,
|
|
420
671
|
buyPriceEUR: lot.pricePerUnitEUR,
|
|
421
672
|
sellPriceEUR: sellPricePerUnitEUR,
|
|
422
|
-
buyCostEUR:
|
|
423
|
-
sellProceedsEUR:
|
|
424
|
-
gainLossEUR:
|
|
673
|
+
buyCostEUR: roundHalfUp2(buyCostEUR),
|
|
674
|
+
sellProceedsEUR: roundHalfUp2(sellProceedsEUR),
|
|
675
|
+
gainLossEUR: roundHalfUp2(gainLossEUR),
|
|
425
676
|
buyFxRate: lot.fxRate,
|
|
426
677
|
sellFxRate: tx.fxRate
|
|
427
678
|
});
|
|
@@ -476,12 +727,12 @@ var Calculator = class {
|
|
|
476
727
|
const bucketAGroups = [...groupsByRate.entries()].map(([taxRate, g]) => ({
|
|
477
728
|
taxRate,
|
|
478
729
|
assetClasses: [...g.assetClasses],
|
|
479
|
-
plusvalenze:
|
|
480
|
-
imposta:
|
|
730
|
+
plusvalenze: roundHalfUp2(g.plusvalenze),
|
|
731
|
+
imposta: roundHalfUp2(g.plusvalenze * taxRate)
|
|
481
732
|
}));
|
|
482
733
|
const bucketAReport = {
|
|
483
734
|
groups: bucketAGroups,
|
|
484
|
-
totalImposta:
|
|
735
|
+
totalImposta: roundHalfUp2(
|
|
485
736
|
bucketAGroups.reduce((s, g) => s + g.imposta, 0)
|
|
486
737
|
)
|
|
487
738
|
};
|
|
@@ -492,8 +743,8 @@ var Calculator = class {
|
|
|
492
743
|
if (lot.gainLossEUR >= 0) bPlusvalenze += lot.gainLossEUR;
|
|
493
744
|
else bMinusvalenze += Math.abs(lot.gainLossEUR);
|
|
494
745
|
}
|
|
495
|
-
bPlusvalenze =
|
|
496
|
-
bMinusvalenze =
|
|
746
|
+
bPlusvalenze = roundHalfUp2(bPlusvalenze);
|
|
747
|
+
bMinusvalenze = roundHalfUp2(bMinusvalenze);
|
|
497
748
|
const carryForwards = [...this._options.carryForward ?? []].sort(
|
|
498
749
|
(a, b) => a.year - b.year
|
|
499
750
|
);
|
|
@@ -506,11 +757,11 @@ var Calculator = class {
|
|
|
506
757
|
carryForwardApplied += consumed;
|
|
507
758
|
remaining -= consumed;
|
|
508
759
|
}
|
|
509
|
-
carryForwardApplied =
|
|
510
|
-
const bNetResult =
|
|
760
|
+
carryForwardApplied = roundHalfUp2(carryForwardApplied);
|
|
761
|
+
const bNetResult = roundHalfUp2(
|
|
511
762
|
bPlusvalenze - bMinusvalenze - carryForwardApplied
|
|
512
763
|
);
|
|
513
|
-
const carryForwardRemaining =
|
|
764
|
+
const carryForwardRemaining = roundHalfUp2(Math.max(0, -bNetResult));
|
|
514
765
|
const bucketBReport = {
|
|
515
766
|
plusvalenze: bPlusvalenze,
|
|
516
767
|
minusvalenze: bMinusvalenze,
|
|
@@ -518,18 +769,41 @@ var Calculator = class {
|
|
|
518
769
|
carryForwardRemaining,
|
|
519
770
|
netResult: bNetResult
|
|
520
771
|
};
|
|
772
|
+
const allIncomeRows = this._options.incomeRows ?? [];
|
|
773
|
+
const filteredIncomeRows = allIncomeRows.filter(
|
|
774
|
+
(row) => new Date(row.date).getFullYear() === taxYear
|
|
775
|
+
);
|
|
776
|
+
if (filteredIncomeRows.length < allIncomeRows.length) {
|
|
777
|
+
warnings.push(`Income rows outside tax year ${taxYear} were skipped.`);
|
|
778
|
+
}
|
|
779
|
+
const quadroRT = buildQuadroRT(
|
|
780
|
+
bucketBReport,
|
|
781
|
+
this._options.carryForward ?? [],
|
|
782
|
+
taxYear
|
|
783
|
+
);
|
|
784
|
+
const quadroRM = buildQuadroRM(
|
|
785
|
+
bucketAGroups.length > 0 ? bucketAReport : void 0,
|
|
786
|
+
filteredIncomeRows,
|
|
787
|
+
taxYear
|
|
788
|
+
);
|
|
789
|
+
const dichiarazioneReport = buildDichiarazioneReport(
|
|
790
|
+
quadroRT,
|
|
791
|
+
quadroRM,
|
|
792
|
+
taxYear
|
|
793
|
+
);
|
|
521
794
|
return {
|
|
522
795
|
method,
|
|
523
796
|
taxYear,
|
|
524
|
-
plusvalenze:
|
|
525
|
-
minusvalenze:
|
|
526
|
-
netResult:
|
|
797
|
+
plusvalenze: roundHalfUp2(plusvalenze),
|
|
798
|
+
minusvalenze: roundHalfUp2(minusvalenze),
|
|
799
|
+
netResult: roundHalfUp2(plusvalenze - minusvalenze),
|
|
527
800
|
lots: matchedLots,
|
|
528
801
|
ratesUsed,
|
|
529
802
|
warnings,
|
|
530
803
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
531
804
|
bucketA: bucketAGroups.length > 0 ? bucketAReport : void 0,
|
|
532
|
-
bucketB: bucketBReport
|
|
805
|
+
bucketB: bucketBReport,
|
|
806
|
+
dichiarazione: dichiarazioneReport
|
|
533
807
|
};
|
|
534
808
|
} else {
|
|
535
809
|
const allIsins = [...new Set(this._transactions.map((t) => t.isin))];
|
|
@@ -544,9 +818,9 @@ var Calculator = class {
|
|
|
544
818
|
return {
|
|
545
819
|
method,
|
|
546
820
|
taxYear,
|
|
547
|
-
plusvalenze:
|
|
548
|
-
minusvalenze:
|
|
549
|
-
netResult:
|
|
821
|
+
plusvalenze: roundHalfUp2(plusvalenze),
|
|
822
|
+
minusvalenze: roundHalfUp2(minusvalenze),
|
|
823
|
+
netResult: roundHalfUp2(plusvalenze - minusvalenze),
|
|
550
824
|
lots: matchedLots,
|
|
551
825
|
ratesUsed,
|
|
552
826
|
warnings,
|