Files
ledgerr/docs/02-interfaces.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

186 lines
4.7 KiB
Markdown

# 02 — Interactors
Each feature under `dev.achmad.ledgerr.domain.<feature>.interactor` owns a set of interactor classes. Interactors are use-case classes that can hold logic and depend on whatever they need (DAOs, Android context, etc.). The UI layer depends only on domain — never on data directly.
Naming convention for methods:
- `await(...)` — suspend, returns a single value
- `awaitAll(...)` — suspend, returns a list
- `subscribeOne(...)` — returns `Flow<T?>`
- `subscribeAll(...)` — returns `Flow<List<T>>`
- Filters are parameters, not separate functions
---
## Feature: expense
### `GetExpenses`
```kotlin
class GetExpenses(dao: ExpenseDao, categoryDao: CategoryDao) {
fun subscribeAll(): Flow<List<ExpenseWithCategory>>
fun subscribeByDateRange(range: DateRange): Flow<List<ExpenseWithCategory>>
suspend fun awaitOne(id: Long): ExpenseWithCategory?
suspend fun awaitAll(query: String = "", range: DateRange? = null): List<ExpenseWithCategory>
}
```
### `UpsertExpense`
```kotlin
class UpsertExpense(dao: ExpenseDao) {
suspend fun await(expense: Expense): Long // insert if id=0, update otherwise
}
```
### `InsertExpenses`
```kotlin
class InsertExpenses(dao: ExpenseDao) {
suspend fun awaitAll(expenses: List<Expense>) // bulk insert, single transaction
}
```
### `DeleteExpense`
```kotlin
class DeleteExpense(dao: ExpenseDao) {
suspend fun await(id: Long)
}
```
### `ReassignExpenseCategory`
```kotlin
class ReassignExpenseCategory(dao: ExpenseDao) {
suspend fun await(fromCategoryId: Long, toCategoryId: Long)
}
```
### `GetExpenseSummary`
```kotlin
class GetExpenseSummary(dao: ExpenseDao, categoryDao: CategoryDao) {
suspend fun await(range: DateRange): ExpenseSummary
}
```
---
## Feature: category
### `GetCategories`
```kotlin
class GetCategories(dao: CategoryDao) {
fun subscribeAll(): Flow<List<Category>>
suspend fun awaitOne(id: Long): Category?
suspend fun awaitAll(): List<Category>
suspend fun awaitDefault(): Category // returns the isDefault=true "Other" category
}
```
### `UpsertCategory`
```kotlin
class UpsertCategory(dao: CategoryDao) {
suspend fun await(category: Category): Long
}
```
### `DeleteCategory`
```kotlin
class DeleteCategory(
dao: CategoryDao,
reassignExpenseCategory: ReassignExpenseCategory,
getCategories: GetCategories,
) {
suspend fun await(id: Long)
// internally: reassign orphaned expenses to "Other", then delete
}
```
### `SeedDefaultCategories`
```kotlin
class SeedDefaultCategories(dao: CategoryDao) {
suspend fun await() // no-op if categories already exist
}
```
---
## Feature: recurring
### `GetRecurringExpenses`
```kotlin
class GetRecurringExpenses(dao: RecurringExpenseDao, categoryDao: CategoryDao) {
fun subscribeAll(): Flow<List<RecurringExpenseWithCategory>>
suspend fun awaitOne(id: Long): RecurringExpense?
}
```
### `UpsertRecurringExpense`
```kotlin
class UpsertRecurringExpense(dao: RecurringExpenseDao) {
suspend fun await(recurring: RecurringExpense): Long
}
```
### `DeleteRecurringExpense`
```kotlin
class DeleteRecurringExpense(dao: RecurringExpenseDao) {
suspend fun await(id: Long)
// does NOT delete expense instances already created from this template
}
```
### `ProcessDueRecurringExpenses`
```kotlin
class ProcessDueRecurringExpenses(
recurringDao: RecurringExpenseDao,
expenseDao: ExpenseDao,
) {
suspend fun await(today: LocalDate = LocalDate.now()): List<Expense>
}
```
---
## Feature: bankstatement
### `BankStatementImporter` (interface)
```kotlin
interface BankStatementImporter {
val bankName: String
suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>>
}
```
### `ImportBRIBankStatement : BankStatementImporter`
```kotlin
class ImportBRIBankStatement(context: Context) : BankStatementImporter {
override val bankName = "BRI"
override suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>> // stub
}
```
### `ImportJagoBankStatement : BankStatementImporter`
```kotlin
class ImportJagoBankStatement(context: Context) : BankStatementImporter {
override val bankName = "Jago"
override suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>> // stub
}
```
### `ImportBNIBankStatement : BankStatementImporter`
```kotlin
class ImportBNIBankStatement(context: Context) : BankStatementImporter {
override val bankName = "BNI"
override suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>> // stub
}
```
`DomainModule` binds all three as `List<BankStatementImporter>` so the UI can show a bank picker.
---
## Feature: export
### `ExportExpensesToCsv`
```kotlin
class ExportExpensesToCsv(expenseDao: ExpenseDao, categoryDao: CategoryDao, context: Context) {
suspend fun await(range: DateRange, outputUri: Uri): Result<Unit>
}
```