v1.9.0 — Amazon auto-categorization
Automatic categorization of Amazon charges
A bank charge reads AMAZON.COM*MB3T81 and says nothing about what you bought, so every Amazon purchase landed uncategorized or in one blunt bucket. Amazon's order emails do name the category, so forwarding those to a throwaway mailbox lets the companion label each Amazon charge on its way in:
AMAZON.COM*MB3T81 · Amazon: Lawn & Garden · TRN-…
Wealthfolio's own rule engine files it from there. Set up in a new Amazon auto-categorization card: three fields, one Gmail filter, one throwaway address. No new container — the mailbox is read at the start of each sync, the only moment the data is used. Entirely optional; an empty ledger is the off switch.
Labels are matched by pattern, not a lookup table. Amazon's vocabulary is unpublished, mixes departments with mid-level categories (Baking, Skincare), and grows at will — so ~12 regexes cover more ground than 200 exact entries, and a label Amazon invents next month files itself. Anything unmatched goes to a configurable default and is announced once in Telegram, so it is visible and one rule away from correct rather than silently wrong.
This cannot double-count a purchase. Order emails never create a transaction — they only add text to the comment of a row SimpleFin itself imported. Three tests pin that structurally: a ledger full of orders against an empty feed imports nothing, enrichment never alters amount/fee/type, and two orders sharing an amount leave the charge imported once and uncategorized.
Amazon itemization is not possible, and that is not a limitation of this addon: Amazon removed item names, quantities and unit prices from these emails on 2026-07-08. Splitting a charge into per-item rows needs per-item prices, so that half is dead at the source. Category tagging is what remains — and since there is nothing to split, it never touches reconciliation.
Two read-only diagnostics ship with it, because the two ways this can silently do nothing are invisible from outside:
docker exec simplefin-sync node dist/companion/src/amazon-check.js --host … --user … --password …
docker exec simplefin-sync node dist/companion/src/amazon-descriptors.jsFixed: a thrown bulk save discarded a whole account's batch
The row-by-row fallback added in 1.8.0 only ever worked on the companion. Its REST adapter returns {errors}; the addon's SDK adapter lets ctx.api.activities.saveMany throw. On that path the first call threw, escaped to the per-account catch, and took every good row in the batch with it — surfaced as a single red Account … failed: Duplicate activity detected, which names the symptom and conceals the damage.
Hit in production when a bank came back online after an outage and republished its history. A thrown save is now normalised into the same shape as a reported one, so the fallback runs on both hosts.
A duplicate refusal is no longer an error either. "A matching activity already exists" means the row is there — the create's goal is met. It is logged (duplicate-refused) with the transaction named, since reconciliation not recognising an existing row is worth finding, but it is not worth alarming about.
Fixed: phantom drift equal to a refused row's amount
windowDelta — the term that keeps a heal run's drift measurement lag-free — is computed from plan.creates: what the sync intends to write. It assumed every planned create lands.
A re-authorised bank re-issued one $1,300 transaction under a new id; reconciliation planned a create; Wealthfolio refused it as a duplicate; the row never landed. The account reported $1,300 of drift while its ledger matched the bank to the penny. Such a run is now declared not measurable, and any drift episode it opened is rolled back.
adjustStartingBalanceForOlderRows had the same bug and it rewrites the starting balance — netting out a refused row moved real money for a row that does not exist. Now filtered on what actually landed.
Changed: "not checked" is no longer shown as "in sync"
The account list showed a green in sync chip whenever no drift was reported. But that covers both compared and matched and could not be compared at all — a pending transaction, a run that reconciled anything, a create the host refused. The second case claimed a verification that never happened, and is how two phantom drift episodes were read as verified balances.
Three states now render distinctly: off by $X, in sync, and a muted not checked with an explanation on hover — deliberately neither green nor red, because an absence of information should not be coloured like a verdict.
Also
- "Imported last run" survives a reload. It was React state set only when you clicked Sync Now, so it read
—after every reload and permanently for anyone whose syncing is done by the companion. Both syncers now persist the count. - Direction-aware drift banner: positive drift is the bank ahead of its own feed (real lag, self-clearing); negative is Wealthfolio holding more than the bank, which lag cannot cause and which does not clear on its own. The reassuring copy no longer prints for both.
Full detail in CHANGELOG.md.