Skip to content

ADR-0007: Category override module

Context

Users can change a transaction's category; the app must remember the choice for future transactions from the same merchant. Reads (during classification) and writes (from the UI) must agree on the key and the normalization.

Decision

Both the override write and read live behind the category assignment module (CategoryClassifier / TransactionRepository.applyCategoryOverride). Normalization happens exactly once, inside the module (MerchantNormalizer.normalize).

Overrides are keyed on (packageName, merchantNormalized) (unique) and read package-scoped.

Consequences

  • The same merchant from two bank apps can map to different categories — the key includes the source package.
  • Callers never normalize themselves, so key drift is impossible.
  • The classifier consults overrides before keyword rules, so a user choice always wins.
  • Future ML classifiers implement the same interface without touching the pipeline or the override store.

Alternatives considered

  • Keying on merchant only — rejected: package scope is required for banks sharing merchant names (e.g. "Grab" from GrabPay vs Maybank).
  • Normalizing at each call site — rejected: drift risk, one canonical normalization is cheaper to reason about.