diff --git a/.gitignore b/.gitignore index 93ad197..2c0f73b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ out/ settings*.json settings*.yml +retries.json docker-compose.yml diff --git a/build.gradle.kts b/build.gradle.kts index f4f39c8..74fdee9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation("com.github.elbekD:kt-telegram-bot:2.2.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson") + 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") diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt b/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt index 3042049..1a18bca 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/Main.kt @@ -46,9 +46,10 @@ fun main() { val monoWebhookUrl = URI(env["MONO_WEBHOOK_URL"]!!) val webhookPort = env["WEBHOOK_PORT"]?.toInt() ?: DEFAULT_HTTP_PORT val settings = Settings.load(Paths.get(env["SETTINGS_FILE"] ?: "settings.yml")) - val jsonRetryRepository = JacksonFileStatementRetryRepository( - Paths.get(env["RETRIES_FILE"] ?: "retries.json") - ) + val jsonRetryRepository = + JacksonFileStatementRetryRepository( + Paths.get(env["RETRIES_FILE"] ?: "retries.json"), + ) val budgetBackend = settings.budgetBackend val webhookSettings = MonoWebhookSettings(setWebhook, monoWebhookUrl, webhookPort) diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/lifecycle/StatementProcessingContext.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/lifecycle/StatementProcessingContext.kt index def96bd..447d991 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/lifecycle/StatementProcessingContext.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/lifecycle/StatementProcessingContext.kt @@ -29,6 +29,5 @@ class StatementProcessingContext( return map.getOrPut(key) { lazyValue() as Any } as T } - fun incrementAttempt(): StatementProcessingContext = - StatementProcessingContext(item, map, attempt + 1) + fun incrementAttempt(): StatementProcessingContext = StatementProcessingContext(item, map, attempt + 1) } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/model/financial/StatementItem.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/model/financial/StatementItem.kt index 7595bbc..1026dee 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/model/financial/StatementItem.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/model/financial/StatementItem.kt @@ -1,8 +1,13 @@ package io.github.smaugfm.monobudget.common.model.financial +import com.fasterxml.jackson.annotation.JsonTypeInfo import kotlinx.datetime.Instant import java.util.Currency +@JsonTypeInfo( + use = JsonTypeInfo.Id.MINIMAL_CLASS, + include = JsonTypeInfo.As.PROPERTY, +) interface StatementItem { val id: String val accountId: BankAccountId 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 a29e0a6..de773b9 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,23 +1,31 @@ package io.github.smaugfm.monobudget.common.retry import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +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 java.nio.file.Path import java.util.UUID +import kotlin.io.path.exists import kotlin.io.path.readText import kotlin.io.path.writeText import kotlin.time.Duration class JacksonFileStatementRetryRepository( - private val path: Path + private val path: Path, ) : StatementRetryRepository { - - private val objectMapper = jacksonObjectMapper() + val objectMapper = + jsonMapper { + enable(SerializationFeature.INDENT_OUTPUT) + addModule(kotlinModule()) + addModule(JavaTimeModule()) + } override suspend fun addRetryRequest( ctx: StatementProcessingContext, - retryWaitDuration: Duration + retryWaitDuration: Duration, ) = StatementRetryRequest(UUID.randomUUID().toString(), ctx, retryWaitDuration) .also { save(getAllRequests() + it) @@ -28,9 +36,14 @@ class JacksonFileStatementRetryRepository( } override suspend fun getAllRequests(): List = - objectMapper.readValue( - path.readText(), object : TypeReference>() {} - ) + if (path.exists()) { + objectMapper.readValue( + path.readText(), + object : TypeReference>() {}, + ) + } else { + emptyList() + } private fun save(list: List) { path.writeText(objectMapper.writeValueAsString(list)) diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/TestBase.kt b/src/test/kotlin/io/github/smaugfm/monobudget/TestBase.kt index 366d112..f7b72ca 100644 --- a/src/test/kotlin/io/github/smaugfm/monobudget/TestBase.kt +++ b/src/test/kotlin/io/github/smaugfm/monobudget/TestBase.kt @@ -18,7 +18,7 @@ import java.util.Currency @ExtendWith(MockKExtension::class) @Suppress("MagicNumber") open class TestBase : KoinTest { - open fun KoinApplication.testKoinApplication() { + open fun testKoinApplication(app: KoinApplication) { } @Suppress("unused") @@ -26,7 +26,7 @@ open class TestBase : KoinTest { @RegisterExtension val koinTestExtension = KoinTestExtension.create { - testKoinApplication() + testKoinApplication(this@create) } @BeforeEach diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetectorTest.kt b/src/test/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetectorTest.kt index 98c5e39..f50e11f 100644 --- a/src/test/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetectorTest.kt +++ b/src/test/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetectorTest.kt @@ -41,8 +41,8 @@ class TransferDetectorTest : TestBase() { cache, ) - override fun KoinApplication.testKoinApplication() { - modules( + override fun testKoinApplication(app: KoinApplication) { + app.modules( module { scope { scoped { TestDetector(get(), get()) } diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTestBase.kt b/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTestBase.kt index 3470874..dbb5400 100644 --- a/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTestBase.kt +++ b/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTestBase.kt @@ -68,7 +68,7 @@ abstract class IntegrationTestBase : TestBase(), CoroutineScope { @MockK lateinit var budgetSettingsVerifier: BudgetSettingsVerifier - override fun KoinApplication.testKoinApplication() { + override fun testKoinApplication(app: KoinApplication) { coEvery { webhookListener.prepare() } just runs coEvery { webhookListener.statements() } returns webhookStatementsFlow @@ -89,7 +89,7 @@ abstract class IntegrationTestBase : TestBase(), CoroutineScope { } returns TestData.categories coEvery { budgetSettingsVerifier.verify() } just runs - setupKoinModules( + app.setupKoinModules( this@IntegrationTestBase, InMemoryStatementRetryRepository(), Settings.load( @@ -99,7 +99,7 @@ abstract class IntegrationTestBase : TestBase(), CoroutineScope { ), MonoWebhookSettings(false, URI.create(""), 0), ) - modules( + app.modules( module { single { lunchmoneyMock } single { webhookListener } bind MonoWebhookListener::class bind StatementSource::class @@ -175,7 +175,7 @@ abstract class IntegrationTestBase : TestBase(), CoroutineScope { } val updateTracker = FailTrackerTransformation( - fails.filterIsInstance() + fails.filterIsInstance(), ) every { lunchmoneyMock.updateTransaction(any(), any(), any(), any(), any()) @@ -184,7 +184,9 @@ abstract class IntegrationTestBase : TestBase(), CoroutineScope { .transformDeferred(updateTracker) } val createTransactionGroupTracker = - FailTrackerTransformation(fails.filterIsInstance()) + FailTrackerTransformation( + fails.filterIsInstance(), + ) every { lunchmoneyMock.createTransactionGroup(any(), any(), any(), any(), any(), any()) } answers { @@ -222,7 +224,9 @@ abstract class IntegrationTestBase : TestBase(), CoroutineScope { .transformDeferred(insertTracker) } val singleTracker = - FailTrackerTransformation(fails.filterIsInstance()) + FailTrackerTransformation( + fails.filterIsInstance(), + ) every { lunchmoneyMock.getSingleTransaction(newTransactionId, any()) } answers { Mono.just( LunchmoneyTransaction( diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/integration/JacksonRepositoryRetriesTest.kt b/src/test/kotlin/io/github/smaugfm/monobudget/integration/JacksonRepositoryRetriesTest.kt new file mode 100644 index 0000000..e46a2a5 --- /dev/null +++ b/src/test/kotlin/io/github/smaugfm/monobudget/integration/JacksonRepositoryRetriesTest.kt @@ -0,0 +1,28 @@ +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")) + } +} diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/integration/RetriesTest.kt b/src/test/kotlin/io/github/smaugfm/monobudget/integration/RetriesTest.kt index a46fe79..66d4076 100644 --- a/src/test/kotlin/io/github/smaugfm/monobudget/integration/RetriesTest.kt +++ b/src/test/kotlin/io/github/smaugfm/monobudget/integration/RetriesTest.kt @@ -15,9 +15,9 @@ import java.math.BigDecimal import java.util.UUID @Suppress("LongMethod") -class RetriesTest : IntegrationTestBase() { +open class RetriesTest : IntegrationTestBase() { @Test - fun `When lunchmoney fails and then recovers transfer transaction is processed correctly`() { + open fun `When lunchmoney fails and then recovers transfer transaction is processed correctly`() { val (newTransactionId, newTransactionId2) = setupTransferMocks( listOf( @@ -129,7 +129,7 @@ class RetriesTest : IntegrationTestBase() { } @Test - fun `When lunchmoney fails and then recovers successful method calls are not retried`() { + open fun `When lunchmoney fails and then recovers successful method calls are not retried`() { val newTransactionId = setupSingleTransactionMocks( listOf( diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionCreatorTest.kt b/src/test/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionCreatorTest.kt index 5319745..1a61e47 100644 --- a/src/test/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionCreatorTest.kt +++ b/src/test/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionCreatorTest.kt @@ -32,8 +32,8 @@ import java.time.LocalDate import java.util.Currency class LunchmoneyTransactionCreatorTest : TestBase() { - override fun KoinApplication.testKoinApplication() { - modules( + override fun testKoinApplication(app: KoinApplication) { + app.modules( module { single { InMemoryStatementRetryRepository() } bind StatementRetryRepository::class scope {