feat(#4): implement bankstatement, export, and data interactors

This commit is contained in:
Achmad Setyabudi Susilo
2026-06-28 17:37:04 +07:00
parent 105c858d57
commit 94d40d4216
8 changed files with 120 additions and 0 deletions
@@ -1,9 +1,15 @@
package dev.achmad.ledgerr.di
import dev.achmad.ledgerr.domain.bankstatement.interactor.BankStatementImporter
import dev.achmad.ledgerr.domain.bankstatement.interactor.ImportBNIBankStatement
import dev.achmad.ledgerr.domain.bankstatement.interactor.ImportBRIBankStatement
import dev.achmad.ledgerr.domain.bankstatement.interactor.ImportJagoBankStatement
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.data.interactor.ClearAllData
import dev.achmad.ledgerr.domain.export.interactor.ExportExpensesToCsv
import dev.achmad.ledgerr.domain.expense.interactor.DeleteExpense
import dev.achmad.ledgerr.domain.expense.interactor.GetExpenseSummary
import dev.achmad.ledgerr.domain.expense.interactor.GetExpenses
@@ -14,6 +20,8 @@ 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module
val domainModule = module {
@@ -33,4 +41,15 @@ val domainModule = module {
factory { UpsertRecurringExpense(get()) }
factory { DeleteRecurringExpense(get()) }
factory { ProcessDueRecurringExpenses(get()) }
factory<BankStatementImporter>(named("bri")) { ImportBRIBankStatement(androidContext()) }
factory<BankStatementImporter>(named("jago")) { ImportJagoBankStatement(androidContext()) }
factory<BankStatementImporter>(named("bni")) { ImportBNIBankStatement(androidContext()) }
factory<List<BankStatementImporter>> {
listOf(get(named("bri")), get(named("jago")), get(named("bni")))
}
factory { ExportExpensesToCsv(get(), get(), androidContext()) }
factory { ClearAllData(get()) }
}
@@ -0,0 +1,9 @@
package dev.achmad.ledgerr.domain.bankstatement.interactor
import android.net.Uri
import dev.achmad.ledgerr.domain.bankstatement.model.PendingImportExpense
interface BankStatementImporter {
val bankName: String
suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>>
}
@@ -0,0 +1,13 @@
package dev.achmad.ledgerr.domain.bankstatement.interactor
import android.content.Context
import android.net.Uri
import dev.achmad.ledgerr.domain.bankstatement.model.PendingImportExpense
class ImportBNIBankStatement(
@Suppress("unused") private val context: Context,
) : BankStatementImporter {
override val bankName: String = "BNI"
override suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>> =
Result.failure(NotImplementedError("BNI import not yet implemented"))
}
@@ -0,0 +1,13 @@
package dev.achmad.ledgerr.domain.bankstatement.interactor
import android.content.Context
import android.net.Uri
import dev.achmad.ledgerr.domain.bankstatement.model.PendingImportExpense
class ImportBRIBankStatement(
@Suppress("unused") private val context: Context,
) : BankStatementImporter {
override val bankName: String = "BRI"
override suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>> =
Result.failure(NotImplementedError("BRI import not yet implemented"))
}
@@ -0,0 +1,13 @@
package dev.achmad.ledgerr.domain.bankstatement.interactor
import android.content.Context
import android.net.Uri
import dev.achmad.ledgerr.domain.bankstatement.model.PendingImportExpense
class ImportJagoBankStatement(
@Suppress("unused") private val context: Context,
) : BankStatementImporter {
override val bankName: String = "Jago"
override suspend fun await(pdfUri: Uri): Result<List<PendingImportExpense>> =
Result.failure(NotImplementedError("Jago import not yet implemented"))
}
@@ -0,0 +1,14 @@
package dev.achmad.ledgerr.domain.data.interactor
import androidx.room.withTransaction
import dev.achmad.ledgerr.data.local.AppDatabase
class ClearAllData(
private val database: AppDatabase,
) {
suspend fun await() {
database.withTransaction {
database.clearAllTables()
}
}
}
@@ -0,0 +1,37 @@
package dev.achmad.ledgerr.domain.export.interactor
import android.content.Context
import android.net.Uri
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 okio.buffer
import okio.sink
class ExportExpensesToCsv(
private val expenseDao: ExpenseDao,
private val categoryDao: CategoryDao,
private val context: Context,
) {
suspend fun await(range: DateRange, outputUri: Uri): Result<Unit> = runCatching {
val expenses = expenseDao.getByDateRange(
startDay = range.start.toEpochDay(),
endDay = range.end.toEpochDay(),
).map { it.toModel() }
val categoryMap = categoryDao.getAll().associate { entity ->
entity.id to entity.toModel()
}
val outputStream = context.contentResolver.openOutputStream(outputUri)
?: error("Failed to open output stream for $outputUri")
outputStream.sink().buffer().use { sink ->
sink.writeUtf8("\uFEFF")
sink.writeUtf8("Date,Category,Amount,Note\n")
expenses.forEach { expense ->
val categoryName = categoryMap[expense.categoryId]?.name.orEmpty()
val note = expense.note.orEmpty()
sink.writeUtf8("${expense.date},$categoryName,${expense.amount},\"$note\"\n")
}
}
}
}
@@ -1,6 +1,7 @@
package dev.achmad.ledgerr.ui.base
import android.app.Application
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
import dev.achmad.ledgerr.di.coreModule
import dev.achmad.ledgerr.di.dataModule
import dev.achmad.ledgerr.di.domainModule
@@ -17,6 +18,7 @@ class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
PDFBoxResourceLoader.init(this)
startKoin {
androidLogger()
androidContext(this@MainApplication)