7.4 KiB
03 — Function TODOs
What each interactor method does, inputs, outputs, and edge cases.
expense / GetExpenses
subscribeAll(): Flow<List<ExpenseWithCategory>>
JOIN expenses with categories, ordered by date DESC, createdAt DESC. Emits on any change to either table.
subscribeByDateRange(range: DateRange): Flow<List<ExpenseWithCategory>>
Same join, filtered: expense.date >= range.start AND expense.date <= range.end. Ordered date DESC.
awaitOne(id: Long): ExpenseWithCategory?
Single JOIN lookup by expense id. Returns null if not found.
awaitAll(query: String, range: DateRange?): List<ExpenseWithCategory>
One-shot search. If query is blank and range is null, returns all expenses (ordered date DESC). If query is non-blank, filters: note LIKE '%query%' OR CAST(amount AS TEXT) LIKE '%query%' OR category.name LIKE '%query%'. If range is non-null, also filters by date range. Results ordered date DESC.
expense / UpsertExpense
await(expense: Expense): Long
If expense.id == 0L: insert and return generated id. Otherwise: update by id and return the existing id. Routes through separate dao.insert / dao.update (the DAO does not expose a @Upsert method).
expense / InsertExpenses
awaitAll(expenses: List<Expense>)
Bulk insert in a single Room transaction. Used after the user confirms PDF import. expense.id must be 0 for all items.
expense / DeleteExpense
await(id: Long)
Deletes expense by primary key. Does not touch any RecurringExpense template that generated it — those continue running independently.
expense / ReassignExpenseCategory
await(fromCategoryId: Long, toCategoryId: Long)
Single SQL UPDATE expenses SET categoryId = :toCategoryId WHERE categoryId = :fromCategoryId. Used by DeleteCategory before removing a category.
expense / GetExpenseSummary
await(range: DateRange): ExpenseSummary
- Fetch all expenses with category in range (one-shot, uses DAO directly).
- Sum all amounts →
totalAmount. - Group by category, sum per group, sort by amount DESC →
byCategory. - Return
ExpenseSummary(totalAmount, byCategory, range).
category / GetCategories
subscribeAll(): Flow<List<Category>>
All categories ordered by name ASC, emits on change.
awaitOne(id: Long): Category?
Nullable lookup by primary key.
awaitAll(): List<Category>
One-shot list, ordered by name ASC.
awaitDefault(): Category
Returns the single category where isDefault = true. Throws IllegalStateException if not found (seeding hasn't run — should not happen in practice).
category / UpsertCategory
await(category: Category): Long
Insert if id == 0, update otherwise. Returns the id. Does not allow changing isDefault — if the incoming category has isDefault = true but the existing DB record has isDefault = false, the DB value wins (impl reads existing isDefault before update). In practice, only the seeder sets isDefault = true.
category / DeleteCategory
await(id: Long)
- Guard: fetch category by id. If
isDefault = true, throwIllegalArgumentException("Cannot delete a default category"). - Find the fallback: call
getCategories.awaitDefault()to get the "Uncategorized" category id. - Call
reassignExpenseCategory.await(id, fallbackId)to move orphaned expenses. - Delete the category from the DB.
Steps 3 and 4 run sequentially in the interactor (not a DB transaction). If step 4 fails, expenses are already reassigned to Uncategorized — acceptable, retry is safe.
category / SeedDefaultCategories
await()
Checks count of categories in DB. If count > 0, no-op. Otherwise inserts the 8 default categories (see 01-data-model.md). Called from MainApplication.onCreate after startKoin completes, using a CoroutineScope(Dispatchers.IO).
recurring / GetRecurringExpenses
subscribeAll(): Flow<List<RecurringExpenseWithCategory>>
All recurring expenses (active and inactive) joined with category, ordered by nextDueDate ASC. Emits on change.
awaitOne(id: Long): RecurringExpense?
Nullable lookup by primary key.
recurring / UpsertRecurringExpense
await(recurring: RecurringExpense): Long
Insert if id == 0, update otherwise. Caller must set nextDueDate = startDate on initial insert.
recurring / DeleteRecurringExpense
await(id: Long)
Deletes the recurring template by id. Expense instances previously created from this template retain their recurringExpenseId value — they become historical records and are not deleted.
recurring / ProcessDueRecurringExpenses
await(today: LocalDate): List<Expense>
- Query recurring expenses where
isActive = true AND nextDueDate <= today. - For each due item:
a. Create
Expense(amount, categoryId, note from template, date = nextDueDate, recurringExpenseId = template.id). b. Insert the expense. c. Advance:newNextDueDate = interval.advance(current nextDueDate). d. Update the recurring template withnextDueDate = newNextDueDate. - Return the list of created expenses so the caller can show a banner.
Edge case — app not opened for multiple intervals: advances nextDueDate by one interval per call. The next app open will process it again if still overdue. This avoids flooding the expense list with back-filled entries.
bankstatement / BankStatementImporter
val bankName: String
Display name shown in the bank picker (e.g. "BRI", "Jago", "BNI").
await(pdfUri: Uri): Result<List<PendingImportExpense>>
Contract for all implementations:
- Open PDF via
ContentResolver.openInputStream(pdfUri). - Extract text with PDFBox-Android (
PDDocument+PDFTextStripper), page by page. - Parse bank-specific format into raw entry models (e.g.
BRIStatementEntry). - Filter to debit-only rows (skip credits/income).
- Map raw entries to
PendingImportExpense— parse Indonesian number format (1.500.000,00), parse date strings toLocalDate. - Return
Result.success(list)orResult.failure(exception).
ImportBRIBankStatement.await — stub: Result.failure(NotImplementedError("BRI import not yet implemented")).
ImportJagoBankStatement.await — stub: same.
ImportBNIBankStatement.await — stub: same.
export / ExportExpensesToCsv
await(range: DateRange, outputUri: Uri): Result<Unit>
- Fetch all
ExpenseWithCategoryin range from DAO (one-shot). - Open
OutputStreamviacontext.contentResolver.openOutputStream(outputUri). - Wrap with Okio
BufferedSink. - Write UTF-8 BOM (
0xEF, 0xBB, 0xBF) for Excel compatibility. - Write header:
Date,Category,Amount,Note. - For each expense:
datein ISO 8601 (yyyy-MM-dd), thencategory.name,amount,"note"(double-quote the note to handle embedded commas). - Close sink.
- Return
Result.success(Unit)orResult.failure(exception).
data / ClearAllData
await()
- Call
database.clearAllTables()(Room built-in) inside awithTransaction { }block on the database's executor. - No-op guard: this is destructive — the caller (
SettingsScreenAlertDialogPreference) is responsible for showing a confirmation dialog before invoking. - After clearing,
SeedDefaultCategoriesshould be re-run so the app retains the 8 default categories. TheSettingsScreenScreenModel callsinject<SeedDefaultCategories>().await()afterClearAllData.await()returns successfully.