diff --git a/build.gradle.kts b/build.gradle.kts index 65be43c..e5972e4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -138,8 +138,9 @@ tasks { } fun KotlinCompilationTask.optIn() { - compilerOptions.freeCompilerArgs.add( + compilerOptions.freeCompilerArgs.addAll( "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", ) } diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/Application.kt b/src/main/kotlin/io/github/smaugfm/monobudget/Application.kt index 8160c5b..8fd0425 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/Application.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/Application.kt @@ -10,7 +10,6 @@ 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 io.github.smaugfm.monobudget.common.verify.ApplicationStartupVerifier -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter @@ -23,7 +22,6 @@ import kotlin.system.exitProcess private val log = KotlinLogging.logger {} -@OptIn(ExperimentalCoroutinesApi::class) class Application : KoinComponent { private val telegramApi by inject() 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 new file mode 100644 index 0000000..453f4fc --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferCache.kt @@ -0,0 +1,9 @@ +package io.github.smaugfm.monobudget.common.account + +import io.github.smaugfm.monobudget.common.misc.ConcurrentExpiringMap +import io.github.smaugfm.monobudget.common.model.financial.StatementItem +import kotlinx.coroutines.Deferred +import kotlin.time.Duration.Companion.minutes + +abstract class TransferCache : + ConcurrentExpiringMap>(1.minutes) diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferBetweenAccountsDetector.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetector.kt similarity index 98% rename from src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferBetweenAccountsDetector.kt rename to src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetector.kt index ec3bb1d..04aafbd 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferBetweenAccountsDetector.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetector.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.Deferred private val log = KotlinLogging.logger {} -abstract class TransferBetweenAccountsDetector( +abstract class TransferDetector( private val bankAccounts: BankAccountService, private val ctx: StatementProcessingContext, private val cache: ConcurrentExpiringMap>, 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 db980b3..6c6b16c 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 @@ -2,7 +2,7 @@ package io.github.smaugfm.monobudget.common.lifecycle import io.github.oshai.kotlinlogging.KotlinLogging import io.github.smaugfm.monobudget.common.account.BankAccountService -import io.github.smaugfm.monobudget.common.account.TransferBetweenAccountsDetector +import io.github.smaugfm.monobudget.common.account.TransferDetector import io.github.smaugfm.monobudget.common.telegram.TelegramMessageSender import io.github.smaugfm.monobudget.common.transaction.TransactionFactory import io.github.smaugfm.monobudget.common.transaction.TransactionMessageFormatter @@ -14,7 +14,7 @@ abstract class StatementItemProcessor( private val ctx: StatementProcessingContext, private val transactionFactory: TransactionFactory, private val bankAccounts: BankAccountService, - private val transferDetector: TransferBetweenAccountsDetector, + private val transferDetector: TransferDetector, private val messageFormatter: TransactionMessageFormatter, private val telegramMessageSender: TelegramMessageSender, ) { 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 f295763..a9e47f2 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 @@ -7,7 +7,7 @@ data class StatementProcessingContext( private val map: MutableMap = mutableMapOf(), val attempt: Int = 0, ) { - suspend fun execIfNotSet( + suspend fun execIfFirst( key: String, block: suspend () -> Unit, ) { diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/telegram/TelegramMessageSender.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/telegram/TelegramMessageSender.kt index 9c15443..99463af 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/telegram/TelegramMessageSender.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/telegram/TelegramMessageSender.kt @@ -21,11 +21,11 @@ class TelegramMessageSender( ) { val chatId = bankAccounts.getTelegramChatIdByAccountId(accountId) if (chatId == null) { - log.error { "Failed to map Monobank account to telegram chat id. Account: $accountId" } + log.error { "Failed to map bank account id to telegram chat id. Account: $accountId" } return } - log.info { "Sending message to telegramChatId=$chatId. monoAccountId=$accountId)" } + log.info { "Sending message to telegramChatId=$chatId. bankAccountId=$accountId)" } telegramApi.sendMessage( chatId = ChatId.IntegerId(chatId), text = newMessage.message, diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/common/util/JsonUtils.kt b/src/main/kotlin/io/github/smaugfm/monobudget/common/util/JsonUtils.kt index 153db7d..55a03fe 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/common/util/JsonUtils.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/common/util/JsonUtils.kt @@ -1,13 +1,11 @@ package io.github.smaugfm.monobudget.common.util -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonBuilder import kotlinx.serialization.json.JsonNamingStrategy fun makeJson(convertSnakeCase: Boolean = false): Json = Json { buildJson(convertSnakeCase) } -@OptIn(ExperimentalSerializationApi::class) fun JsonBuilder.buildJson(convertSnakeCase: Boolean = false) { if (convertSnakeCase) { namingStrategy = JsonNamingStrategy.SnakeCase diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyCategoryService.kt b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyCategoryService.kt index 71fe3e3..e1d8b97 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyCategoryService.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyCategoryService.kt @@ -66,7 +66,7 @@ class LunchmoneyCategoryService( null, ).awaitSingle() - return budgets.firstOrNull { it.categoryId != null && it.categoryId == categoryId } + return budgets.firstOrNull { categoryId == it.categoryId } } private fun toCategoryBudget( diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyMonoTransferBetweenAccountsDetector.kt b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyMonoTransferBetweenAccountsDetector.kt deleted file mode 100644 index f494d04..0000000 --- a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyMonoTransferBetweenAccountsDetector.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.smaugfm.monobudget.lunchmoney - -import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction -import io.github.smaugfm.monobudget.common.account.BankAccountService -import io.github.smaugfm.monobudget.common.account.TransferBetweenAccountsDetector -import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext -import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingScopeComponent -import io.github.smaugfm.monobudget.common.misc.ConcurrentExpiringMap -import io.github.smaugfm.monobudget.common.model.financial.StatementItem -import kotlinx.coroutines.Deferred -import org.koin.core.annotation.Scope -import org.koin.core.annotation.Scoped -import kotlin.time.Duration.Companion.minutes - -@Scoped -@Scope(StatementProcessingScopeComponent::class) -class LunchmoneyMonoTransferBetweenAccountsDetector( - bankAccounts: BankAccountService, - ctx: StatementProcessingContext, -) : TransferBetweenAccountsDetector(bankAccounts, ctx, cache) { - companion object { - private val cache = - ConcurrentExpiringMap>(1.minutes) - } -} diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyStatementItemProcessor.kt b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyStatementItemProcessor.kt index e2ffba8..55f5b8b 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyStatementItemProcessor.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyStatementItemProcessor.kt @@ -3,7 +3,7 @@ package io.github.smaugfm.monobudget.lunchmoney import io.github.smaugfm.lunchmoney.model.LunchmoneyInsertTransaction import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction import io.github.smaugfm.monobudget.common.account.BankAccountService -import io.github.smaugfm.monobudget.common.account.TransferBetweenAccountsDetector +import io.github.smaugfm.monobudget.common.account.TransferDetector import io.github.smaugfm.monobudget.common.lifecycle.StatementItemProcessor import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingScopeComponent @@ -19,7 +19,7 @@ class LunchmoneyStatementItemProcessor( ctx: StatementProcessingContext, transactionFactory: TransactionFactory, bankAccounts: BankAccountService, - transferDetector: TransferBetweenAccountsDetector, + transferDetector: TransferDetector, messageFormatter: TransactionMessageFormatter, telegramMessageSender: TelegramMessageSender, ) : StatementItemProcessor( diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionCreator.kt b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionCreator.kt index a81cfca..acd4def 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionCreator.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransactionCreator.kt @@ -63,7 +63,7 @@ class LunchmoneyTransactionCreator( "Existing LunchmoneyTransaction: $existingTransaction" } - ctx.execIfNotSet("transactionUpdated") { + ctx.execIfFirst("transactionUpdated") { api.updateTransaction( transactionId = existingTransaction.id, transaction = diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransferCache.kt b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransferCache.kt new file mode 100644 index 0000000..1dfed23 --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransferCache.kt @@ -0,0 +1,8 @@ +package io.github.smaugfm.monobudget.lunchmoney + +import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction +import io.github.smaugfm.monobudget.common.account.TransferCache +import org.koin.core.annotation.Single + +@Single +class LunchmoneyTransferCache : TransferCache() diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransferDetector.kt b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransferDetector.kt new file mode 100644 index 0000000..0bc5593 --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/lunchmoney/LunchmoneyTransferDetector.kt @@ -0,0 +1,17 @@ +package io.github.smaugfm.monobudget.lunchmoney + +import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction +import io.github.smaugfm.monobudget.common.account.BankAccountService +import io.github.smaugfm.monobudget.common.account.TransferDetector +import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext +import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingScopeComponent +import org.koin.core.annotation.Scope +import org.koin.core.annotation.Scoped + +@Scoped +@Scope(StatementProcessingScopeComponent::class) +class LunchmoneyTransferDetector( + bankAccounts: BankAccountService, + ctx: StatementProcessingContext, + cache: LunchmoneyTransferCache, +) : TransferDetector(bankAccounts, ctx, cache) diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabStatementItemProcessor.kt b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabStatementItemProcessor.kt index 9aa2a77..197af09 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabStatementItemProcessor.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabStatementItemProcessor.kt @@ -1,7 +1,7 @@ package io.github.smaugfm.monobudget.ynab import io.github.smaugfm.monobudget.common.account.BankAccountService -import io.github.smaugfm.monobudget.common.account.TransferBetweenAccountsDetector +import io.github.smaugfm.monobudget.common.account.TransferDetector import io.github.smaugfm.monobudget.common.lifecycle.StatementItemProcessor import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingScopeComponent @@ -19,7 +19,7 @@ class YnabStatementItemProcessor( ctx: StatementProcessingContext, transactionFactory: TransactionFactory, bankAccounts: BankAccountService, - transferDetector: TransferBetweenAccountsDetector, + transferDetector: TransferDetector, messageFormatter: TransactionMessageFormatter, telegramMessageSender: TelegramMessageSender, ) : StatementItemProcessor( diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferCache.kt b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferCache.kt new file mode 100644 index 0000000..46bcdaa --- /dev/null +++ b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferCache.kt @@ -0,0 +1,8 @@ +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 + +@Single +class YnabTransferCache : TransferCache() diff --git a/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabMonoTransferBetweenAccountsDetector.kt b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferDetector.kt similarity index 51% rename from src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabMonoTransferBetweenAccountsDetector.kt rename to src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferDetector.kt index b980505..02a7f79 100644 --- a/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabMonoTransferBetweenAccountsDetector.kt +++ b/src/main/kotlin/io/github/smaugfm/monobudget/ynab/YnabTransferDetector.kt @@ -1,29 +1,21 @@ package io.github.smaugfm.monobudget.ynab import io.github.smaugfm.monobudget.common.account.BankAccountService -import io.github.smaugfm.monobudget.common.account.TransferBetweenAccountsDetector +import io.github.smaugfm.monobudget.common.account.TransferDetector import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingScopeComponent -import io.github.smaugfm.monobudget.common.misc.ConcurrentExpiringMap -import io.github.smaugfm.monobudget.common.model.financial.StatementItem import io.github.smaugfm.monobudget.ynab.model.YnabTransactionDetail -import kotlinx.coroutines.Deferred import org.koin.core.annotation.Scope import org.koin.core.annotation.Scoped -import kotlin.time.Duration.Companion.minutes @Scoped @Scope(StatementProcessingScopeComponent::class) -class YnabMonoTransferBetweenAccountsDetector( +class YnabTransferDetector( bankAccounts: BankAccountService, ctx: StatementProcessingContext, -) : TransferBetweenAccountsDetector( + cache: YnabTransferCache, +) : TransferDetector( bankAccounts, ctx, cache, - ) { - companion object { - private val cache = - ConcurrentExpiringMap>(1.minutes) - } -} + ) diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/common/mono/MonoTransferBetweenAccountsDetectorTest.kt b/src/test/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetectorTest.kt similarity index 89% rename from src/test/kotlin/io/github/smaugfm/monobudget/common/mono/MonoTransferBetweenAccountsDetectorTest.kt rename to src/test/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetectorTest.kt index def3169..98c5e39 100644 --- a/src/test/kotlin/io/github/smaugfm/monobudget/common/mono/MonoTransferBetweenAccountsDetectorTest.kt +++ b/src/test/kotlin/io/github/smaugfm/monobudget/common/account/TransferDetectorTest.kt @@ -1,13 +1,11 @@ -package io.github.smaugfm.monobudget.common.mono +package io.github.smaugfm.monobudget.common.account import assertk.assertThat import assertk.assertions.isInstanceOf import io.github.oshai.kotlinlogging.KotlinLogging import io.github.smaugfm.monobudget.TestBase -import io.github.smaugfm.monobudget.common.account.BankAccountService import io.github.smaugfm.monobudget.common.account.MaybeTransferStatement.NotTransfer import io.github.smaugfm.monobudget.common.account.MaybeTransferStatement.Transfer -import io.github.smaugfm.monobudget.common.account.TransferBetweenAccountsDetector import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingScopeComponent import io.github.smaugfm.monobudget.common.misc.ConcurrentExpiringMap @@ -29,7 +27,7 @@ import kotlin.time.Duration.Companion.minutes private val log = KotlinLogging.logger { } -class MonoTransferBetweenAccountsDetectorTest : TestBase() { +class TransferDetectorTest : TestBase() { companion object { private val cache = ConcurrentExpiringMap>(1.minutes) } @@ -37,11 +35,11 @@ class MonoTransferBetweenAccountsDetectorTest : TestBase() { class TestDetector( bankAccounts: BankAccountService, ctx: StatementProcessingContext, - ) : TransferBetweenAccountsDetector( - bankAccounts, - ctx, - cache, - ) + ) : TransferDetector( + bankAccounts, + ctx, + cache, + ) override fun KoinApplication.testKoinApplication() { modules( @@ -55,7 +53,7 @@ class MonoTransferBetweenAccountsDetectorTest : TestBase() { @Timeout(2, unit = TimeUnit.SECONDS) @Test - fun test() { + fun `Detects transfer`() { val ctx1 = StatementProcessingContext(statementItem1()) val ctx2 = StatementProcessingContext(statementItem2()) diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTest.kt b/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTest.kt index 82521ae..b831814 100644 --- a/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTest.kt +++ b/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTest.kt @@ -1,75 +1,28 @@ package io.github.smaugfm.monobudget.integration -import io.github.oshai.kotlinlogging.KotlinLogging -import io.github.smaugfm.lunchmoney.api.LunchmoneyApi -import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryMultiple +import com.elbekd.bot.model.ChatId import io.github.smaugfm.lunchmoney.model.LunchmoneyInsertTransaction import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction import io.github.smaugfm.monobank.model.MonoStatementItem import io.github.smaugfm.monobank.model.MonoWebhookResponseData -import io.github.smaugfm.monobudget.Application -import io.github.smaugfm.monobudget.TestBase import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext -import io.github.smaugfm.monobudget.common.misc.PeriodicFetcherFactory -import io.github.smaugfm.monobudget.common.model.settings.Settings -import io.github.smaugfm.monobudget.common.telegram.TelegramApi -import io.github.smaugfm.monobudget.common.verify.ApplicationStartupVerifier -import io.github.smaugfm.monobudget.common.verify.BudgetSettingsVerifier import io.github.smaugfm.monobudget.integration.TestData.UAH -import io.github.smaugfm.monobudget.mono.MonoWebhookListener -import io.github.smaugfm.monobudget.mono.MonoWebhookSettings import io.github.smaugfm.monobudget.mono.MonobankWebhookResponseStatementItem -import io.github.smaugfm.monobudget.setupKoinModules import io.mockk.InternalPlatformDsl.toStr -import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.confirmVerified import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.runs +import io.mockk.verifySequence import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.datetime.Clock import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.koin.core.KoinApplication -import org.koin.dsl.bind -import org.koin.dsl.module -import java.net.URI -import java.nio.file.Paths +import reactor.core.publisher.Mono +import java.math.BigDecimal import java.util.UUID -import kotlin.coroutines.cancellation.CancellationException - -private val log = KotlinLogging.logger {} - -class IntegrationTest : TestBase(), CoroutineScope { - - override val coroutineContext = Dispatchers.Default - - @MockK - lateinit var lunchmoneyMock: LunchmoneyApi - - @MockK - lateinit var tgMock: TelegramApi - - @MockK - lateinit var periodicFetcherFactory: PeriodicFetcherFactory - - @MockK - lateinit var webhookListener: MonoWebhookListener - private val webhookStatementsFlow = MutableSharedFlow() - - @MockK - lateinit var categoriesFetcherMock: - PeriodicFetcherFactory.PeriodicFetcher> - - @MockK - lateinit var budgetSettingsVerifier: BudgetSettingsVerifier +@Suppress("LongMethod") +class IntegrationTest : IntegrationTestBase(), CoroutineScope { @Test fun `When nothing happens finishes normally`() { runTestApplication { @@ -78,85 +31,233 @@ class IntegrationTest : TestBase(), CoroutineScope { } @Test - fun `Mono webhook triggers new transaction creation`() { + fun `Other account transaction triggers transfer`() { + val (newTransactionId, newTransactionId2) = setupTransferMocks { it.amount > BigDecimal.ZERO } + runTestApplication { webhookStatementsFlow.emit( StatementProcessingContext( MonobankWebhookResponseStatementItem( - d = MonoWebhookResponseData( - account = "MONO-EXAMPLE-UAH", - statementItem = MonoStatementItem( - id = UUID.randomUUID().toStr(), - time = Clock.System.now(), - description = "test", - mcc = 4829, - originalMcc = 4829, - hold = true, - amount = -5600, - operationAmount = -5600, - currencyCode = UAH, - commissionRate = 0, - cashbackAmount = 0, - balance = 0, - ) - ), accountCurrency = UAH - ) - ) + d = + MonoWebhookResponseData( + account = "MONO-EXAMPLE-UAH", + statementItem = + MonoStatementItem( + id = UUID.randomUUID().toStr(), + time = Clock.System.now(), + description = "Від: 777777****1234", + mcc = 4829, + originalMcc = 4829, + hold = true, + amount = 5600, + operationAmount = 5600, + currencyCode = UAH, + commissionRate = 0, + cashbackAmount = 0, + balance = 0, + ), + ), + accountCurrency = UAH, + ), + ), ) + coVerify(timeout = 1000, exactly = 2) { + tgMock.sendMessage( + match { + it is ChatId.IntegerId && it.id == 55555555L + }, + any(), + any(), + any(), + any(), + ) + } + verifySequence { + lunchmoneyMock.insertTransactions(any(), any(), any(), any(), any(), any()) + lunchmoneyMock.getSingleTransaction(eq(newTransactionId), any()) + lunchmoneyMock.updateTransaction(eq(newTransactionId), any(), any(), any(), any()) + lunchmoneyMock.insertTransactions(any(), any(), any(), any(), any(), any()) + lunchmoneyMock.getSingleTransaction(eq(newTransactionId2), any()) + lunchmoneyMock.createTransactionGroup( + any(), + "Transfer", + listOf(newTransactionId, newTransactionId2), + 444444L, + any(), + any(), + ) + } + confirmVerified(lunchmoneyMock) } } - override fun KoinApplication.testKoinApplication() { - coEvery { webhookListener.prepare() } just runs - coEvery { webhookListener.statements() } returns webhookStatementsFlow + @Test + fun `Mono transfer triggers single and transfer transaction creation`() { + val (newTransactionId, newTransactionId2) = setupTransferMocks { it.amount < BigDecimal.ZERO } - every { tgMock.start(any()) } answers { - this@IntegrationTest.launch {} - } - every { - periodicFetcherFactory.create>( - "Lunchmoney categories", - any(), + runTestApplication { + webhookStatementsFlow.emit( + StatementProcessingContext( + MonobankWebhookResponseStatementItem( + d = + MonoWebhookResponseData( + account = "MONO-EXAMPLE-UAH2", + statementItem = + MonoStatementItem( + id = UUID.randomUUID().toStr(), + time = Clock.System.now(), + description = "test send", + mcc = 4829, + originalMcc = 4829, + hold = true, + amount = -5600, + operationAmount = -5600, + currencyCode = UAH, + commissionRate = 0, + cashbackAmount = 0, + balance = 0, + ), + ), + accountCurrency = UAH, + ), + ), ) - } returns categoriesFetcherMock - coEvery { - categoriesFetcherMock.fetched() - } returns TestData.categories - coEvery { budgetSettingsVerifier.verify() } just runs - - setupKoinModules( - this@IntegrationTest, - Settings.load( - Paths.get( - IntegrationTest::class.java.classLoader.getResource("test-settings.yml")!!.path, + webhookStatementsFlow.emit( + StatementProcessingContext( + MonobankWebhookResponseStatementItem( + d = + MonoWebhookResponseData( + account = "MONO-EXAMPLE-UAH", + statementItem = + MonoStatementItem( + id = UUID.randomUUID().toStr(), + time = Clock.System.now(), + description = "test receive", + mcc = 4829, + originalMcc = 4829, + hold = true, + amount = 5600, + operationAmount = 5600, + currencyCode = UAH, + commissionRate = 0, + cashbackAmount = 0, + balance = 0, + ), + ), + accountCurrency = UAH, + ), ), - ), - MonoWebhookSettings(false, URI.create(""), 0), - ) - modules( - module { - single { lunchmoneyMock } - single { webhookListener } - single { tgMock } - single { budgetSettingsVerifier } bind ApplicationStartupVerifier::class - single { periodicFetcherFactory } - }, - ) + ) + coVerify(timeout = 1000, exactly = 1) { + tgMock.sendMessage( + match { + it is ChatId.IntegerId && it.id == 55555555L + }, + any(), + any(), + any(), + any(), + ) + } + coVerify(timeout = 1000, exactly = 1) { + tgMock.sendMessage( + match { + it is ChatId.IntegerId && it.id == 55555556L + }, + any(), + any(), + any(), + any(), + ) + } + verifySequence { + lunchmoneyMock.insertTransactions(any(), any(), any(), any(), any(), any()) + lunchmoneyMock.getSingleTransaction(eq(newTransactionId), any()) + lunchmoneyMock.updateTransaction(eq(newTransactionId), any(), any(), any(), any()) + lunchmoneyMock.insertTransactions(any(), any(), any(), any(), any(), any()) + lunchmoneyMock.getSingleTransaction(eq(newTransactionId2), any()) + lunchmoneyMock.createTransactionGroup( + any(), + "Transfer", + listOf(newTransactionId, newTransactionId2), + 444444L, + any(), + any(), + ) + } + confirmVerified(lunchmoneyMock) + } } - private fun runTestApplication(block: suspend () -> Unit) { - assertThrows { - runBlocking(coroutineContext) { - Application().also { - launch { - it.run() - } - block() - log.info { "Shutting down" } - coroutineContext.cancel() - } + @Test + fun `Mono webhook triggers new transaction creation`() { + var insertTransaction: LunchmoneyInsertTransaction? = null + val newTransactionId = 1L + every { lunchmoneyMock.insertTransactions(any(), any(), any(), any(), any(), any()) } answers { + insertTransaction = firstArg>()[0] + Mono.just(listOf(newTransactionId)) + } + every { lunchmoneyMock.getSingleTransaction(newTransactionId, any()) } answers { + Mono.just( + LunchmoneyTransaction( + id = newTransactionId, + isGroup = false, + date = insertTransaction!!.date, + payee = insertTransaction!!.payee!!, + amount = insertTransaction!!.amount, + currency = insertTransaction!!.currency!!, + toBase = 1.0, + notes = insertTransaction?.notes, + categoryId = insertTransaction?.categoryId, + status = insertTransaction!!.status!!, + ), + ) + } + + runTestApplication { + webhookStatementsFlow.emit( + StatementProcessingContext( + MonobankWebhookResponseStatementItem( + d = + MonoWebhookResponseData( + account = "MONO-EXAMPLE-UAH", + statementItem = + MonoStatementItem( + id = UUID.randomUUID().toStr(), + time = Clock.System.now(), + description = "test", + mcc = 4829, + originalMcc = 4829, + hold = true, + amount = -5600, + operationAmount = -5600, + currencyCode = UAH, + commissionRate = 0, + cashbackAmount = 0, + balance = 0, + ), + ), + accountCurrency = UAH, + ), + ), + ) + coVerify(timeout = 1000, exactly = 1) { + tgMock.sendMessage( + match { + it is ChatId.IntegerId && it.id == 55555555L + }, + any(), + any(), + any(), + any(), + ) + } + verifySequence { + lunchmoneyMock.insertTransactions(any(), any(), any(), any(), any(), any()) + lunchmoneyMock.getSingleTransaction(eq(newTransactionId), any()) } + confirmVerified(lunchmoneyMock) } } } diff --git a/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTestBase.kt b/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTestBase.kt new file mode 100644 index 0000000..94fe747 --- /dev/null +++ b/src/test/kotlin/io/github/smaugfm/monobudget/integration/IntegrationTestBase.kt @@ -0,0 +1,181 @@ +package io.github.smaugfm.monobudget.integration + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.github.smaugfm.lunchmoney.api.LunchmoneyApi +import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryMultiple +import io.github.smaugfm.lunchmoney.model.LunchmoneyInsertTransaction +import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction +import io.github.smaugfm.monobudget.Application +import io.github.smaugfm.monobudget.TestBase +import io.github.smaugfm.monobudget.common.lifecycle.StatementProcessingContext +import io.github.smaugfm.monobudget.common.misc.PeriodicFetcherFactory +import io.github.smaugfm.monobudget.common.model.settings.Settings +import io.github.smaugfm.monobudget.common.statement.StatementSource +import io.github.smaugfm.monobudget.common.telegram.TelegramApi +import io.github.smaugfm.monobudget.common.verify.ApplicationStartupVerifier +import io.github.smaugfm.monobudget.common.verify.BudgetSettingsVerifier +import io.github.smaugfm.monobudget.mono.MonoWebhookListener +import io.github.smaugfm.monobudget.mono.MonoWebhookSettings +import io.github.smaugfm.monobudget.setupKoinModules +import io.mockk.coEvery +import io.mockk.every +import io.mockk.excludeRecords +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.assertThrows +import org.koin.core.KoinApplication +import org.koin.dsl.bind +import org.koin.dsl.module +import reactor.core.publisher.Mono +import java.net.URI +import java.nio.file.Paths +import kotlin.coroutines.cancellation.CancellationException + +private val log = KotlinLogging.logger {} + +@Suppress("MagicNumber") +abstract class IntegrationTestBase : TestBase(), CoroutineScope { + override val coroutineContext = Dispatchers.Default + + @MockK + lateinit var lunchmoneyMock: LunchmoneyApi + + @MockK + lateinit var tgMock: TelegramApi + + @MockK + lateinit var periodicFetcherFactory: PeriodicFetcherFactory + + @MockK + lateinit var webhookListener: MonoWebhookListener + protected val webhookStatementsFlow = MutableSharedFlow(100) + + @MockK + lateinit var categoriesFetcherMock: + PeriodicFetcherFactory.PeriodicFetcher> + + @MockK + lateinit var budgetSettingsVerifier: BudgetSettingsVerifier + + override fun KoinApplication.testKoinApplication() { + coEvery { webhookListener.prepare() } just runs + coEvery { webhookListener.statements() } returns webhookStatementsFlow + + coEvery { tgMock.sendMessage(any(), any(), any(), any(), any()) } returns mockk() + every { tgMock.start(any()) } answers { + this@IntegrationTestBase.launch {} + } + every { lunchmoneyMock.getBudgetSummary(any(), any(), any()) } returns Mono.just(listOf()) + excludeRecords { lunchmoneyMock.getBudgetSummary(any(), any(), any()) } + every { + periodicFetcherFactory.create>( + "Lunchmoney categories", + any(), + ) + } returns categoriesFetcherMock + coEvery { + categoriesFetcherMock.fetched() + } returns TestData.categories + coEvery { budgetSettingsVerifier.verify() } just runs + + setupKoinModules( + this@IntegrationTestBase, + Settings.load( + Paths.get( + IntegrationTest::class.java.classLoader.getResource("test-settings.yml")!!.path, + ), + ), + MonoWebhookSettings(false, URI.create(""), 0), + ) + modules( + module { + single { lunchmoneyMock } + single { webhookListener } bind MonoWebhookListener::class bind StatementSource::class + single { tgMock } + single { budgetSettingsVerifier } bind ApplicationStartupVerifier::class + single { periodicFetcherFactory } + }, + ) + } + + protected fun setupTransferMocks(isFirst: (LunchmoneyInsertTransaction) -> Boolean): Pair { + var insertTransaction: LunchmoneyInsertTransaction? = null + var insertTransaction2: LunchmoneyInsertTransaction? = null + val newTransactionId = 1L + val newTransactionId2 = 2L + val trGroupId = 3L + every { lunchmoneyMock.insertTransactions(any(), any(), any(), any(), any(), any()) } answers { + val i = firstArg>()[0] + if (isFirst(i)) { + insertTransaction = i + Mono.just(listOf(newTransactionId)) + } else { + insertTransaction2 = i + Mono.just(listOf(newTransactionId2)) + } + } + every { lunchmoneyMock.getSingleTransaction(newTransactionId, any()) } answers { + Mono.just( + LunchmoneyTransaction( + id = newTransactionId, + isGroup = false, + date = insertTransaction!!.date, + payee = insertTransaction!!.payee!!, + amount = insertTransaction!!.amount, + currency = insertTransaction!!.currency!!, + toBase = 1.0, + notes = insertTransaction?.notes, + categoryId = insertTransaction?.categoryId, + status = insertTransaction!!.status!!, + ), + ) + } + every { lunchmoneyMock.getSingleTransaction(newTransactionId2, any()) } answers { + Mono.just( + LunchmoneyTransaction( + id = newTransactionId2, + isGroup = false, + date = insertTransaction2!!.date, + payee = insertTransaction2!!.payee!!, + amount = insertTransaction2!!.amount, + currency = insertTransaction2!!.currency!!, + toBase = 1.0, + notes = insertTransaction2?.notes, + categoryId = insertTransaction2?.categoryId, + status = insertTransaction2!!.status!!, + ), + ) + } + every { + lunchmoneyMock.updateTransaction(any(), any(), any(), any(), any()) + } returns Mono.just(mockk()) + every { + lunchmoneyMock.createTransactionGroup(any(), any(), any(), any(), any(), any()) + } returns Mono.just(trGroupId) + + return Pair(newTransactionId, newTransactionId2) + } + + protected fun runTestApplication(block: suspend () -> Unit) { + assertThrows { + runBlocking(coroutineContext) { + Application().also { + launch { + it.run() + } + block() + log.info { "Shutting down" } + coroutineContext.cancel() + } + } + } + } +} diff --git a/src/test/resources/test-settings.yml b/src/test/resources/test-settings.yml index 1dd6855..5991a5e 100644 --- a/src/test/resources/test-settings.yml +++ b/src/test/resources/test-settings.yml @@ -33,12 +33,12 @@ accounts: telegramChatId: 55555555 currency: UAH - ! - accountId: MONO-EXAMPLE-USD + accountId: MONO-EXAMPLE-UAH2 token: "" - alias: Example USD + alias: Example UAH budgetAccountId: 66068 telegramChatId: 55555556 - currency: USD + currency: UAH retry: interval: 1s mcc: