From deab262731224b20dc8e926ef66579c2f529ca85 Mon Sep 17 00:00:00 2001 From: Dmitry Marchuk <code@dmarchuk.com> Date: Mon, 27 Nov 2023 21:37:24 +0200 Subject: [PATCH] Fix Jackson serialization, MonoApis definitions, other scope issues #dockerpush #latest --- build.gradle.kts | 1 + .../io/github/smaugfm/monobudget/Main.kt | 2 +- .../lifecycle/StatementItemProcessor.kt | 4 +- .../JacksonFileStatementRetryRepository.kt | 36 ++++++++++++++- .../common/retry/StatementRetryRequest.kt | 5 ++- .../monobudget/common/telegram/TelegramApi.kt | 2 +- .../TransactionMessageFormatter.kt | 11 +++-- .../LunchmoneyTransactionMessageFormatter.kt | 13 +++--- .../github/smaugfm/monobudget/mono/MonoApi.kt | 4 +- .../ynab/YnabTransactionMessageFormatter.kt | 11 ++--- ...JacksonFileStatementRetryRepositoryTest.kt | 45 +++++++++++++++++++ .../JacksonRepositoryRetriesTest.kt | 28 ------------ 12 files changed, 106 insertions(+), 56 deletions(-) create mode 100644 src/test/kotlin/io/github/smaugfm/monobudget/common/retry/JacksonFileStatementRetryRepositoryTest.kt delete mode 100644 src/test/kotlin/io/github/smaugfm/monobudget/integration/JacksonRepositoryRetriesTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 74fdee9..2257b09 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -86,6 +86,7 @@ dependencies { } ktlint { + ignoreFailures.set(true) version.set("1.0.1") enableExperimentalRules.set(true) filter { diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt b/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt index 1a18bca..f1cd8e7 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt @@ -106,7 +106,7 @@ private fun runtimeModule( single { settings.retry } single { apiRetry() } settings.accounts.settings.filterIsInstance<MonoAccountSettings>() - .forEach { s -> single { MonoApi(s.token, s.accountId) } } + .forEach { s -> single(StringQualifier(s.alias)) { MonoApi(s.token, s.accountId, s.alias) } } settings.transfer.forEach { s -> single(qualifier = StringQualifier(s.descriptionRegex.pattern)) { s } } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/lifecycle/StatementItemProcessor.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/lifecycle/StatementItemProcessor.kt index 6c6b16c..3d08b37 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/lifecycle/StatementItemProcessor.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/lifecycle/StatementItemProcessor.kt @@ -28,7 +28,7 @@ abstract class StatementItemProcessor<TTransaction, TNewTransaction>( transferDetector.checkTransfer() val transaction = transactionFactory.create(maybeTransfer) - val message = messageFormatter.format(transaction) + val message = messageFormatter.format(ctx.item, transaction) telegramMessageSender.send(ctx.item.accountId, message) } @@ -42,7 +42,7 @@ abstract class StatementItemProcessor<TTransaction, TNewTransaction>( this.pp() } else { "\tAmount: ${amount}\n" + - "\tDescription: $description" + + "\tDescription: $description\n" + "\tMemo: $comment" } } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/retry/JacksonFileStatementRetryRepository.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/retry/JacksonFileStatementRetryRepository.kt index 22bfbb9..4100417 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/retry/JacksonFileStatementRetryRepository.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/retry/JacksonFileStatementRetryRepository.kt @@ -1,11 +1,19 @@ package io.github.smaugfm.monobudget.common.retry +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext +import kotlinx.datetime.Instant import java.nio.file.Path import java.nio.file.StandardOpenOption import java.util.UUID @@ -17,11 +25,17 @@ import kotlin.time.Duration class JacksonFileStatementRetryRepository( private val path: Path, ) : StatementRetryRepository { - val objectMapper = + internal val objectMapper = jsonMapper { enable(SerializationFeature.INDENT_OUTPUT) addModule(kotlinModule()) addModule(JavaTimeModule()) + addModule( + SimpleModule().also { + it.addSerializer(KotlinxTimeInstantJacksonSerializer()) + it.addDeserializer(Instant::class.java, KotlinxTimeInstantJacksonDeserializer()) + }, + ) } override suspend fun addRetryRequest( @@ -50,7 +64,25 @@ class JacksonFileStatementRetryRepository( path.writeText( objectMapper.writeValueAsString(list), Charsets.UTF_8, - StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.CREATE, ) } + + private class KotlinxTimeInstantJacksonSerializer : StdSerializer<Instant>(Instant::class.java) { + override fun serialize( + value: Instant?, + gen: JsonGenerator?, + provider: SerializerProvider?, + ) { + gen?.writeString(value?.toString()) + } + } + + private class KotlinxTimeInstantJacksonDeserializer : StdDeserializer<Instant>(Instant::class.java) { + override fun deserialize( + p: JsonParser?, + ctxt: DeserializationContext?, + ): Instant = Instant.parse(p?.valueAsString!!) + } } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/retry/StatementRetryRequest.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/retry/StatementRetryRequest.kt index 68c2098..c918476 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/retry/StatementRetryRequest.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/retry/StatementRetryRequest.kt @@ -1,5 +1,6 @@ package io.github.smaugfm.monobudget.common.retry +import com.fasterxml.jackson.annotation.JsonIgnore import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -16,5 +17,7 @@ data class StatementRetryRequest( Clock.System.now() + retryIn, ) - val retryIn get() = (retryAt - Clock.System.now()).coerceAtLeast(Duration.ZERO) + val retryIn + @JsonIgnore + get() = (retryAt - Clock.System.now()).coerceAtLeast(Duration.ZERO) } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/telegram/TelegramApi.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/telegram/TelegramApi.kt index 9df7c54..58096b2 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/telegram/TelegramApi.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/telegram/TelegramApi.kt @@ -46,7 +46,7 @@ class TelegramApi( allowSendingWithoutReply = null, replyMarkup = replyMarkup, ).also { - log.debug { "Sending message. \n\tTo: $chatId\n\ttext: $text\n\tkeyboard: ${replyMarkup?.pp()}" } + log.debug { "Sent message. \n\tTo: $chatId\n\ttext: $text\n\tkeyboard: ${replyMarkup?.pp()}" } } } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/transaction/TransactionMessageFormatter.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/transaction/TransactionMessageFormatter.kt index 9bb4e22..619a37a 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/transaction/TransactionMessageFormatter.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/transaction/TransactionMessageFormatter.kt @@ -17,14 +17,16 @@ import kotlin.math.roundToLong private val log = KotlinLogging.logger {} -abstract class TransactionMessageFormatter<TTransaction>( - protected val statementItem: StatementItem, -) : KoinComponent { +abstract class TransactionMessageFormatter<TTransaction> : KoinComponent { private val bankAccounts: BankAccountService by inject() - suspend fun format(transaction: TTransaction): MessageWithReplyKeyboard { + suspend fun format( + statementItem: StatementItem, + transaction: TTransaction, + ): MessageWithReplyKeyboard { val msg = formatHTMLStatementMessage( + statementItem, bankAccounts.getAccountCurrency(statementItem.accountId)!!, transaction, ) @@ -53,6 +55,7 @@ abstract class TransactionMessageFormatter<TTransaction>( protected abstract fun getReplyKeyboard(pressed: PressedButtons): InlineKeyboardMarkup protected abstract suspend fun formatHTMLStatementMessage( + statementItem: StatementItem, accountCurrency: Currency, transaction: TTransaction, ): String diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionMessageFormatter.kt b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionMessageFormatter.kt index b5caac9..e9e5b98 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionMessageFormatter.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionMessageFormatter.kt @@ -4,28 +4,24 @@ import com.elbekd.bot.types.InlineKeyboardMarkup import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction import io.github.smaugfm.lunchmoney.model.enumeration.LunchmoneyTransactionStatus import io.github.smaugfm.monobudget.common.category.CategoryService -import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext -import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingScopeComponent import io.github.smaugfm.monobudget.common.misc.MCC import io.github.smaugfm.monobudget.common.model.callback.ActionCallbackType import io.github.smaugfm.monobudget.common.model.callback.PressedButtons import io.github.smaugfm.monobudget.common.model.callback.TransactionUpdateType +import io.github.smaugfm.monobudget.common.model.financial.StatementItem import io.github.smaugfm.monobudget.common.transaction.TransactionMessageFormatter import io.github.smaugfm.monobudget.common.util.formatW import io.github.smaugfm.monobudget.common.util.replaceNewLines import io.github.smaugfm.monobudget.common.util.toLocalDateTime import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate -import org.koin.core.annotation.Scope -import org.koin.core.annotation.Scoped +import org.koin.core.annotation.Single import java.util.Currency -@Scoped -@Scope(StatementProcessingScopeComponent::class) +@Single class LunchmoneyTransactionMessageFormatter( private val categoryService: CategoryService, - private val ctx: StatementProcessingContext, -) : TransactionMessageFormatter<LunchmoneyTransaction>(ctx.item) { +) : TransactionMessageFormatter<LunchmoneyTransaction>() { private val shouldNotifyStatuses = setOf( LunchmoneyTransactionStatus.UNCLEARED, @@ -34,6 +30,7 @@ class LunchmoneyTransactionMessageFormatter( ) override suspend fun formatHTMLStatementMessage( + statementItem: StatementItem, accountCurrency: Currency, transaction: LunchmoneyTransaction, ): String { diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/mono/MonoApi.kt b/src/main/kotlin/io/github/smaugfm/monobudget/mono/MonoApi.kt index e8ecabf..917fa1f 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/mono/MonoApi.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/mono/MonoApi.kt @@ -17,7 +17,7 @@ import java.net.URI private val log = KotlinLogging.logger {} -class MonoApi(token: String, val accountId: BankAccountId) { +class MonoApi(token: String, val accountId: BankAccountId, private val alias: String) { init { require(token.isNotBlank()) } @@ -37,7 +37,7 @@ class MonoApi(token: String, val accountId: BankAccountId) { get(url.path) { call.response.status(HttpStatusCode.OK) call.respondText("OK\n", ContentType.Text.Plain) - log.info { "Webhook setup successful: $url" } + log.info { "Webhook setup for $alias successful: $url" } waitForWebhook.complete(Unit) } } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransactionMessageFormatter.kt b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransactionMessageFormatter.kt index a527b41..7819a16 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransactionMessageFormatter.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransactionMessageFormatter.kt @@ -2,7 +2,6 @@ package io.github.smaugfm.monobudget.ynab import com.elbekd.bot.types.InlineKeyboardMarkup import io.github.smaugfm.monobudget.common.category.CategoryService -import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingScopeComponent import io.github.smaugfm.monobudget.common.misc.MCC import io.github.smaugfm.monobudget.common.model.callback.PressedButtons import io.github.smaugfm.monobudget.common.model.callback.TransactionUpdateType @@ -11,17 +10,15 @@ import io.github.smaugfm.monobudget.common.transaction.TransactionMessageFormatt import io.github.smaugfm.monobudget.common.util.replaceNewLines import io.github.smaugfm.monobudget.ynab.model.YnabCleared import io.github.smaugfm.monobudget.ynab.model.YnabTransactionDetail -import org.koin.core.annotation.Scope -import org.koin.core.annotation.Scoped +import org.koin.core.annotation.Single import java.util.Currency -@Scoped -@Scope(StatementProcessingScopeComponent::class) +@Single class YnabTransactionMessageFormatter( private val categoryService: CategoryService, - statementItem: StatementItem, -) : TransactionMessageFormatter<YnabTransactionDetail>(statementItem) { +) : TransactionMessageFormatter<YnabTransactionDetail>() { override suspend fun formatHTMLStatementMessage( + statementItem: StatementItem, accountCurrency: Currency, transaction: YnabTransactionDetail, ): String { diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/common/retry/JacksonFileStatementRetryRepositoryTest.kt b/src/test/kotlin/io/github/smaugfm/monobudget/common/retry/JacksonFileStatementRetryRepositoryTest.kt new file mode 100644 index 0000000..b383861 --- /dev/null +++ b/src/test/kotlin/io/github/smaugfm/monobudget/common/retry/JacksonFileStatementRetryRepositoryTest.kt @@ -0,0 +1,45 @@ +package io.github.smaugfm.monobudget.common.retry + +import assertk.assertThat +import assertk.assertions.isEmpty +import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext +import io.github.smaugfm.monobudget.integration.RetriesTest +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.koin.core.KoinApplication +import org.koin.dsl.bind +import org.koin.dsl.module +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.time.Duration + +class JacksonFileStatementRetryRepositoryTest : RetriesTest() { + private val repo = JacksonFileStatementRetryRepository(Paths.get("retries.json")) + + override fun testKoinApplication(app: KoinApplication) { + super.testKoinApplication(app) + app.modules( + module { + single { repo } bind StatementRetryRepository::class + }, + ) + } + + @Test + fun `Mono statement item serializes & deserializes correctly`() { + val repo = JacksonFileStatementRetryRepository(Paths.get("retries.json")) + runBlocking { + val req1 = repo.addRetryRequest(StatementProcessingContext(statementItem1()), Duration.ZERO) + val req2 = repo.addRetryRequest(StatementProcessingContext(statementItem1()), Duration.ZERO) + repo.removeRetryRequest(req1.id) + repo.removeRetryRequest(req2.id) + assertThat(repo.getAllRequests()).isEmpty() + } + } + + @BeforeEach + fun deleteFile() { + Files.deleteIfExists(Paths.get("retries.json")) + } +} diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/integration/JacksonRepositoryRetriesTest.kt b/src/test/kotlin/io/github/smaugfm/monobudget/integration/JacksonRepositoryRetriesTest.kt deleted file mode 100644 index e46a2a5..0000000 --- a/src/test/kotlin/io/github/smaugfm/monobudget/integration/JacksonRepositoryRetriesTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.smaugfm.monobudget.integration - -import io.github.smaugfm.monobudget.common.retry.JacksonFileStatementRetryRepository -import io.github.smaugfm.monobudget.common.retry.StatementRetryRepository -import org.junit.jupiter.api.BeforeEach -import org.koin.core.KoinApplication -import org.koin.dsl.bind -import org.koin.dsl.module -import java.nio.file.Files -import java.nio.file.Paths - -class JacksonRepositoryRetriesTest : RetriesTest() { - private val repo = JacksonFileStatementRetryRepository(Paths.get("retries.json")) - - override fun testKoinApplication(app: KoinApplication) { - super.testKoinApplication(app) - app.modules( - module { - single { repo } bind StatementRetryRepository::class - }, - ) - } - - @BeforeEach - fun deleteFile() { - Files.deleteIfExists(Paths.get("retries.json")) - } -}