Files
ledgerr/docs/02-interfaces.md
admin 51c54749cb docs + AGENTS: no-implementation rule, issue-driven workflow, Vico, Uncategorized, ClearAllData, AddEditRecurringScreen
- 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
2026-06-28 15:47:28 +07:00

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 value
  • awaitAll(...) — suspend, returns a list
  • subscribeOne(...) — returns Flow<T?>
  • subscribeAll(...) — returns Flow<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
}