3e30423083
- 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
186 lines
4.7 KiB
Markdown
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>
|
|
}
|
|
```
|