Files
ledgerr/AGENTS.md
T
Achmad Setyabudi Susilo 3e30423083 docs: add architecture docs, AGENTS.md, CLAUDE.md, and copy UI components
- Add docs/01-04 covering data model, interfaces, function TODOs, and implementation plan
- Add AGENTS.md with project conventions, architecture rules, feature workflow, and git policy
- Add CLAUDE.md pointing to AGENTS.md
- Copy UI components and utils from info-krl-android (preference widgets, scrollbars, etc.)
- Delete obsolete UiModule.kt
2026-06-28 15:08:37 +07:00

246 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ledgerr — Agent Instructions
Personal Android expense tracking app. Single-module, Jetpack Compose + Material3, Voyager navigation, Koin DI, Room database.
---
## Package structure
```
dev.achmad.ledgerr/
├── core/
│ ├── network/ - OkHttp helpers (not used by expense features)
│ └── preference/ - PreferenceStore, AndroidPreferenceStore, Preference<T>
├── di/
│ ├── util/
│ │ └── KoinExtensions.kt - inject<T>() and injectLazy<T>() helpers
│ ├── CoreModule.kt
│ ├── DataModule.kt
│ ├── DomainModule.kt
│ └── PreferenceModule.kt - replaces UiModule.kt (delete UiModule if it exists)
├── domain/
│ ├── preference/
│ │ ├── AppPreference.kt
│ │ └── ExpensePreference.kt
│ ├── expense/
│ │ ├── model/
│ │ └── interactor/
│ ├── category/
│ │ ├── model/
│ │ └── interactor/
│ ├── recurring/
│ │ ├── model/
│ │ └── interactor/
│ ├── bankstatement/
│ │ ├── model/
│ │ └── interactor/
│ └── export/
│ └── interactor/
├── data/
│ └── local/
│ ├── AppDatabase.kt
│ ├── converter/
│ ├── dao/
│ ├── entity/
│ └── mapper/
└── ui/
├── base/
│ ├── MainApplication.kt
│ └── MainActivity.kt
├── components/ - shared Compose components (already copied)
│ └── preference/ - PreferenceScreen + widgets
├── screens/
│ ├── home/ HomeScreen.kt
│ ├── expenses/ ExpenseListScreen.kt
│ ├── add_edit_expense/ AddEditExpenseScreen.kt
│ ├── category/ CategoryScreen.kt
│ └── settings/ SettingsScreen.kt
├── theme/
└── util/
```
---
## Architecture rules
### Domain / interactors
- Each folder in `domain/` is a **feature** with `model/` and `interactor/` subpackages.
- Interactor classes are named after the **action**: `GetExpenses`, `UpsertExpense`, `DeleteCategory`, etc.
- Method names follow this convention:
- `await(...)` — single suspend result
- `awaitAll(...)` — list suspend result
- `awaitOne(id)` — fetch one by ID
- `subscribeAll(...)` — returns `Flow`
- Interactors take DAOs (or `Context` for PDF/export) directly as constructor params — **no repository layer**.
- Interactors can contain business logic (think use-cases).
### Dependency injection (Koin)
- **DI modules live in `di/`**, not `core/`.
- `CoreModule` provides `SharedPreferences` and `PreferenceStore` (as `AndroidPreferenceStore`). Nothing else.
- Interactors are registered as `factory { }`, not `single`.
- Preference classes are registered as `single { }` in `PreferenceModule`.
- DAOs are registered as `single { get<AppDatabase>().xyzDao() }` in `DataModule`.
- Bank statement importers use named qualifiers: `factory<BankStatementImporter>(named("bri")) { ... }`.
- **ScreenModels are NOT registered in Koin.** Never add a ScreenModel to any Koin module.
### ScreenModel pattern
Always use `rememberScreenModel { }` in `Screen.Content()`. Inject dependencies via `inject()` as constructor default params:
```kotlin
class HomeScreenModel(
private val getExpenses: GetExpenses = inject(),
private val appPreference: AppPreference = inject(),
) : ScreenModel { ... }
// In Screen.Content():
val screenModel = rememberScreenModel { HomeScreenModel() }
```
`inject<T>()` is at `dev.achmad.ledgerr.di.util.KoinExtensions`.
### Navigation
- Pure Voyager stack — `Navigator`, `navigator.push(...)`, `navigator.pop()`.
- **No `TabNavigator` at root level.**
- `ExpenseListScreen` has 2 internal tabs using plain Compose `TabRow` + `HorizontalPager` (not Voyager tabs).
- Screen flow:
```
HomeScreen (root)
├── FAB expand → "Manual" → AddEditExpenseScreen
│ → "Import Bank Statement" → ImportBankStatementScreen
├── Export icon → date range dialog → SAF CreateDocument → writes CSV
├── "See all" → ExpenseListScreen
│ ├── Tab: Expenses (search + filter)
│ │ └── FAB expand → same as HomeScreen FAB
│ └── Tab: Recurring
├── "Manage Categories" button (dashboard body) → CategoryScreen
└── Settings icon → SettingsScreen
AddEditExpenseScreen → CategoryScreen (manage categories button)
ImportBankStatementScreen (1 Voyager Screen, 1 ScreenModel, 2 composable functions)
├── ImportBankStatementPickerContent — state is BankPicker or Processing
│ select bank → SAF OpenDocument → loading overlay while parsing
└── ImportBankStatementConfirmationContent — state is Confirmation
toggle/edit per row → InsertExpenses → navigator.pop()
```
- **Export** has no dedicated screen. Icon in `HomeScreen` and `ExpenseListScreen` top bars; also a `TextPreference` in `SettingsScreen`. Tapping shows a date range dialog first, then opens SAF `CreateDocument`. CSV uses ISO 8601 dates (`yyyy-MM-dd`) with UTF-8 BOM.
- **Import** goes through `ImportBankStatementScreen` — one Voyager Screen, one ScreenModel, two internal composables switched by `StateFlow<ImportState>`. `ImportBankStatementPickerContent` handles bank selection and processing; `ImportBankStatementConfirmationContent` handles review, edit, and confirm. On confirm calls `InsertExpenses.awaitAll()` then pops.
### Settings screen
`SettingsScreen` uses the `PreferenceScreen` composable from `ui/components/preference/`. Pattern:
```kotlin
PreferenceScreen(
itemsProvider = {
listOf(
Preference.PreferenceItem.ListPreference(
title = "Theme",
preference = appPreference.appTheme(),
entries = mapOf(...),
),
Preference.PreferenceItem.TextPreference(
title = "Export CSV",
onClick = { /* launch SAF CreateDocument */ }
),
Preference.PreferenceItem.AlertDialogPreference(
title = "Clear all data",
onConfirm = { screenModel.clearData() }
),
)
}
)
```
### Preference classes
Located in `domain/preference/`. Take `PreferenceStore` as constructor param. Methods return `Preference<T>` objects from `core/preference/`:
```kotlin
class AppPreference(private val store: PreferenceStore) {
fun appTheme() = store.getEnum("app_theme", defaultValue = AppTheme.SYSTEM)
}
```
### Data layer
- `LocalDate` stored as `Long` epoch day via `LocalDateConverter` (`LocalDate.toEpochDay()` / `LocalDate.ofEpochDay()`).
- Use `@Upsert` Room annotation for insert-or-update operations.
- `AppDatabase.Callback.onCreate` seeds default categories using `CoroutineScope(Dispatchers.IO)` — Koin is not ready inside Room callbacks.
- minSdk 26, so `java.time.*` is available without desugaring.
### Bank statement import
- `BankStatementImporter` interface: `val bankName: String` + `suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>>`.
- Stubs: `ImportBRIBankStatement`, `ImportJagoBankStatement`, `ImportBNIBankStatement` — all return `Result.failure(NotImplementedError(...))`.
- PDF text extraction: `com.tom-roush:pdfbox-android`. Requires `PDFBoxResourceLoader.init(applicationContext)` in `MainApplication.onCreate()`.
- No manifest permissions needed — SAF handles file access.
### Export
- `ExportExpensesToCsv` interactor writes UTF-8 BOM CSV via Okio to a SAF URI.
- SAF launchers (`ActivityResultContracts.OpenDocument`, `ActivityResultContracts.CreateDocument`) are registered in the Screen composable; URIs are passed to the ScreenModel.
---
## Workflow for new features
Follow these steps in order every time. Do not skip ahead to implementation without explicit approval.
1. **Define structs** — add any new models to the relevant `domain/<feature>/model/` package.
2. **Define interfaces** — add or update interactor class signatures and method stubs in `domain/<feature>/interactor/`. If a new feature has no existing feature folder, create one.
3. **Add TODOs** — fill method bodies with `TODO("...")` comments describing the intended behavior, edge cases, and any invariants.
4. **Self-review** — think through the full implementation. If anything requires going outside or beyond what was defined in steps 13 (new models, new methods, changed signatures), go back to the relevant step and update it before continuing.
5. **Prompt for review** — stop and present a summary of what was defined. Wait for an explicit go-ahead before writing any real implementation code.
6. **Implement** — only after approval: replace TODOs with real code, wire up DI in the relevant module, update any callers.
---
## Key dependencies
| Library | Version | Purpose |
|---------|---------|---------|
| Jetpack Compose + Material3 | (from BOM) | UI |
| Voyager | — | Navigation + ScreenModel |
| Koin | 4.2.2 | DI |
| Room | 2.7.1 | Local DB |
| PDFBox-Android | 2.0.27.0 | PDF text extraction |
| Okio | (transitive) | CSV write |
No chart library — use Canvas (`drawArc` for pie, `drawRect` for bar).
---
## Git
- **Never commit or push unless the user explicitly asks.** Do not auto-commit after completing a task, do not squash, amend, or rebase without being told to.
- **Never add `Co-Authored-By` lines to commit messages.**
---
## Things to avoid
- Do not register ScreenModels in Koin.
- Do not create a repository layer between interactors and DAOs.
- Do not add a `TabNavigator` at the root.
- Do not create a dedicated screen for export — it is a direct action (date range dialog → SAF picker).
- Do not add manifest permissions for file access — SAF handles it.
- Do not use an external chart library.
- Do not rename `MainApplication` back to `LedgerrApp`.
- `UiModule.kt` has been deleted — do not recreate it; the replacement is `PreferenceModule.kt`.
- Do not use `@KoinViewModel` or `viewModel {}` — this project uses Voyager `ScreenModel`.
---
## Docs
Full design documentation is in `docs/`:
- `01-data-model.md` — domain models per feature
- `02-interfaces.md` — all interactor signatures
- `03-function-todos.md` — per-method behavior and edge cases
- `04-implementation-plan.md` — package structure, DI wiring, build order