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
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# 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>
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user