Implement expense interactors (#2) #11
Reference in New Issue
Block a user
Delete Branch "feat/2-implement-expense-interactors"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
GetExpenses(4 methods),UpsertExpense,InsertExpenses,DeleteExpense,GetExpenseSummary(ReassignExpenseCategorywas already implemented onmain)ExpenseWithCategoryRow.toModel()mapper for the joined DAO row (used by bothsubscribe*methods andawaitOne)factory { }inDomainModuleInsertExpenses.awaitAllenforcesid == 0viarequirebefore delegating to the plain@Insertdocs/03-function-todos.mddoc drift (UpsertExpenseroutes through separateinsert/update, not@Upsert)GetExpenseSummaryexplaining the orphan-category drop behaviorTest plan
./gradlew assembleDebugsucceedsGetExpenses.awaitOnereturnsnullwhen either the expense or its category row is missingInsertExpenses.awaitAllthrowsIllegalArgumentExceptionon any non-zeroidGetExpenses.awaitAllfilters onnote/category.namecase-insensitively and onamountas a string matchGetExpenseSummary.awaitsums amounts and groups by category, sorted DESCDomainModule)Closes #2
PR looks good and is faithful to the spec. A few findings, in priority order:
Must address:
GetExpenses.awaitOnedoesn't use the newExpenseWithCategoryRow.toModel()helper that this PR adds toExpenseMapper.kt— see inline comment.InsertExpenses.awaitAlldoesn't enforce the "allid == 0" invariant fromdocs/03-function-todos.md:33— see inline comment.assembleDebugsucceeds" — a smoke test that calls each method (or unit tests if the project has any) would be reassuring.Nits (non-blocking):
GetExpenses.ktsearch filter usesamountStr.contains(query, ignoreCase = true)—ignoreCaseis meaningless on numeric strings; drop it.docs/03-function-todos.md:26saysUpsertExpense.await"Uses Room@Upsert", but the implementation routes through separatedao.insert/dao.update(and the DAO has no@Upsertmethod). Doc drift, not a code bug.GetExpenseSummarysilently drops expenses with deleted categories viamapNotNull, which is asymmetric with the@Relationnon-nullable path used bysubscribeAll/subscribeByDateRange. Worth a one-line comment, or returningbyCategorywithout the dropped amounts.Verdict: approve after the two must-fix items.
@@ -0,0 +25,4 @@val expense = dao.getById(id) ?: return nullval category = categoryDao.getById(expense.categoryId) ?: return nullreturn ExpenseWithCategory(expense = expense.toModel(),This manually inlines what the new
ExpenseWithCategoryRow.toModel()mapper inExpenseMapper.ktalready does. The helper is added in this same PR and used bysubscribeAll(line 16) andsubscribeByDateRange(line 22), but not here. Pick one for consistency:ExpenseWithCategoryRow(expense, category).toModel()here, or.map { it.expense.toModel() ... }in the two subscribers.@@ -0,0 +9,4 @@) {suspend fun awaitAll(expenses: List<Expense>) {if (expenses.isEmpty()) returndao.insertAll(expenses.map { it.toEntity() })docs/03-function-todos.md:33says "expense.id must be 0 for all items", but this isn't enforced. A non-zero id would hit the plain@Insertondao.insertAll(noOnConflictStrategy.REPLACE) and throwSQLiteConstraintExceptionat runtime. Either:require(expenses.all { it.id == 0L })(fail fast with a clear message), or.map { it.copy(id = 0).toEntity() }(silently normalize — less safe, but matches the bulk-import use case where ids are caller-provided).