ADR-0007: Category override module
- Status: Accepted
- Date: 2026-08-11
- Deciders: maintainers
- Related: classification.md
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.