From 6a1128421271673ab1b163de78cc491dac733076 Mon Sep 17 00:00:00 2001 From: Achmad Setyabudi Susilo Date: Sun, 28 Jun 2026 16:33:41 +0700 Subject: [PATCH] feat(#3): implement recurring interactors --- .../dev/achmad/ledgerr/di/DomainModule.kt | 9 +++++ .../interactor/DeleteRecurringExpense.kt | 11 ++++++ .../interactor/GetRecurringExpenses.kt | 25 +++++++++++++ .../interactor/ProcessDueRecurringExpenses.kt | 36 +++++++++++++++++++ .../interactor/UpsertRecurringExpense.kt | 19 ++++++++++ 5 files changed, 100 insertions(+) create mode 100644 app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/DeleteRecurringExpense.kt create mode 100644 app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/GetRecurringExpenses.kt create mode 100644 app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/ProcessDueRecurringExpenses.kt create mode 100644 app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/UpsertRecurringExpense.kt diff --git a/app/src/main/java/dev/achmad/ledgerr/di/DomainModule.kt b/app/src/main/java/dev/achmad/ledgerr/di/DomainModule.kt index 56481b8..37af15b 100644 --- a/app/src/main/java/dev/achmad/ledgerr/di/DomainModule.kt +++ b/app/src/main/java/dev/achmad/ledgerr/di/DomainModule.kt @@ -5,6 +5,10 @@ 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.ReassignExpenseCategory +import dev.achmad.ledgerr.domain.recurring.interactor.DeleteRecurringExpense +import dev.achmad.ledgerr.domain.recurring.interactor.GetRecurringExpenses +import dev.achmad.ledgerr.domain.recurring.interactor.ProcessDueRecurringExpenses +import dev.achmad.ledgerr.domain.recurring.interactor.UpsertRecurringExpense import org.koin.dsl.module val domainModule = module { @@ -14,4 +18,9 @@ val domainModule = module { factory { SeedDefaultCategories(get()) } factory { ReassignExpenseCategory(get()) } + + factory { GetRecurringExpenses(get()) } + factory { UpsertRecurringExpense(get()) } + factory { DeleteRecurringExpense(get()) } + factory { ProcessDueRecurringExpenses(get(), get()) } } diff --git a/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/DeleteRecurringExpense.kt b/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/DeleteRecurringExpense.kt new file mode 100644 index 0000000..aaaf91a --- /dev/null +++ b/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/DeleteRecurringExpense.kt @@ -0,0 +1,11 @@ +package dev.achmad.ledgerr.domain.recurring.interactor + +import dev.achmad.ledgerr.data.local.dao.RecurringExpenseDao + +class DeleteRecurringExpense( + private val dao: RecurringExpenseDao, +) { + suspend fun await(id: Long) { + dao.deleteById(id) + } +} diff --git a/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/GetRecurringExpenses.kt b/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/GetRecurringExpenses.kt new file mode 100644 index 0000000..418038b --- /dev/null +++ b/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/GetRecurringExpenses.kt @@ -0,0 +1,25 @@ +package dev.achmad.ledgerr.domain.recurring.interactor + +import dev.achmad.ledgerr.data.local.dao.RecurringExpenseDao +import dev.achmad.ledgerr.data.local.mapper.toModel +import dev.achmad.ledgerr.domain.recurring.model.RecurringExpense +import dev.achmad.ledgerr.domain.recurring.model.RecurringExpenseWithCategory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetRecurringExpenses( + private val recurringDao: RecurringExpenseDao, +) { + fun subscribeAll(): Flow> = + recurringDao.subscribeAll().map { rows -> + rows.map { row -> + RecurringExpenseWithCategory( + recurring = row.recurring.toModel(), + category = row.category.toModel(), + ) + } + } + + suspend fun awaitOne(id: Long): RecurringExpense? = + recurringDao.getById(id)?.toModel() +} diff --git a/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/ProcessDueRecurringExpenses.kt b/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/ProcessDueRecurringExpenses.kt new file mode 100644 index 0000000..b85c8aa --- /dev/null +++ b/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/ProcessDueRecurringExpenses.kt @@ -0,0 +1,36 @@ +package dev.achmad.ledgerr.domain.recurring.interactor + +import dev.achmad.ledgerr.data.local.dao.ExpenseDao +import dev.achmad.ledgerr.data.local.dao.RecurringExpenseDao +import dev.achmad.ledgerr.data.local.mapper.toEntity +import dev.achmad.ledgerr.data.local.mapper.toModel +import dev.achmad.ledgerr.domain.expense.model.Expense +import java.time.LocalDate + +class ProcessDueRecurringExpenses( + private val recurringDao: RecurringExpenseDao, + private val expenseDao: ExpenseDao, +) { + suspend fun await(today: LocalDate = LocalDate.now()): List { + val dueTemplates = recurringDao.getDue(today.toEpochDay()) + if (dueTemplates.isEmpty()) return emptyList() + val created = mutableListOf() + for (templateEntity in dueTemplates) { + val template = templateEntity.toModel() + val expense = Expense( + amount = template.amount, + categoryId = template.categoryId, + date = template.nextDueDate, + note = template.note, + recurringExpenseId = template.id, + ) + val newId = expenseDao.insert(expense.toEntity()) + created += expense.copy(id = newId) + val advanced = template.copy( + nextDueDate = template.interval.advance(template.nextDueDate), + ) + recurringDao.update(advanced.toEntity()) + } + return created + } +} diff --git a/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/UpsertRecurringExpense.kt b/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/UpsertRecurringExpense.kt new file mode 100644 index 0000000..6c84104 --- /dev/null +++ b/app/src/main/java/dev/achmad/ledgerr/domain/recurring/interactor/UpsertRecurringExpense.kt @@ -0,0 +1,19 @@ +package dev.achmad.ledgerr.domain.recurring.interactor + +import dev.achmad.ledgerr.data.local.dao.RecurringExpenseDao +import dev.achmad.ledgerr.data.local.mapper.toEntity +import dev.achmad.ledgerr.domain.recurring.model.RecurringExpense + +class UpsertRecurringExpense( + private val dao: RecurringExpenseDao, +) { + suspend fun await(recurring: RecurringExpense): Long { + val entity = recurring.toEntity() + return if (recurring.id == 0L) { + dao.insert(entity) + } else { + dao.update(entity) + recurring.id + } + } +}