csv import: map a currency column, or foreign rows land as AUD
ci / lint-test (push) Successful in 41s

Deleting the Frollo importer dropped its currency handling and nothing replaced
it: ColumnMapping had no currency column, so applyMapping could never produce
foreign_currency_code and batchInsertCSVTransactions' support for it was
unreachable from the UI. The USD 10,782 salary imported as A$10,782 — about a
third under, sitting in a column of AUD figures looking entirely normal, which
is the same defect the owner spotted in the first place.

An optional Currency column now sets foreign_currency_code and
foreign_currency_amount when the cell is a three-letter code other than AUD, and
leaves amount_aud NULL rather than inventing a rate. That is the shape order
ingestion already uses and what AMOUNT_UNCONVERTED looks for, so the row renders
as its native figure with "no AUD rate" and the statement supplies the real
number when it arrives.

Caught by reading the imported row rather than the import summary: the summary
said 171 inserted and was right about everything it reported.
This commit is contained in:
2026-08-13 15:08:50 +10:00
parent 461c021e7a
commit 3edcc27781
3 changed files with 59 additions and 1 deletions
+23
View File
@@ -17,6 +17,16 @@ export interface ColumnMapping {
* measured against the same statement coverage.
*/
accountCol?: string;
/**
* Column holding the ISO currency code, when the file is multi-currency.
*
* Without it a foreign row is stored as if it were AUD — a USD 10,782 salary
* lands as A$10,782, understating it by about a third. Mapping it records the
* native figure with `foreign_currency_code` set and `amount_aud` left NULL,
* which is the same shape order ingestion uses and what `AMOUNT_UNCONVERTED`
* looks for.
*/
currencyCol?: string;
/**
* Column holding the provider's own row id, when it has one.
*
@@ -44,6 +54,9 @@ export interface ParsedTransaction {
account?: string;
/** Provider row id, when a column was mapped. */
source_ref?: string;
/** Set only for a non-AUD row: the native amount and its code. */
foreign_currency_amount?: number;
foreign_currency_code?: string;
}
export function parseCSVRows(text: string): string[][] {
@@ -130,6 +143,7 @@ export function applyMapping(
const categoryIdx = mapping.categoryCol ? idx(mapping.categoryCol) : -1;
const accountIdx = mapping.accountCol ? idx(mapping.accountCol) : -1;
const sourceRefIdx = mapping.sourceRefCol ? idx(mapping.sourceRefCol) : -1;
const currencyIdx = mapping.currencyCol ? idx(mapping.currencyCol) : -1;
const results: ParsedTransaction[] = [];
for (const row of dataRows) {
@@ -162,6 +176,15 @@ export function applyMapping(
if (categoryIdx >= 0 && row[categoryIdx]) tx.category = row[categoryIdx].trim();
if (accountIdx >= 0 && row[accountIdx]) tx.account = row[accountIdx].trim();
if (sourceRefIdx >= 0 && row[sourceRefIdx]) tx.source_ref = row[sourceRefIdx].trim();
if (currencyIdx >= 0) {
const code = (row[currencyIdx] ?? "").trim().toUpperCase();
// AUD needs no marking; anything else is native and unconverted. No rate
// is invented — the statement supplies the AUD figure when it arrives.
if (/^[A-Z]{3}$/.test(code) && code !== "AUD") {
tx.foreign_currency_code = code;
tx.foreign_currency_amount = amount;
}
}
results.push(tx);
}
return results;