Skip to content

Commit

Permalink
Integration test wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Dmytro Marchuk authored and smaugfm committed Nov 23, 2023
1 parent 4ab9f27 commit 5ec5b4d
Show file tree
Hide file tree
Showing 122 changed files with 1,228 additions and 892 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
indent_size = 4
insert_final_newline = true
max_line_length = 110
ktlint_standard_no-wildcard-imports = disabled
15 changes: 8 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask
import org.codehaus.plexus.util.Os
import org.jetbrains.kotlin.gradle.dsl.KotlinCommonCompilerOptions
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
import org.jlleitschuh.gradle.ktlint.KtlintExtension
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType

plugins {
Expand All @@ -27,6 +26,7 @@ val koin = "3.5.0"
val koinKsp = "1.3.0"
val resilience4jVersion = "1.7.0"
val kotlinxCoroutines = "1.7.3"
val sealedEnum = "0.7.0"

val githubToken: String? by project

Expand All @@ -43,6 +43,8 @@ dependencies {
implementation("io.github.smaugfm:lunchmoney:1.0.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutines")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutines")
implementation("com.github.livefront.sealed-enum:runtime:$sealedEnum")
ksp("com.github.livefront.sealed-enum:ksp:$sealedEnum")
implementation("com.uchuhimo:kotlinx-bimap:1.2")
implementation("com.github.elbekD:kt-telegram-bot:2.2.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
Expand All @@ -68,8 +70,10 @@ dependencies {
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
implementation("io.ktor:ktor-server-content-negotiation:$ktor")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
testImplementation("org.junit.jupiter:junit-jupiter:$junit")
testImplementation("org.junit.jupiter:junit-jupiter-api:$junit")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junit")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("ch.qos.logback:logback-core:$logback")
implementation("ch.qos.logback:logback-classic:$logback")

Expand All @@ -78,11 +82,8 @@ dependencies {
}
}

configurations.all {
resolutionStrategy.cacheChangingModulesFor(1, TimeUnit.SECONDS)
}

configure<KtlintExtension> {
ktlint {
version.set("1.0.1")
enableExperimentalRules.set(true)
filter {
exclude("**/generated/**")
Expand Down Expand Up @@ -138,7 +139,7 @@ tasks {

fun <T : KotlinCommonCompilerOptions> KotlinCompilationTask<T>.optIn() {
compilerOptions.freeCompilerArgs.add(
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}

Expand Down
3 changes: 0 additions & 3 deletions gradle.properties

This file was deleted.

12 changes: 6 additions & 6 deletions src/main/kotlin/io/github/smaugfm/monobudget/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@ private val log = KotlinLogging.logger {}
class Application<TTransaction, TNewTransaction> :
KoinComponent {
private val telegramApi by inject<TelegramApi>()
private val statementServices by injectAll<StatementSource>()
private val statementSources by injectAll<StatementSource>()
private val startupVerifiers by injectAll<ApplicationStartupVerifier>()
private val telegramCallbackHandler by inject<TelegramCallbackHandler<TTransaction>>()
private val statementEvents by inject<StatementProcessingEventDelivery>()

suspend fun run() {
runStartupChecks()

statementServices.forEach { it.prepare() }
statementSources.forEach { it.prepare() }

val telegramJob = telegramApi.start(telegramCallbackHandler::handle)
statementServices.asFlow()
telegramApi.start(telegramCallbackHandler::handle)
log.info { "Started application" }

statementSources.asFlow()
.flatMapMerge { it.statements() }
.filter(statementEvents::onNewStatement)
.map(::StatementProcessingScopeComponent)
Expand All @@ -58,8 +60,6 @@ class Application<TTransaction, TNewTransaction> :
}
}
.collect()
log.info { "Started application" }
telegramJob.join()
}

private suspend fun runStartupChecks() {
Expand Down
67 changes: 29 additions & 38 deletions src/main/kotlin/io/github/smaugfm/monobudget/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import io.github.smaugfm.monobudget.common.model.BudgetBackend
import io.github.smaugfm.monobudget.common.model.BudgetBackend.Lunchmoney
import io.github.smaugfm.monobudget.common.model.BudgetBackend.YNAB
import io.github.smaugfm.monobudget.common.model.settings.MonoAccountSettings
import io.github.smaugfm.monobudget.common.model.settings.OtherAccountSettings
import io.github.smaugfm.monobudget.common.model.settings.Settings
import io.github.smaugfm.monobudget.lunchmoney.LunchmoneyModule
import io.github.smaugfm.monobudget.mono.MonoApi
Expand Down Expand Up @@ -66,7 +65,7 @@ fun main() {
fun KoinApplication.setupKoinModules(
coroutineScope: CoroutineScope,
settings: Settings,
webhookSettings: MonoWebhookSettings
webhookSettings: MonoWebhookSettings,
) {
printLogger(Level.ERROR)
modules(runtimeModule(coroutineScope, settings, webhookSettings))
Expand All @@ -76,18 +75,17 @@ fun KoinApplication.setupKoinModules(
when (settings.budgetBackend) {
is Lunchmoney -> lunchmoneyModule(settings.budgetBackend)
is YNAB -> ynabModule()
}
},
)
}

private fun runtimeModule(
coroutineScope: CoroutineScope,
settings: Settings,
webhookSettings: MonoWebhookSettings
webhookSettings: MonoWebhookSettings,
) = module {
when (settings.budgetBackend) {
is Lunchmoney ->
single { settings.budgetBackend }
is Lunchmoney -> single { settings.budgetBackend }

is YNAB -> single { settings.budgetBackend }
} bind BudgetBackend::class
Expand All @@ -96,47 +94,40 @@ private fun runtimeModule(
single { settings.mcc }
single { settings.bot }
single { settings.accounts }
single { settings.retry }
single { apiRetry() }
settings.accounts.settings
.forEach { accountSettings ->
single {
when (accountSettings) {
is MonoAccountSettings ->
MonoApi(accountSettings.token, accountSettings.accountId)

is OtherAccountSettings ->
accountSettings
}
}
}
settings.accounts.settings.filterIsInstance<MonoAccountSettings>()
.forEach { s -> single { MonoApi(s.token, s.accountId) } }
settings.transfer.forEach { s ->
single(qualifier = StringQualifier(s.descriptionRegex.pattern)) { s }
}
single { coroutineScope }
}

@Suppress("MagicNumber")
private fun apiRetry(): Retry = Retry.of(
"apiRetry",
RetryConfig {
maxAttempts(3)
failAfterMaxAttempts(true)
intervalFunction(
IntervalFunction.ofExponentialBackoff(
Duration.ofSeconds(1),
2.0
private fun apiRetry(): Retry =
Retry.of(
"apiRetry",
RetryConfig {
maxAttempts(3)
failAfterMaxAttempts(true)
intervalFunction(
IntervalFunction.ofExponentialBackoff(
Duration.ofSeconds(1),
2.0,
),
)
)
}
)
},
)

private fun lunchmoneyModule(budgetBackend: Lunchmoney) = module {
single {
LunchmoneyApi(
budgetBackend.token,
requestTransformer = RetryOperator.of(get())
)
}
} + LunchmoneyModule().module
private fun lunchmoneyModule(budgetBackend: Lunchmoney) =
module {
single {
LunchmoneyApi(
budgetBackend.token,
requestTransformer = RetryOperator.of(get()),
)
}
} + LunchmoneyModule().module

private fun ynabModule() = listOf(YnabModule().module)
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import java.util.Currency

abstract class BankAccountService {
abstract suspend fun getAccounts(): List<Account>

abstract fun getTelegramChatIdByAccountId(accountId: BankAccountId): Long?

abstract fun getBudgetAccountId(accountId: BankAccountId): String?

suspend fun getAccountAlias(accountId: BankAccountId): String? =
getAccounts().find { it.id == accountId }?.alias

suspend fun getAccountCurrency(accountId: BankAccountId): Currency? =
getAccounts().find { it.id == accountId }?.currency
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import kotlinx.coroutines.CompletableDeferred

sealed class MaybeTransferStatement<TTransaction> {
abstract val statement: StatementItem

data class Transfer<TTransaction>(
override val statement: StatementItem,
private val processed: TTransaction
private val processed: TTransaction,
) : MaybeTransferStatement<TTransaction>() {
@Suppress("UNCHECKED_CAST")
fun <T : Any> processed(): T {
Expand All @@ -17,7 +18,7 @@ sealed class MaybeTransferStatement<TTransaction> {

class NotTransfer<TTransaction>(
override val statement: StatementItem,
private val processedDeferred: CompletableDeferred<TTransaction>
private val processedDeferred: CompletableDeferred<TTransaction>,
) : MaybeTransferStatement<TTransaction>() {
@Volatile
private var ran = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ private val log = KotlinLogging.logger {}
abstract class TransferBetweenAccountsDetector<TTransaction>(
private val bankAccounts: BankAccountService,
private val ctx: StatementProcessingContext,
private val cache: ConcurrentExpiringMap<StatementItem, Deferred<TTransaction>>
private val cache: ConcurrentExpiringMap<StatementItem, Deferred<TTransaction>>,
) {
suspend fun checkTransfer(): MaybeTransferStatement<TTransaction> =
ctx.getOrPut("transfer") {
val existingTransfer = cache.entries.firstOrNull { (recentStatementItem) ->
checkIsTransferTransactions(recentStatementItem)
}?.value?.await()
val existingTransfer =
cache.entries.firstOrNull { (recentStatementItem) ->
checkIsTransferTransactions(recentStatementItem)
}?.value?.await()

if (existingTransfer != null) {
log.debug {
Expand Down Expand Up @@ -53,15 +54,21 @@ abstract class TransferBetweenAccountsDetector<TTransaction>(
}
}

private suspend fun currencyMatch(new: StatementItem, existing: StatementItem): Boolean {
private suspend fun currencyMatch(
new: StatementItem,
existing: StatementItem,
): Boolean {
val newTransactionAccountCurrency = bankAccounts.getAccountCurrency(new.accountId)
val existingTransactionAccountCurrency = bankAccounts.getAccountCurrency(existing.accountId)
return new.currency == existing.currency ||
newTransactionAccountCurrency == existing.currency ||
existingTransactionAccountCurrency == new.currency
}

private fun amountMatch(new: StatementItem, existing: StatementItem): Boolean {
private fun amountMatch(
new: StatementItem,
existing: StatementItem,
): Boolean {
val a1 = new.amount
val a2 = existing.amount
val oa1 = new.operationAmount
Expand All @@ -74,5 +81,8 @@ abstract class TransferBetweenAccountsDetector<TTransaction>(
return a1.equalsInverted(oa2) || oa1.equalsInverted(a2)
}

private fun mccMatch(new: StatementItem, existing: StatementItem) = new.mcc == existing.mcc
private fun mccMatch(
new: StatementItem,
existing: StatementItem,
) = new.mcc == existing.mcc
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ abstract class CategoryService : KoinComponent {

data class BudgetedCategory(
val categoryName: String,
val budget: CategoryBudget?
val budget: CategoryBudget?,
) {
data class CategoryBudget(
val left: Amount,
val budgetedThisMonth: Amount
val budgetedThisMonth: Amount,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package io.github.smaugfm.monobudget.common.exception

class BudgetBackendError(
cause: Throwable,
val userMessage: String
val userMessage: String,
) : Exception(cause)
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ abstract class StatementItemProcessor<TTransaction, TNewTransaction>(
private val bankAccounts: BankAccountService,
private val transferDetector: TransferBetweenAccountsDetector<TTransaction>,
private val messageFormatter: TransactionMessageFormatter<TTransaction>,
private val telegramMessageSender: TelegramMessageSender
private val telegramMessageSender: TelegramMessageSender,
) {

suspend fun process() {
logStatement()
processStatement()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ data class StatementProcessingContext(
private val map: MutableMap<String, Any> = mutableMapOf(),
val attempt: Int = 0,
) {
suspend fun execIfNotSet(key: String, block: suspend () -> Unit) {
suspend fun execIfNotSet(
key: String,
block: suspend () -> Unit,
) {
val flag = map[key] as Boolean?
if (flag == null || !flag) {
block().also {
Expand All @@ -16,11 +19,13 @@ data class StatementProcessingContext(
}
}

suspend fun <T> getOrPut(key: String, lazyValue: suspend () -> T): T {
suspend fun <T> getOrPut(
key: String,
lazyValue: suspend () -> T,
): T {
@Suppress("UNCHECKED_CAST")
return map.getOrPut(key) { lazyValue() as Any } as T
}

fun retryCopy(): StatementProcessingContext =
StatementProcessingContext(item, map, attempt + 1)
fun retryCopy(): StatementProcessingContext = StatementProcessingContext(item, map, attempt + 1)
}
Loading

0 comments on commit 5ec5b4d

Please sign in to comment.