51c54749cb
- AGENTS.md: add No-implementation rule and Issue-driven workflow section - docs/01: rename default fallback category from 'Other' to 'Uncategorized' - docs/02, 03: update 'Other' references to 'Uncategorized' in delete/seed/default flows - docs/04: replace Canvas charts with Vico 2.x dep, add data feature folder, add AddEditRecurringScreen, document tab-aware FAB and shared ExportAction helper
5.0 KiB
5.0 KiB
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 valueawaitAll(...)— suspend, returns a listsubscribeOne(...)— returnsFlow<T?>subscribeAll(...)— returnsFlow<List<T>>- Filters are parameters, not separate functions
Feature: expense
GetExpenses
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
class UpsertExpense(dao: ExpenseDao) {
suspend fun await(expense: Expense): Long // insert if id=0, update otherwise
}
InsertExpenses
class InsertExpenses(dao: ExpenseDao) {
suspend fun awaitAll(expenses: List<Expense>) // bulk insert, single transaction
}
DeleteExpense
class DeleteExpense(dao: ExpenseDao) {
suspend fun await(id: Long)
}
ReassignExpenseCategory
class ReassignExpenseCategory(dao: ExpenseDao) {
suspend fun await(fromCategoryId: Long, toCategoryId: Long)
}
GetExpenseSummary
class GetExpenseSummary(dao: ExpenseDao, categoryDao: CategoryDao) {
suspend fun await(range: DateRange): ExpenseSummary
}
Feature: category
GetCategories
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 "Uncategorized" category
}
UpsertCategory
class UpsertCategory(dao: CategoryDao) {
suspend fun await(category: Category): Long
}
DeleteCategory
class DeleteCategory(
dao: CategoryDao,
reassignExpenseCategory: ReassignExpenseCategory,
getCategories: GetCategories,
) {
suspend fun await(id: Long)
// internally: reassign orphaned expenses to "Uncategorized", then delete
}
SeedDefaultCategories
class SeedDefaultCategories(dao: CategoryDao) {
suspend fun await() // no-op if categories already exist
}
Feature: recurring
GetRecurringExpenses
class GetRecurringExpenses(dao: RecurringExpenseDao, categoryDao: CategoryDao) {
fun subscribeAll(): Flow<List<RecurringExpenseWithCategory>>
suspend fun awaitOne(id: Long): RecurringExpense?
}
UpsertRecurringExpense
class UpsertRecurringExpense(dao: RecurringExpenseDao) {
suspend fun await(recurring: RecurringExpense): Long
}
DeleteRecurringExpense
class DeleteRecurringExpense(dao: RecurringExpenseDao) {
suspend fun await(id: Long)
// does NOT delete expense instances already created from this template
}
ProcessDueRecurringExpenses
class ProcessDueRecurringExpenses(
recurringDao: RecurringExpenseDao,
expenseDao: ExpenseDao,
) {
suspend fun await(today: LocalDate = LocalDate.now()): List<Expense>
}
Feature: bankstatement
BankStatementImporter (interface)
interface BankStatementImporter {
val bankName: String
suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>>
}
ImportBRIBankStatement : BankStatementImporter
class ImportBRIBankStatement(context: Context) : BankStatementImporter {
override val bankName = "BRI"
override suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>> // stub
}
ImportJagoBankStatement : BankStatementImporter
class ImportJagoBankStatement(context: Context) : BankStatementImporter {
override val bankName = "Jago"
override suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>> // stub
}
ImportBNIBankStatement : BankStatementImporter
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
class ExportExpensesToCsv(expenseDao: ExpenseDao, categoryDao: CategoryDao, context: Context) {
suspend fun await(range: DateRange, outputUri: Uri): Result<Unit>
}
Feature: data
Wipe-all-data administrative action. Triggered from SettingsScreen "Clear all data" AlertDialogPreference.
ClearAllData
class ClearAllData(database: AppDatabase) {
suspend fun await() // deletes all rows from every table
}