From 8d26ddaeb1da5fa93169556c24b6ea0be723bd8f Mon Sep 17 00:00:00 2001 From: Dmitry Marchuk Date: Sun, 11 Aug 2024 21:15:06 +0300 Subject: [PATCH] monobudget import: WIP --- .gitignore | 1 + build.gradle.kts | 2 +- .../github/smaugfm/monobudget/Application.kt | 54 ++---------- .../io/github/smaugfm/monobudget/Main.kt | 17 ++-- .../monobudget/common/BaseApplication.kt | 62 ++++++++++++++ .../common/account/TransferCache.kt | 6 +- .../serializer/LocalDateAsISOSerializer.kt | 2 +- .../transaction/NewTransactionFactory.kt | 2 +- .../monobudget/importer/CsvMonoItem.kt | 26 ++++++ .../importer/ImportAccountConfig.kt | 8 ++ .../monobudget/importer/ImportApplication.kt | 53 ++++++++++++ .../monobudget/importer/ImportConfig.kt | 30 +++++++ .../importer/ImportStatementItem.kt | 28 +++++++ .../importer/ImportStatementSource.kt | 49 +++++++++++ .../importer/MonobankInstantSerializer.kt | 42 ++++++++++ .../LunchmoneyNewTransactionFactory.kt | 6 +- .../lunchmoney/LunchmoneyTransferCache.kt | 5 +- .../monobudget/ynab/YnabTransferCache.kt | 5 +- .../monobudget/common/misc/Playground.kt | 84 +------------------ 19 files changed, 338 insertions(+), 144 deletions(-) create mode 100644 src/main/kotlin/io/github/smaugfm/monobudget/common/BaseApplication.kt create mode 100644 src/main/kotlin/io/github/smaugfm/monobudget/importer/CsvMonoItem.kt create mode 100644 src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportAccountConfig.kt create mode 100644 src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportApplication.kt create mode 100644 src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportConfig.kt create mode 100644 src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportStatementItem.kt create mode 100644 src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportStatementSource.kt create mode 100644 src/main/kotlin/io/github/smaugfm/monobudget/importer/MonobankInstantSerializer.kt diff --git a/.gitignore b/.gitignore index 2c0f73b..61d2cae 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ out/ settings*.json settings*.yml +import-config*.yml retries.json docker-compose.yml diff --git a/build.gradle.kts b/build.gradle.kts index f15f2f9..9d8de35 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,7 +54,7 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson") implementation("de.brudaswen.kotlinx.serialization:kotlinx-serialization-csv:2.0.0") implementation("com.charleskorn.kaml:kaml:0.55.0") - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0") implementation("io.github.oshai:kotlin-logging:5.1.0") implementation("com.google.code.gson:gson:2.10.1") testImplementation("io.mockk:mockk:1.13.8") diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/Application.kt b/src/main/kotlin/io/github/smaugfm/monobudget/Application.kt index f93ce1c..4130bb0 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/Application.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/Application.kt @@ -1,66 +1,24 @@ package io.github.smaugfm.monobudget import io.github.oshai.kotlinlogging.KotlinLogging -import io.github.smaugfm.monobudget.common.exception.BudgetBackendException +import io.github.smaugfm.monobudget.common.BaseApplication import io.github.smaugfm.monobudget.common.startup.ApplicationStartupVerifier import io.github.smaugfm.monobudget.common.statement.StatementSource -import io.github.smaugfm.monobudget.common.statement.lifecycle.StatementEvents -import io.github.smaugfm.monobudget.common.statement.lifecycle.StatementItemProcessor -import io.github.smaugfm.monobudget.common.statement.lifecycle.StatementProcessingScopeComponent import io.github.smaugfm.monobudget.common.telegram.TelegramApi import io.github.smaugfm.monobudget.common.telegram.TelegramCallbackHandler import io.github.smaugfm.monobudget.common.util.injectAll -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapMerge -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import org.koin.core.component.KoinComponent import org.koin.core.component.inject import kotlin.system.exitProcess private val log = KotlinLogging.logger {} -class Application : - KoinComponent { +class Application(statementSources: List) : + BaseApplication(statementSources) { private val telegramApi by inject() - private val statementSources by injectAll() private val startupVerifiers by injectAll() private val telegramCallbackHandler by inject>() - private val statementEvents by inject() - suspend fun run() { - runStartupChecks() - - statementSources.forEach { it.prepare() } - - telegramApi.start(telegramCallbackHandler::handle) - log.info { "Started application" } - - statementSources.asFlow() - .flatMapMerge { it.statements() } - .filter(statementEvents::onNewStatement) - .map(::StatementProcessingScopeComponent) - .onEach { - with(it) { - try { - scope.get>() - .process() - statementEvents.onStatementEnd(ctx) - } catch (e: BudgetBackendException) { - statementEvents.onStatementRetry(ctx, e) - } catch (e: Throwable) { - statementEvents.onStatementError(ctx, e) - } finally { - scope.close() - } - } - } - .collect() - } - - private suspend fun runStartupChecks() { + override suspend fun beforeStart() { try { startupVerifiers.forEach { it.verify() } } catch (e: Throwable) { @@ -68,4 +26,8 @@ class Application : exitProcess(1) } } + + override suspend fun afterSourcesPrepare() { + telegramApi.start(telegramCallbackHandler::handle) + } } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt b/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt index 4d15d92..e6b0752 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt @@ -14,6 +14,7 @@ import io.github.smaugfm.monobudget.common.model.settings.MonoAccountSettings import io.github.smaugfm.monobudget.common.model.settings.Settings import io.github.smaugfm.monobudget.common.retry.JacksonFileStatementRetryRepository import io.github.smaugfm.monobudget.common.retry.StatementRetryRepository +import io.github.smaugfm.monobudget.importer.ImportApplication import io.github.smaugfm.monobudget.lunchmoney.LunchmoneyModule import io.github.smaugfm.monobudget.mono.MonoApi import io.github.smaugfm.monobudget.mono.MonoModule @@ -37,7 +38,13 @@ private val log = KotlinLogging.logger {} private const val DEFAULT_HTTP_PORT = 80 -fun main() { +fun main(args: Array) { + if (args.size == 1 && args[0] == "import") { + return runBlocking { + ImportApplication.main(this) + } + } + val env = System.getenv() val setWebhook = env["SET_WEBHOOK"]?.toBoolean() ?: false val monoWebhookUrl = URI(env["MONO_WEBHOOK_URL"]!!) @@ -54,13 +61,13 @@ fun main() { log.debug { "\twebhookSettings: $webhookSettings" } runBlocking { - startKoin { + val koin = startKoin { setupKoinModules(this@runBlocking, jsonRetryRepository, settings, webhookSettings) - } + }.koin when (budgetBackend) { - is Lunchmoney -> Application() - is YNAB -> Application() + is Lunchmoney -> Application(koin.getAll()) + is YNAB -> Application(koin.getAll()) }.run() } } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/BaseApplication.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/BaseApplication.kt new file mode 100644 index 0000000..2e64870 --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/BaseApplication.kt @@ -0,0 +1,62 @@ +package io.github.smaugfm.monobudget.common + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.github.smaugfm.monobudget.common.exception.BudgetBackendException +import io.github.smaugfm.monobudget.common.statement.StatementSource +import io.github.smaugfm.monobudget.common.statement.lifecycle.StatementEvents +import io.github.smaugfm.monobudget.common.statement.lifecycle.StatementItemProcessor +import io.github.smaugfm.monobudget.common.statement.lifecycle.StatementProcessingScopeComponent +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +private val log = KotlinLogging.logger {} + +open class BaseApplication( + private val statementSources: List +) : KoinComponent { + + private val statementEvents by inject() + + suspend fun run() { + beforeStart() + + statementSources.forEach { it.prepare() } + + afterSourcesPrepare() + + log.info { "Started application" } + + statementSources.asFlow() + .flatMapMerge { it.statements() } + .filter(statementEvents::onNewStatement) + .map(::StatementProcessingScopeComponent) + .onEach { + with(it) { + try { + scope.get>() + .process() + statementEvents.onStatementEnd(ctx) + } catch (e: BudgetBackendException) { + statementEvents.onStatementRetry(ctx, e) + } catch (e: Throwable) { + statementEvents.onStatementError(ctx, e) + } finally { + scope.close() + } + } + } + .collect() + } + + protected open suspend fun beforeStart() { + } + + protected open suspend fun afterSourcesPrepare() { + } +} diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferCache.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferCache.kt index 9c8cc17..69bd496 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferCache.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferCache.kt @@ -3,7 +3,7 @@ package io.github.smaugfm.monobudget.common.account import io.github.smaugfm.monobudget.common.model.financial.StatementItem import io.github.smaugfm.monobudget.common.util.misc.ConcurrentExpiringMap import kotlinx.coroutines.Deferred -import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration -abstract class TransferCache : - ConcurrentExpiringMap>(1.minutes) +abstract class TransferCache(expirationDuration: Duration) : + ConcurrentExpiringMap>(expirationDuration) diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/model/serializer/LocalDateAsISOSerializer.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/model/serializer/LocalDateAsISOSerializer.kt index 8914a7f..84399fe 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/model/serializer/LocalDateAsISOSerializer.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/model/serializer/LocalDateAsISOSerializer.kt @@ -23,6 +23,6 @@ class LocalDateAsISOSerializer : KSerializer { } override fun deserialize(decoder: Decoder): LocalDate { - return decoder.decodeString().toLocalDate() + return LocalDate.parse(decoder.decodeString()) } } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/transaction/NewTransactionFactory.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/transaction/NewTransactionFactory.kt index c733b2a..95ae86e 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/transaction/NewTransactionFactory.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/transaction/NewTransactionFactory.kt @@ -24,6 +24,6 @@ abstract class NewTransactionFactory : KoinComponent { companion object { @JvmStatic - protected fun StatementItem?.formatDescription() = (this?.description ?: "").replaceNewLines() + fun StatementItem?.formatDescription() = (this?.description ?: "").replaceNewLines() } } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/importer/CsvMonoItem.kt b/src/main/kotlin/io/github/smaugfm/monobudget/importer/CsvMonoItem.kt new file mode 100644 index 0000000..19d0809 --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/importer/CsvMonoItem.kt @@ -0,0 +1,26 @@ +package io.github.smaugfm.monobudget.importer + +import io.github.smaugfm.monobudget.common.model.financial.BankAccountId +import io.github.smaugfm.monobudget.common.model.serializer.CurrencyAsStringSerializer +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +import java.util.Currency + +@Serializable +internal data class CsvMonoItem( + @Serializable(MonobankInstantSerializer::class) + val date: Instant, + val description: String, + val mcc: Int, + val cardCurrencyAmount: Double, + val transactionCurrencyAmount: Double, + @Serializable(CurrencyAsStringSerializer::class) + val currency: Currency, + val exchangeRate: Double?, + val cardCurrencyCommissionAmount: Double?, + val cardCurrencyCashbackAmount: Double?, + val balance: Double?, +) { + fun toStatementItem(accountId: BankAccountId, accountCurrency: Currency) = + ImportStatementItem(this, accountId, accountCurrency) +} diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportAccountConfig.kt b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportAccountConfig.kt new file mode 100644 index 0000000..bad7258 --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportAccountConfig.kt @@ -0,0 +1,8 @@ +package io.github.smaugfm.monobudget.importer + +import io.github.smaugfm.monobudget.common.model.financial.BankAccountId + +data class ImportAccountConfig( + val accountAlias: String, + val transactionsFileContent: String +) diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportApplication.kt b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportApplication.kt new file mode 100644 index 0000000..c1dc37c --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportApplication.kt @@ -0,0 +1,53 @@ +package io.github.smaugfm.monobudget.importer + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.github.smaugfm.lunchmoney.model.LunchmoneyInsertTransaction +import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction +import io.github.smaugfm.monobudget.common.BaseApplication +import io.github.smaugfm.monobudget.common.model.settings.Settings +import io.github.smaugfm.monobudget.common.retry.InMemoryStatementRetryRepository +import io.github.smaugfm.monobudget.lunchmoney.LunchmoneyNewTransactionFactory +import io.github.smaugfm.monobudget.lunchmoney.LunchmoneyTransferCache +import io.github.smaugfm.monobudget.mono.MonoWebhookSettings +import io.github.smaugfm.monobudget.setupKoinModules +import kotlinx.coroutines.CoroutineScope +import kotlinx.datetime.Clock +import org.koin.core.context.startKoin +import org.koin.dsl.module +import java.net.URI +import java.nio.file.Paths +import kotlin.time.Duration.Companion.seconds + +private val log = KotlinLogging.logger {} + +class ImportApplication(source: ImportStatementSource) : + BaseApplication(listOf(source)) { + + companion object { + suspend fun main(coroutineScope: CoroutineScope) { + val settings = Settings.load( + Paths.get(System.getenv()["SETTINGS_FILE"] ?: "settings.yml") + ) + val importConfig = ImportConfig.load( + Paths.get(System.getenv()["IMPORT_CONFIG_FILE"] ?: "import-config.yml") + ) + val noteSuffix = " monobudget-import-${Clock.System.now()}" + log.info { "Inserting with note suffix: \"$noteSuffix\"" } + + startKoin { + setupKoinModules( + coroutineScope, + InMemoryStatementRetryRepository(), + settings, + MonoWebhookSettings(false, URI.create("none://none"), 0) + ) + modules(module { + single { LunchmoneyNewTransactionFactory(noteSuffix) } + single { LunchmoneyTransferCache(Long.MAX_VALUE.seconds) } + }) + }.koin + + ImportApplication(ImportStatementSource(importConfig.getImports())).run() + } + } +} diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportConfig.kt b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportConfig.kt new file mode 100644 index 0000000..5761580 --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportConfig.kt @@ -0,0 +1,30 @@ +package io.github.smaugfm.monobudget.importer + +import com.charleskorn.kaml.Yaml +import com.charleskorn.kaml.YamlConfiguration +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import java.io.File +import java.nio.file.Path + +private val log = KotlinLogging.logger {} + +@Serializable +data class ImportConfig( + private val imports: Map +) { + fun getImports() = + imports.entries.map { ImportAccountConfig(it.key, it.value) } + + companion object { + fun load(path: Path): ImportConfig = load(File(path.toString()).readText()) + + private fun load(content: String): ImportConfig = + Yaml(configuration = YamlConfiguration(strictMode = false)) + .decodeFromString(content) + .also { + log.debug { "Loaded import-config: $it" } + } + } +} diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportStatementItem.kt b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportStatementItem.kt new file mode 100644 index 0000000..a6a6556 --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportStatementItem.kt @@ -0,0 +1,28 @@ +package io.github.smaugfm.monobudget.importer + +import io.github.smaugfm.monobudget.common.model.financial.Amount +import io.github.smaugfm.monobudget.common.model.financial.BankAccountId +import io.github.smaugfm.monobudget.common.model.financial.StatementItem +import java.util.Currency +import java.util.UUID + +internal data class ImportStatementItem( + val csv: CsvMonoItem, + override val accountId: BankAccountId, + val accountCurrency: Currency +) : StatementItem { + override val id = UUID.randomUUID().toString() + override val time = csv.date + override val description = csv.description + override val comment = null + override val mcc = csv.mcc + override val amount = Amount.fromLunchmoneyAmount( + csv.cardCurrencyAmount, + accountCurrency + ) + override val operationAmount = Amount.fromLunchmoneyAmount( + csv.transactionCurrencyAmount, + csv.currency + ) + override val currency = csv.currency +} diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportStatementSource.kt b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportStatementSource.kt new file mode 100644 index 0000000..9a2f02d --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/importer/ImportStatementSource.kt @@ -0,0 +1,49 @@ +package io.github.smaugfm.monobudget.importer + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.github.smaugfm.monobudget.common.account.BankAccountService +import io.github.smaugfm.monobudget.common.model.financial.StatementItem +import io.github.smaugfm.monobudget.common.statement.StatementSource +import io.github.smaugfm.monobudget.common.statement.lifecycle.StatementProcessingContext +import io.github.smaugfm.monobudget.mono.MonoApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.serialization.csv.Csv +import kotlinx.serialization.decodeFromString +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.qualifier.StringQualifier +import java.io.File + +private val log = KotlinLogging.logger {} + +class ImportStatementSource(private val configs: List) : + StatementSource, KoinComponent { + + private lateinit var statementItems: List + + private val bankAccountsService: BankAccountService by inject() + private val csv = Csv { + hasHeaderRecord = false + nullString = "—" + } + + override suspend fun prepare() { + statementItems = configs.map { config -> + val accountId = getKoin().get(StringQualifier(config.accountAlias)) + .accountId + val accountCurrency = bankAccountsService.getAccountCurrency(accountId)!! + val csvItems = csv.decodeFromString>( + File(config.transactionsFileContent).readText() + .substringAfter("\n") + ) + + log.info { "Loaded ${csvItems.size} transactions for ${config.accountAlias}" } + csvItems.map { it.toStatementItem(config.accountAlias, accountCurrency) } + }.flatten().sortedBy { it.time } + } + + override suspend fun statements(): Flow { + return statementItems.map(::StatementProcessingContext).asFlow() + } +} diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/importer/MonobankInstantSerializer.kt b/src/main/kotlin/io/github/smaugfm/monobudget/importer/MonobankInstantSerializer.kt new file mode 100644 index 0000000..b559799 --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/importer/MonobankInstantSerializer.kt @@ -0,0 +1,42 @@ +package io.github.smaugfm.monobudget.importer + +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atDate +import kotlinx.datetime.format.char +import kotlinx.datetime.toInstant +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +class MonobankInstantSerializer : KSerializer { + + override val descriptor = + PrimitiveSerialDescriptor(this::class.qualifiedName!!, PrimitiveKind.STRING) + + private val dateFormat = LocalDate.Format { + dayOfMonth() + char('.') + monthNumber() + char('.') + year() + } + + override fun deserialize(decoder: Decoder): Instant { + val str = decoder.decodeString() + + val localdate = LocalDate.parse(str.substringBefore(" "), dateFormat) + val localtime = LocalTime.parse(str.substringAfter(" ")) + + return localtime.atDate(localdate).toInstant(TimeZone.currentSystemDefault()) + } + + override fun serialize(encoder: Encoder, value: Instant) { + TODO("Not yet implemented") + } +} + diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyNewTransactionFactory.kt b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyNewTransactionFactory.kt index fe461cf..c9ab861 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyNewTransactionFactory.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyNewTransactionFactory.kt @@ -13,7 +13,9 @@ import java.util.Currency private val log = KotlinLogging.logger {} @Single -class LunchmoneyNewTransactionFactory : NewTransactionFactory() { +class LunchmoneyNewTransactionFactory(val noteSuffix: String = "") : + NewTransactionFactory() { + override suspend fun create(statement: StatementItem): LunchmoneyInsertTransaction { log.debug { "Transforming Monobank statement to Lunchmoney transaction." } @@ -28,7 +30,7 @@ class LunchmoneyNewTransactionFactory : NewTransactionFactory() +class LunchmoneyTransferCache(expirationDuration: Duration = 1.minutes) : + TransferCache(expirationDuration) diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferCache.kt b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferCache.kt index 46bcdaa..b1b9003 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferCache.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferCache.kt @@ -3,6 +3,9 @@ package io.github.smaugfm.monobudget.ynab import io.github.smaugfm.monobudget.common.account.TransferCache import io.github.smaugfm.monobudget.ynab.model.YnabTransactionDetail import org.koin.core.annotation.Single +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes @Single -class YnabTransferCache : TransferCache() +class YnabTransferCache(expirationDuration: Duration = 1.minutes) : + TransferCache(expirationDuration) diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/common/misc/Playground.kt b/src/test/kotlin/io/github/smaugfm/monobudget/common/misc/Playground.kt index 58921cb..c699b98 100644 --- a/src/test/kotlin/io/github/smaugfm/monobudget/common/misc/Playground.kt +++ b/src/test/kotlin/io/github/smaugfm/monobudget/common/misc/Playground.kt @@ -3,21 +3,14 @@ package io.github.smaugfm.monobudget.common.misc import io.github.smaugfm.lunchmoney.api.LunchmoneyApi import io.github.smaugfm.monobudget.common.model.BudgetBackend import io.github.smaugfm.monobudget.common.model.settings.Settings -import io.github.smaugfm.monobudget.common.util.MCCRegistry import io.github.smaugfm.monobudget.common.util.misc.PeriodicFetcherFactory import io.github.smaugfm.monobudget.lunchmoney.LunchmoneyCategoryService +import io.github.smaugfm.monobudget.mono.MonoApi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.csv.Csv -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test import org.koin.core.context.startKoin import org.koin.dsl.bind import org.koin.dsl.module @@ -50,79 +43,4 @@ class Playground : KoinTest { } } } - - @Test - @Disabled - fun test() { - runBlocking { - val csv = - Csv { - hasHeaderRecord = true - } - - println() - println() - println() - - val output = - csv.decodeFromString>( - Paths.get("/Users/smaugfm/Downloads/report_26-03-2023_11-30-38.csv") - .toFile().readText(), - ).map { - CsvOutputItem( - it.time, - categorySuggestion.inferCategoryNameByMcc(it.mcc.toInt()) ?: "", - it.details, - if (it.currency == "UAH") { - it.amount + it.currency - } else { - it.operationAmount + it.currency - }, - "${it.mcc} " + MCCRegistry.map[it.mcc.toInt()]?.fullDescription, - ) - } - - Paths.get("/Users/smaugfm/Downloads/output.csv").toFile().writeText( - csv.encodeToString(output), - ) - } - } - - @Serializable - data class CsvOutputItem( - @SerialName("Дата i час операції") - val time: String, - @SerialName("Запропонована категорія") - val suggestedCategory: String, - @SerialName("Деталі операції") - val details: String, - @SerialName("Сума операції") - val amount: String, - @SerialName("MCC деталі") - val mcc: String, - ) - - @Serializable - data class CsvMonoItem( - @SerialName("Дата i час операції") - val time: String, - @SerialName("Деталі операції") - val details: String, - @SerialName("MCC") - val mcc: String, - @SerialName("Сума в валюті картки (UAH)") - val amount: String, - @SerialName("Сума в валюті операції") - val operationAmount: String, - @SerialName("Валюта") - val currency: String, - @SerialName("Курс") - val exchangeRate: String, - @SerialName("Сума комісій (UAH)") - val commissionAmount: String, - @SerialName("Сума кешбеку (UAH)") - val cashbackAmount: String, - @SerialName("Залишок після операції") - val balance: String, - ) }