Skip to content

Codebase Map

Layout: explore packages under android-app/app/src/main/java/com/zharif/autobudget/ directly; the module-level map and layer diagram live in overview.md. This page holds only what the code can't tell you at a glance, plus extension recipes.

Orientation notes:

  • parser/ and the money/export codecs in common/ are pure Kotlin — no Android imports; JVM-only unit tests depend on it.
  • Seeds: data/seed/DefaultCategories.kt (10 default categories) and parser/banks/BankRuleSets.kt (62 production rules plus 12 debug-shell rules keyed on com.android.shell; debug ones never ship in release).
  • Generated — do not edit by hand: ui/components/MaterialIconCatalog.kt lists all 2083 Material "filled" icon names; regenerate with android-app/scripts/generate_icon_catalog.sh.
  • Exported schema JSON under android-app/app/schemas/ is the source of truth for migration tests.

Universal conventions live in AGENTS.md; behavior-level decisions trace to ADRs (index in overview.md).

Extension recipes

Add a parse rule for an existing bank

  1. Add a ParserRule to BankRuleSets for that package. Regex must use named groups (?<amount>...), optional (?<merchant>...) / (?<account>...). Direction: DEBIT/CREDIT/AUTO; status: AUTO/explicit.
  2. Add golden fixtures: app/src/test/resources/fixtures/<pkg>.json (or extend an existing file) with the expected amountMillis, merchant, direction, status, confidence.
  3. Run ./gradlew testDebugUnitTestParseGoldenTest must pass and keep ≥90% HIGH confidence; update the starter-rule counts in parser.md (intro + package table).
  4. Bump RuleRepository.RULE_SEED_VERSION (1 → 2 → …). Existing installs reconcile once per version: new starter rules are inserted (keyed on the stable ParserRule.idrules.sourceId), legacy rows get their sourceId backfilled by matching label, and user-edited or deleted rules are never touched or re-added (ADR-0019).

Improve ruleset matching from captured notifications

Capture dumps arrive as base64+gzip JSON: {version, notifications:[{sourcePackage, rawText, capturedAt, isMasked}]}.

  1. Decode (base64 -d | gunzip); group samples by sourcePackage.
  2. Simulate that package's current rules against each rawText BEFORE writing any regex; note which miss and why. Match order: embedded JSON wins only when both merchantName and transactionAmount are non-blank — otherwise regex rules run.
  3. Build patterns from the shared tokens in BankRuleSets ($anyCurrencyAmount, $merchantName, $accountRef). Known traps: masked accounts carry 2–6 asterisks (***5660) while $accountRef allows exactly 4; exclude '/ from the merchant class when the boundary is 's; captures pass through cutAtStopWord, so check trailing qualifiers trim cleanly.
  4. Re-simulate: every failing rawText must hit and zero existing fixture bodies may match. Verify detector verdicts per sample too — direction/ status flags (received, refund, pending, …) scan the whole text before the rule's DEBIT/CREDIT/AUTO kind applies.
  5. Finish with Add a parse rule for an existing bank: golden fixtures from the real rawTexts, RULE_SEED_VERSION bump, ParseGoldenTest ≥90% HIGH gate, parser.md counts.

Add a bank package

  1. Add PACKAGE_* constant + rules in BankRuleSets.
  2. Add the app to ui/onboarding/BankCatalog.kt (KNOWN_BANK_APPS) with a display label.
  3. Add <package android:name="…"/> to the <queries> block in AndroidManifest.xml so package detection works on Android 11+.
  4. Fixtures + tests as above.

Add a currency

  1. Add one entry to Currency.SPECS (code, minor digits, symbol) in common/Currency.kt.
  2. If the parser should detect its symbol/token in text, add to Currency.TOKENS (loose, for AmountParser) and Currency.DETECT (word-boundary-safe, for whole-text detection).
  3. supportedCodes() feeds the Settings currency picker automatically.
  4. Add a MoneyFormatterTest / AmountParserTest case if the minor digits or symbol differ from the norm (e.g. JPY 0 digits).

Add a category keyword

  1. Edit KeywordClassifier.DEFAULT_KEYWORDS (or a user's stored overrides). Note stored values replace defaults per category — they don't merge.
  2. Existing installs: the stored category_keywords JSON wins; editing in Settings and saving persists the new list.

Change the category set / icons

  • Defaults live in data/seed/DefaultCategories.kt; icons are strings resolved by ui/components/CategoryIcon.kt (categoryIconVector). Legacy emoji seeds are mapped too.
  • CategoryRepository.ensureSeeded only seeds when the table is empty — existing installs keep their categories.

Category icons come from the full Material "filled" catalog (ADR-0023):

  • ui/components/MaterialIconCatalog.kt is generated — do not edit by hand. It lists all 2083 icon names and resolves "Filled.X" → vector via chunked whens.
  • Regenerate after bumping compose-material-icons-extended: android-app/scripts/generate_icon_catalog.sh [path-to-sources.jar] (defaults to the newest sources jar in the Gradle cache).
  • New categories store the canonical ImageVector.name (e.g. "Filled.Pets"). Legacy short ids ("fastfood") and emoji still resolve via the legacy when fallback in categoryIconVector.
  • The category editor's picker is ui/components/IconPicker.kt (search + lazy grid in a bottom sheet). Search logic filterIconNames is a pure, JVM-testable function.

DB migration procedure

  1. Change entities/DAOs in data/db/.
  2. Bump @Database(version = N+1) in AppDatabase.
  3. Write MIGRATION_N_N+1 and register in addMigrations(...) (DatabaseModule). Prefer SQL (ALTER TABLE …) over Room.databaseBuilder(...).fallbackToDestructiveMigration() — data loss.
  4. Inspect the exported schema JSON in app/schemas/ after build — it's the source of truth for migration tests.
  5. Never change a released migration retroactively.

Add a screen

  1. Add a route constant to Routes in ui/navigation/AutoBudgetNavHost.kt.
  2. Add composable(route) { … } in navGraph(...) with args (navArgument + NavType).
  3. Create the screen + ViewModel (@HiltViewModel, inject repositories, expose one StateFlow<UiState> via combine+flatMapLatest+stateIn).
  4. Reuse ui/components/* (MonthSelector, EmptyState, TransactionRow…).
  5. If it's a tab, add a TabItem to TABS.

Add a setting

  1. Add a KEY_* constant to SettingsRepository.
  2. Read via settings.observe(KEY) (flow) or settings.get(KEY) (one-shot); write via settings.put(KEY, value).
  3. The value lives in the encrypted settings table (Room) — no SharedPreferences for new settings.

Where decisions live

Every behavioral rule above traces to an ADR in docs/adr/; the index table is in docs/architecture/overview.md.