feat(#2): implement expense interactors

This commit is contained in:
Achmad Setyabudi Susilo
2026-06-28 16:33:39 +07:00
parent 179f5fe2f8
commit 46f882b3c3
7 changed files with 152 additions and 0 deletions
@@ -1,7 +1,9 @@
package dev.achmad.ledgerr.data.local.mapper
import dev.achmad.ledgerr.data.local.dao.ExpenseWithCategoryRow
import dev.achmad.ledgerr.data.local.entity.ExpenseEntity
import dev.achmad.ledgerr.domain.expense.model.Expense
import dev.achmad.ledgerr.domain.expense.model.ExpenseWithCategory
fun ExpenseEntity.toModel(): Expense = Expense(
id = id,
@@ -22,3 +24,8 @@ fun Expense.toEntity(): ExpenseEntity = ExpenseEntity(
recurringExpenseId = recurringExpenseId,
createdAt = createdAt,
)
fun ExpenseWithCategoryRow.toModel(): ExpenseWithCategory = ExpenseWithCategory(
expense = expense.toModel(),
category = category.toModel(),
)
@@ -4,7 +4,12 @@ import dev.achmad.ledgerr.domain.category.interactor.DeleteCategory
import dev.achmad.ledgerr.domain.category.interactor.GetCategories
import dev.achmad.ledgerr.domain.category.interactor.SeedDefaultCategories
import dev.achmad.ledgerr.domain.category.interactor.UpsertCategory
import dev.achmad.ledgerr.domain.expense.interactor.DeleteExpense
import dev.achmad.ledgerr.domain.expense.interactor.GetExpenseSummary
import dev.achmad.ledgerr.domain.expense.interactor.GetExpenses
import dev.achmad.ledgerr.domain.expense.interactor.InsertExpenses
import dev.achmad.ledgerr.domain.expense.interactor.ReassignExpenseCategory
import dev.achmad.ledgerr.domain.expense.interactor.UpsertExpense
import org.koin.dsl.module
val domainModule = module {
@@ -13,5 +18,10 @@ val domainModule = module {
factory { DeleteCategory(get(), get(), get()) }
factory { SeedDefaultCategories(get()) }
factory { GetExpenses(get(), get()) }
factory { UpsertExpense(get()) }
factory { InsertExpenses(get()) }
factory { DeleteExpense(get()) }
factory { ReassignExpenseCategory(get()) }
factory { GetExpenseSummary(get(), get()) }
}
@@ -0,0 +1,11 @@
package dev.achmad.ledgerr.domain.expense.interactor
import dev.achmad.ledgerr.data.local.dao.ExpenseDao
class DeleteExpense(
private val dao: ExpenseDao,
) {
suspend fun await(id: Long) {
dao.deleteById(id)
}
}
@@ -0,0 +1,33 @@
package dev.achmad.ledgerr.domain.expense.interactor
import dev.achmad.ledgerr.data.local.dao.CategoryDao
import dev.achmad.ledgerr.data.local.dao.ExpenseDao
import dev.achmad.ledgerr.data.local.mapper.toModel
import dev.achmad.ledgerr.domain.expense.model.DateRange
import dev.achmad.ledgerr.domain.expense.model.ExpenseSummary
class GetExpenseSummary(
private val dao: ExpenseDao,
private val categoryDao: CategoryDao,
) {
suspend fun await(range: DateRange): ExpenseSummary {
val expenses = dao.getByDateRange(
startDay = range.start.toEpochDay(),
endDay = range.end.toEpochDay(),
)
val totalAmount = expenses.sumOf { it.amount }
val categoryMap = categoryDao.getAll().associate { it.id to it.toModel() }
val byCategory = expenses
.groupBy { it.categoryId }
.mapNotNull { (categoryId, group) ->
val category = categoryMap[categoryId] ?: return@mapNotNull null
category to group.sumOf { it.amount }
}
.sortedByDescending { it.second }
return ExpenseSummary(
totalAmount = totalAmount,
byCategory = byCategory,
period = range,
)
}
}
@@ -0,0 +1,60 @@
package dev.achmad.ledgerr.domain.expense.interactor
import dev.achmad.ledgerr.data.local.dao.CategoryDao
import dev.achmad.ledgerr.data.local.dao.ExpenseDao
import dev.achmad.ledgerr.data.local.mapper.toModel
import dev.achmad.ledgerr.domain.expense.model.DateRange
import dev.achmad.ledgerr.domain.expense.model.ExpenseWithCategory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class GetExpenses(
private val dao: ExpenseDao,
private val categoryDao: CategoryDao,
) {
fun subscribeAll(): Flow<List<ExpenseWithCategory>> =
dao.subscribeAll().map { rows -> rows.map { it.toModel() } }
fun subscribeByDateRange(range: DateRange): Flow<List<ExpenseWithCategory>> =
dao.subscribeByDateRange(
startDay = range.start.toEpochDay(),
endDay = range.end.toEpochDay(),
).map { rows -> rows.map { it.toModel() } }
suspend fun awaitOne(id: Long): ExpenseWithCategory? {
val expense = dao.getById(id) ?: return null
val category = categoryDao.getById(expense.categoryId) ?: return null
return ExpenseWithCategory(
expense = expense.toModel(),
category = category.toModel(),
)
}
suspend fun awaitAll(
query: String = "",
range: DateRange? = null,
): List<ExpenseWithCategory> {
val categoryMap = categoryDao.getAll().associate { it.id to it.toModel() }
val expenses = dao.search(
rangeStart = range?.start?.toEpochDay(),
rangeEnd = range?.end?.toEpochDay(),
)
return expenses
.filter { entity ->
if (query.isBlank()) return@filter true
val note = entity.note.orEmpty()
val amountStr = entity.amount.toString()
val categoryName = categoryMap[entity.categoryId]?.name.orEmpty()
note.contains(query, ignoreCase = true) ||
amountStr.contains(query, ignoreCase = true) ||
categoryName.contains(query, ignoreCase = true)
}
.mapNotNull { entity ->
val category = categoryMap[entity.categoryId] ?: return@mapNotNull null
ExpenseWithCategory(
expense = entity.toModel(),
category = category,
)
}
}
}
@@ -0,0 +1,14 @@
package dev.achmad.ledgerr.domain.expense.interactor
import dev.achmad.ledgerr.data.local.dao.ExpenseDao
import dev.achmad.ledgerr.data.local.mapper.toEntity
import dev.achmad.ledgerr.domain.expense.model.Expense
class InsertExpenses(
private val dao: ExpenseDao,
) {
suspend fun awaitAll(expenses: List<Expense>) {
if (expenses.isEmpty()) return
dao.insertAll(expenses.map { it.toEntity() })
}
}
@@ -0,0 +1,17 @@
package dev.achmad.ledgerr.domain.expense.interactor
import dev.achmad.ledgerr.data.local.dao.ExpenseDao
import dev.achmad.ledgerr.data.local.mapper.toEntity
import dev.achmad.ledgerr.domain.expense.model.Expense
class UpsertExpense(
private val dao: ExpenseDao,
) {
suspend fun await(expense: Expense): Long {
if (expense.id == 0L) {
return dao.insert(expense.toEntity())
}
dao.update(expense.toEntity())
return expense.id
}
}