@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.js
CHANGED
|
@@ -110,6 +110,10 @@ function warningToEnglish(w) {
|
|
|
110
110
|
return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} \u2014 skipped`;
|
|
111
111
|
case "QUANTITY_ZERO":
|
|
112
112
|
return `Row ${w.row}: quantity is 0 \u2014 skipped`;
|
|
113
|
+
case "MISSING_ISIN_INCOME":
|
|
114
|
+
return `Row ${w.row}: blank ISIN on income row \u2014 skipped`;
|
|
115
|
+
case "ORPHAN_WITHHOLDING":
|
|
116
|
+
return `Withholding row for ${w.isin} on ${w.date}: no matching income row \u2014 skipped`;
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
|
|
@@ -125,6 +129,8 @@ var REQUIRED_COLUMNS = [
|
|
|
125
129
|
"Transaction costs currency",
|
|
126
130
|
"Product"
|
|
127
131
|
];
|
|
132
|
+
var INCOME_KEYWORDS = ["DIVIDEND", "COUPON", "INTEREST", "CEDOLA"];
|
|
133
|
+
var TAX_KEYWORDS = ["DIVIDEND TAX", "WITHHOLDING", "RITENUTA"];
|
|
128
134
|
function parseCSVRow(line) {
|
|
129
135
|
const fields = [];
|
|
130
136
|
let i = 0;
|
|
@@ -166,6 +172,7 @@ function parseDate(ddmmyyyy) {
|
|
|
166
172
|
var DEGIROParser = class {
|
|
167
173
|
_warningEntries = [];
|
|
168
174
|
_snapshot;
|
|
175
|
+
_incomeRows = [];
|
|
169
176
|
constructor(snapshot) {
|
|
170
177
|
this._snapshot = snapshot;
|
|
171
178
|
}
|
|
@@ -182,6 +189,7 @@ var DEGIROParser = class {
|
|
|
182
189
|
*/
|
|
183
190
|
parse(csv) {
|
|
184
191
|
this._warningEntries = [];
|
|
192
|
+
this._incomeRows = [];
|
|
185
193
|
if (typeof csv !== "string" || csv.includes("\0")) {
|
|
186
194
|
throw new ParseError("INVALID_CSV");
|
|
187
195
|
}
|
|
@@ -206,17 +214,70 @@ var DEGIROParser = class {
|
|
|
206
214
|
}
|
|
207
215
|
const snapshot = this._snapshot ?? getActiveSnapshot();
|
|
208
216
|
const transactions = [];
|
|
217
|
+
const incomeCandidates = [];
|
|
218
|
+
const withholdingCandidates = [];
|
|
209
219
|
for (let r = 1; r < rows.length; r++) {
|
|
210
220
|
const row = rows[r];
|
|
211
221
|
const rowIndex = r + 1;
|
|
212
222
|
if (row.every((cell) => cell.trim() === "")) continue;
|
|
213
223
|
const get = (col) => (row[colIndex[col]] ?? "").trim();
|
|
214
224
|
const isin = get("ISIN");
|
|
225
|
+
const product = get("Product");
|
|
226
|
+
const qtyStr = get("Quantity");
|
|
227
|
+
const rawQty = parseFloat(qtyStr);
|
|
228
|
+
const qtyIsZeroOrBlank = qtyStr === "" || isNaN(rawQty) || rawQty === 0;
|
|
229
|
+
if (qtyIsZeroOrBlank) {
|
|
230
|
+
const upperProduct = product.toUpperCase();
|
|
231
|
+
const isTaxKeyword = TAX_KEYWORDS.some((k) => upperProduct.includes(k));
|
|
232
|
+
const isIncomeKeyword = !isTaxKeyword && INCOME_KEYWORDS.some((k) => upperProduct.includes(k));
|
|
233
|
+
if (isTaxKeyword || isIncomeKeyword) {
|
|
234
|
+
const rawLocalValue = parseFloat(get("Local value"));
|
|
235
|
+
const localValue = isNaN(rawLocalValue) ? 0 : rawLocalValue;
|
|
236
|
+
const isoDate2 = parseDate(get("Date"));
|
|
237
|
+
if (isTaxKeyword && localValue < 0) {
|
|
238
|
+
if (!isin) {
|
|
239
|
+
this._warningEntries.push({
|
|
240
|
+
code: "MISSING_ISIN_INCOME",
|
|
241
|
+
row: rowIndex
|
|
242
|
+
});
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
withholdingCandidates.push({
|
|
246
|
+
isin,
|
|
247
|
+
date: isoDate2,
|
|
248
|
+
amount: Math.abs(localValue),
|
|
249
|
+
row: rowIndex
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (isIncomeKeyword && localValue > 0) {
|
|
254
|
+
if (!isin) {
|
|
255
|
+
this._warningEntries.push({
|
|
256
|
+
code: "MISSING_ISIN_INCOME",
|
|
257
|
+
row: rowIndex
|
|
258
|
+
});
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const incomeType = upperProduct.includes(
|
|
262
|
+
"DIVIDEND"
|
|
263
|
+
) ? "dividend" : "coupon";
|
|
264
|
+
incomeCandidates.push({
|
|
265
|
+
isin,
|
|
266
|
+
product,
|
|
267
|
+
date: isoDate2,
|
|
268
|
+
incomeType,
|
|
269
|
+
localValue,
|
|
270
|
+
currency: get("Local value currency"),
|
|
271
|
+
row: rowIndex
|
|
272
|
+
});
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
215
277
|
if (!isin) {
|
|
216
278
|
this._warningEntries.push({ code: "MISSING_ISIN", row: rowIndex });
|
|
217
279
|
continue;
|
|
218
280
|
}
|
|
219
|
-
const rawQty = parseFloat(get("Quantity"));
|
|
220
281
|
if (isNaN(rawQty) || rawQty === 0) {
|
|
221
282
|
this._warningEntries.push({ code: "QUANTITY_ZERO", row: rowIndex });
|
|
222
283
|
continue;
|
|
@@ -257,7 +318,6 @@ var DEGIROParser = class {
|
|
|
257
318
|
const rawFees = parseFloat(get("Transaction costs"));
|
|
258
319
|
const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);
|
|
259
320
|
const pricePerUnit = parseFloat(get("Price"));
|
|
260
|
-
const product = get("Product");
|
|
261
321
|
transactions.push({
|
|
262
322
|
isin,
|
|
263
323
|
product,
|
|
@@ -272,6 +332,73 @@ var DEGIROParser = class {
|
|
|
272
332
|
fxRate
|
|
273
333
|
});
|
|
274
334
|
}
|
|
335
|
+
const withholdingByKey = /* @__PURE__ */ new Map();
|
|
336
|
+
for (const w of withholdingCandidates) {
|
|
337
|
+
const key = `${w.isin}|${w.date}`;
|
|
338
|
+
const existing = withholdingByKey.get(key);
|
|
339
|
+
withholdingByKey.set(key, {
|
|
340
|
+
isin: w.isin,
|
|
341
|
+
date: w.date,
|
|
342
|
+
amount: (existing?.amount ?? 0) + w.amount
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const consumedKeys = /* @__PURE__ */ new Set();
|
|
346
|
+
const incomeRows = [];
|
|
347
|
+
for (const c of incomeCandidates) {
|
|
348
|
+
const key = `${c.isin}|${c.date}`;
|
|
349
|
+
const matchedWithholding = withholdingByKey.get(key);
|
|
350
|
+
let grossAmount;
|
|
351
|
+
let withholdingTax;
|
|
352
|
+
let fxRate;
|
|
353
|
+
if (c.currency === "EUR") {
|
|
354
|
+
grossAmount = Math.abs(c.localValue);
|
|
355
|
+
withholdingTax = matchedWithholding?.amount ?? 0;
|
|
356
|
+
fxRate = void 0;
|
|
357
|
+
} else {
|
|
358
|
+
const rate = lookupRate(c.currency, c.date, snapshot);
|
|
359
|
+
if (rate === null) {
|
|
360
|
+
if (snapshot[c.currency] === void 0) {
|
|
361
|
+
this._warningEntries.push({
|
|
362
|
+
code: "UNSUPPORTED_CURRENCY",
|
|
363
|
+
row: c.row,
|
|
364
|
+
currency: c.currency
|
|
365
|
+
});
|
|
366
|
+
} else {
|
|
367
|
+
this._warningEntries.push({
|
|
368
|
+
code: "NO_ECB_RATE",
|
|
369
|
+
row: c.row,
|
|
370
|
+
currency: c.currency,
|
|
371
|
+
date: c.date
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
grossAmount = Math.abs(c.localValue) / rate;
|
|
377
|
+
withholdingTax = matchedWithholding ? matchedWithholding.amount / rate : 0;
|
|
378
|
+
fxRate = rate;
|
|
379
|
+
}
|
|
380
|
+
if (matchedWithholding) consumedKeys.add(key);
|
|
381
|
+
incomeRows.push({
|
|
382
|
+
isin: c.isin,
|
|
383
|
+
product: c.product,
|
|
384
|
+
date: c.date,
|
|
385
|
+
incomeType: c.incomeType,
|
|
386
|
+
grossAmount,
|
|
387
|
+
withholdingTax,
|
|
388
|
+
currency: c.currency,
|
|
389
|
+
fxRate
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
for (const [key, w] of withholdingByKey) {
|
|
393
|
+
if (!consumedKeys.has(key)) {
|
|
394
|
+
this._warningEntries.push({
|
|
395
|
+
code: "ORPHAN_WITHHOLDING",
|
|
396
|
+
isin: w.isin,
|
|
397
|
+
date: w.date
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
this._incomeRows = incomeRows;
|
|
275
402
|
return transactions;
|
|
276
403
|
}
|
|
277
404
|
get warnings() {
|
|
@@ -280,12 +407,136 @@ var DEGIROParser = class {
|
|
|
280
407
|
get warningEntries() {
|
|
281
408
|
return this._warningEntries;
|
|
282
409
|
}
|
|
410
|
+
get incomeRows() {
|
|
411
|
+
return this._incomeRows;
|
|
412
|
+
}
|
|
283
413
|
};
|
|
284
414
|
|
|
285
|
-
// src/
|
|
415
|
+
// src/dichiarazione/engine.ts
|
|
416
|
+
import { writeFile } from "fs/promises";
|
|
286
417
|
function roundHalfUp(x) {
|
|
287
418
|
return Math.sign(x) * Math.round(Math.abs(x) * 100) / 100;
|
|
288
419
|
}
|
|
420
|
+
function buildQuadroRT(bucketB, carryForward, taxYear) {
|
|
421
|
+
const plusvalenze = bucketB.plusvalenze;
|
|
422
|
+
const minusvalenze = bucketB.minusvalenze;
|
|
423
|
+
const differenza = roundHalfUp(plusvalenze - minusvalenze);
|
|
424
|
+
if (differenza > 0) {
|
|
425
|
+
const sorted = [...carryForward].sort((a, b) => a.year - b.year);
|
|
426
|
+
let remaining = differenza;
|
|
427
|
+
const carryForwardApplied = [];
|
|
428
|
+
for (const entry of sorted) {
|
|
429
|
+
if (taxYear - entry.year > 4) continue;
|
|
430
|
+
const consumed = Math.min(entry.amount, remaining);
|
|
431
|
+
if (consumed > 0) {
|
|
432
|
+
carryForwardApplied.push({
|
|
433
|
+
annoOrigine: entry.year,
|
|
434
|
+
importo: roundHalfUp(consumed)
|
|
435
|
+
});
|
|
436
|
+
remaining -= consumed;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const imponibileNetto = roundHalfUp(remaining);
|
|
440
|
+
const imposta = roundHalfUp(imponibileNetto * 0.26);
|
|
441
|
+
return {
|
|
442
|
+
plusvalenze,
|
|
443
|
+
minusvalenze,
|
|
444
|
+
differenza,
|
|
445
|
+
carryForwardApplied,
|
|
446
|
+
imponibileNetto,
|
|
447
|
+
imposta,
|
|
448
|
+
carryForwardRiportato: []
|
|
449
|
+
};
|
|
450
|
+
} else if (differenza < 0) {
|
|
451
|
+
return {
|
|
452
|
+
plusvalenze,
|
|
453
|
+
minusvalenze,
|
|
454
|
+
differenza,
|
|
455
|
+
carryForwardApplied: [],
|
|
456
|
+
imponibileNetto: 0,
|
|
457
|
+
imposta: 0,
|
|
458
|
+
carryForwardRiportato: [
|
|
459
|
+
{ annoOrigine: taxYear, importo: roundHalfUp(Math.abs(differenza)) }
|
|
460
|
+
]
|
|
461
|
+
};
|
|
462
|
+
} else {
|
|
463
|
+
return {
|
|
464
|
+
plusvalenze,
|
|
465
|
+
minusvalenze,
|
|
466
|
+
differenza: 0,
|
|
467
|
+
carryForwardApplied: [],
|
|
468
|
+
imponibileNetto: 0,
|
|
469
|
+
imposta: 0,
|
|
470
|
+
carryForwardRiportato: []
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function buildQuadroRM(bucketA, incomeRows, taxYear) {
|
|
475
|
+
const groups = bucketA?.groups ?? [];
|
|
476
|
+
const group26 = groups.find((g) => g.taxRate === 0.26);
|
|
477
|
+
const group125 = groups.find((g) => g.taxRate === 0.125);
|
|
478
|
+
const capitaleAliquota26 = {
|
|
479
|
+
plusvalenze: group26?.plusvalenze ?? 0,
|
|
480
|
+
imposta: group26?.imposta ?? 0
|
|
481
|
+
};
|
|
482
|
+
const capitaleAliquota125 = {
|
|
483
|
+
plusvalenze: group125?.plusvalenze ?? 0,
|
|
484
|
+
imposta: group125?.imposta ?? 0
|
|
485
|
+
};
|
|
486
|
+
const dividendiEsteri = incomeRows.filter((r) => r.incomeType === "dividend").map((r) => ({
|
|
487
|
+
isin: r.isin,
|
|
488
|
+
prodotto: r.product,
|
|
489
|
+
lordo: r.grossAmount,
|
|
490
|
+
rittenutaEstera: r.withholdingTax
|
|
491
|
+
}));
|
|
492
|
+
const cedole = incomeRows.filter((r) => r.incomeType === "coupon").map((r) => ({
|
|
493
|
+
isin: r.isin,
|
|
494
|
+
prodotto: r.product,
|
|
495
|
+
importo: r.grossAmount,
|
|
496
|
+
rittenutaEstera: r.withholdingTax
|
|
497
|
+
}));
|
|
498
|
+
return {
|
|
499
|
+
capitaleAliquota26,
|
|
500
|
+
capitaleAliquota125,
|
|
501
|
+
dividendiEsteri,
|
|
502
|
+
cedole
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function buildDichiarazioneReport(quadroRT, quadroRM, taxYear) {
|
|
506
|
+
const version = 1;
|
|
507
|
+
const modello = "Redditi PF";
|
|
508
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
509
|
+
return {
|
|
510
|
+
version,
|
|
511
|
+
annoImposta: taxYear,
|
|
512
|
+
modello,
|
|
513
|
+
generatedAt,
|
|
514
|
+
quadroRT,
|
|
515
|
+
quadroRM,
|
|
516
|
+
exportTo: async (path2) => {
|
|
517
|
+
await writeFile(
|
|
518
|
+
path2,
|
|
519
|
+
JSON.stringify(
|
|
520
|
+
{
|
|
521
|
+
version,
|
|
522
|
+
annoImposta: taxYear,
|
|
523
|
+
modello,
|
|
524
|
+
generatedAt,
|
|
525
|
+
quadroRT,
|
|
526
|
+
quadroRM
|
|
527
|
+
},
|
|
528
|
+
null,
|
|
529
|
+
2
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/calculator/index.ts
|
|
537
|
+
function roundHalfUp2(x) {
|
|
538
|
+
return Math.sign(x) * Math.round(Math.abs(x) * 100) / 100;
|
|
539
|
+
}
|
|
289
540
|
function inferTaxYear(transactions) {
|
|
290
541
|
const counts = {};
|
|
291
542
|
for (const t of transactions) {
|
|
@@ -377,9 +628,9 @@ var Calculator = class {
|
|
|
377
628
|
sellDate: tx.date,
|
|
378
629
|
buyPriceEUR: lot.pricePerUnitEUR,
|
|
379
630
|
sellPriceEUR: sellPricePerUnitEUR,
|
|
380
|
-
buyCostEUR:
|
|
381
|
-
sellProceedsEUR:
|
|
382
|
-
gainLossEUR:
|
|
631
|
+
buyCostEUR: roundHalfUp2(buyCostEUR),
|
|
632
|
+
sellProceedsEUR: roundHalfUp2(sellProceedsEUR),
|
|
633
|
+
gainLossEUR: roundHalfUp2(gainLossEUR),
|
|
383
634
|
buyFxRate: lot.fxRate,
|
|
384
635
|
sellFxRate: tx.fxRate
|
|
385
636
|
});
|
|
@@ -434,12 +685,12 @@ var Calculator = class {
|
|
|
434
685
|
const bucketAGroups = [...groupsByRate.entries()].map(([taxRate, g]) => ({
|
|
435
686
|
taxRate,
|
|
436
687
|
assetClasses: [...g.assetClasses],
|
|
437
|
-
plusvalenze:
|
|
438
|
-
imposta:
|
|
688
|
+
plusvalenze: roundHalfUp2(g.plusvalenze),
|
|
689
|
+
imposta: roundHalfUp2(g.plusvalenze * taxRate)
|
|
439
690
|
}));
|
|
440
691
|
const bucketAReport = {
|
|
441
692
|
groups: bucketAGroups,
|
|
442
|
-
totalImposta:
|
|
693
|
+
totalImposta: roundHalfUp2(
|
|
443
694
|
bucketAGroups.reduce((s, g) => s + g.imposta, 0)
|
|
444
695
|
)
|
|
445
696
|
};
|
|
@@ -450,8 +701,8 @@ var Calculator = class {
|
|
|
450
701
|
if (lot.gainLossEUR >= 0) bPlusvalenze += lot.gainLossEUR;
|
|
451
702
|
else bMinusvalenze += Math.abs(lot.gainLossEUR);
|
|
452
703
|
}
|
|
453
|
-
bPlusvalenze =
|
|
454
|
-
bMinusvalenze =
|
|
704
|
+
bPlusvalenze = roundHalfUp2(bPlusvalenze);
|
|
705
|
+
bMinusvalenze = roundHalfUp2(bMinusvalenze);
|
|
455
706
|
const carryForwards = [...this._options.carryForward ?? []].sort(
|
|
456
707
|
(a, b) => a.year - b.year
|
|
457
708
|
);
|
|
@@ -464,11 +715,11 @@ var Calculator = class {
|
|
|
464
715
|
carryForwardApplied += consumed;
|
|
465
716
|
remaining -= consumed;
|
|
466
717
|
}
|
|
467
|
-
carryForwardApplied =
|
|
468
|
-
const bNetResult =
|
|
718
|
+
carryForwardApplied = roundHalfUp2(carryForwardApplied);
|
|
719
|
+
const bNetResult = roundHalfUp2(
|
|
469
720
|
bPlusvalenze - bMinusvalenze - carryForwardApplied
|
|
470
721
|
);
|
|
471
|
-
const carryForwardRemaining =
|
|
722
|
+
const carryForwardRemaining = roundHalfUp2(Math.max(0, -bNetResult));
|
|
472
723
|
const bucketBReport = {
|
|
473
724
|
plusvalenze: bPlusvalenze,
|
|
474
725
|
minusvalenze: bMinusvalenze,
|
|
@@ -476,18 +727,41 @@ var Calculator = class {
|
|
|
476
727
|
carryForwardRemaining,
|
|
477
728
|
netResult: bNetResult
|
|
478
729
|
};
|
|
730
|
+
const allIncomeRows = this._options.incomeRows ?? [];
|
|
731
|
+
const filteredIncomeRows = allIncomeRows.filter(
|
|
732
|
+
(row) => new Date(row.date).getFullYear() === taxYear
|
|
733
|
+
);
|
|
734
|
+
if (filteredIncomeRows.length < allIncomeRows.length) {
|
|
735
|
+
warnings.push(`Income rows outside tax year ${taxYear} were skipped.`);
|
|
736
|
+
}
|
|
737
|
+
const quadroRT = buildQuadroRT(
|
|
738
|
+
bucketBReport,
|
|
739
|
+
this._options.carryForward ?? [],
|
|
740
|
+
taxYear
|
|
741
|
+
);
|
|
742
|
+
const quadroRM = buildQuadroRM(
|
|
743
|
+
bucketAGroups.length > 0 ? bucketAReport : void 0,
|
|
744
|
+
filteredIncomeRows,
|
|
745
|
+
taxYear
|
|
746
|
+
);
|
|
747
|
+
const dichiarazioneReport = buildDichiarazioneReport(
|
|
748
|
+
quadroRT,
|
|
749
|
+
quadroRM,
|
|
750
|
+
taxYear
|
|
751
|
+
);
|
|
479
752
|
return {
|
|
480
753
|
method,
|
|
481
754
|
taxYear,
|
|
482
|
-
plusvalenze:
|
|
483
|
-
minusvalenze:
|
|
484
|
-
netResult:
|
|
755
|
+
plusvalenze: roundHalfUp2(plusvalenze),
|
|
756
|
+
minusvalenze: roundHalfUp2(minusvalenze),
|
|
757
|
+
netResult: roundHalfUp2(plusvalenze - minusvalenze),
|
|
485
758
|
lots: matchedLots,
|
|
486
759
|
ratesUsed,
|
|
487
760
|
warnings,
|
|
488
761
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
489
762
|
bucketA: bucketAGroups.length > 0 ? bucketAReport : void 0,
|
|
490
|
-
bucketB: bucketBReport
|
|
763
|
+
bucketB: bucketBReport,
|
|
764
|
+
dichiarazione: dichiarazioneReport
|
|
491
765
|
};
|
|
492
766
|
} else {
|
|
493
767
|
const allIsins = [...new Set(this._transactions.map((t) => t.isin))];
|
|
@@ -502,9 +776,9 @@ var Calculator = class {
|
|
|
502
776
|
return {
|
|
503
777
|
method,
|
|
504
778
|
taxYear,
|
|
505
|
-
plusvalenze:
|
|
506
|
-
minusvalenze:
|
|
507
|
-
netResult:
|
|
779
|
+
plusvalenze: roundHalfUp2(plusvalenze),
|
|
780
|
+
minusvalenze: roundHalfUp2(minusvalenze),
|
|
781
|
+
netResult: roundHalfUp2(plusvalenze - minusvalenze),
|
|
508
782
|
lots: matchedLots,
|
|
509
783
|
ratesUsed,
|
|
510
784
|
warnings,
|