# 03 — Function TODOs What each interactor method does, inputs, outputs, and edge cases. --- ## expense / GetExpenses ### `subscribeAll(): Flow>` JOIN expenses with categories, ordered by `date DESC`, `createdAt DESC`. Emits on any change to either table. ### `subscribeByDateRange(range: DateRange): Flow>` 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` 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. Uses Room `@Upsert`. --- ## expense / InsertExpenses ### `awaitAll(expenses: List)` 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` 1. Fetch all expenses with category in range (one-shot, uses DAO directly). 2. Sum all amounts → `totalAmount`. 3. Group by category, sum per group, sort by amount DESC → `byCategory`. 4. Return `ExpenseSummary(totalAmount, byCategory, range)`. --- ## category / GetCategories ### `subscribeAll(): Flow>` All categories ordered by name ASC, emits on change. ### `awaitOne(id: Long): Category?` Nullable lookup by primary key. ### `awaitAll(): List` 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)` 1. Guard: fetch category by id. If `isDefault = true`, throw `IllegalArgumentException("Cannot delete a default category")`. 2. Find the fallback: call `getCategories.awaitDefault()` to get the "Other" category id. 3. Call `reassignExpenseCategory.await(id, fallbackId)` to move orphaned expenses. 4. 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 Other — 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>` 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` 1. Query recurring expenses where `isActive = true AND nextDueDate <= today`. 2. 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 with `nextDueDate = newNextDueDate`. 3. 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>` Contract for all implementations: 1. Open PDF via `ContentResolver.openInputStream(pdfUri)`. 2. Extract text with PDFBox-Android (`PDDocument` + `PDFTextStripper`), page by page. 3. Parse bank-specific format into raw entry models (e.g. `BRIStatementEntry`). 4. Filter to debit-only rows (skip credits/income). 5. Map raw entries to `PendingImportExpense` — parse Indonesian number format (`1.500.000,00`), parse date strings to `LocalDate`. 6. Return `Result.success(list)` or `Result.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` 1. Fetch all `ExpenseWithCategory` in range from DAO (one-shot). 2. Open `OutputStream` via `context.contentResolver.openOutputStream(outputUri)`. 3. Wrap with Okio `BufferedSink`. 4. Write UTF-8 BOM (`0xEF, 0xBB, 0xBF`) for Excel compatibility. 5. Write header: `Date,Category,Amount,Note`. 6. For each expense: `date` in ISO 8601 (`yyyy-MM-dd`), then `category.name`, `amount`, `"note"` (double-quote the note to handle embedded commas). 7. Close sink. 8. Return `Result.success(Unit)` or `Result.failure(exception)`.