diff --git a/build.gradle.kts b/build.gradle.kts
index 1ad0bfb..0071c6c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,7 +10,7 @@ plugins {
kotlin("jvm") version "2.0.0"
kotlin("plugin.serialization") version "2.0.0"
id("org.jlleitschuh.gradle.ktlint") version "11.2.0"
- id("io.gitlab.arturbosch.detekt") version "1.22.0"
+ id("io.gitlab.arturbosch.detekt") version "1.23.6"
id("org.jetbrains.dokka") version "1.9.20"
id("com.github.breadmoirai.github-release") version "2.4.1"
signing
@@ -18,18 +18,18 @@ plugins {
}
group = "io.github.smaugfm"
-version = "1.0.2"
+version = "1.0.3-SNAPSHOT"
val isReleaseVersion = !version.toString().endsWith("SNAPSHOT")
repositories {
mavenCentral()
}
-val reactor= "3.6.8"
+val reactor = "3.6.8"
val reactorNetty = "1.1.21"
val mockserver = "5.15.0"
val logback = "1.5.6"
-val javaVersion = "11"
+val javaVersion = "17"
val resilience4jVersion = "1.7.0"
dependencies {
diff --git a/detekt-baseline.xml b/detekt-baseline.xml
index c6c17bb..1733478 100644
--- a/detekt-baseline.xml
+++ b/detekt-baseline.xml
@@ -14,4 +14,21 @@
TooManyFunctions:RequestExecutor.kt$RequestExecutor
UtilityClassWithPublicConstructor:TestBase.kt$TestBase
+
+ LongParameterList:LunchmoneyApi.kt$LunchmoneyApi$( assetId: Long, typeName: LunchmoneyAssetType? = null, subtypeName: String? = null, name: String? = null, displayName: String? = null, balance: BigDecimal? = null, balanceAsOf: Instant? = null, currency: Currency? = null, institutionName: String? = null, closedOn: LocalDate? = null, excludeTransactions: Boolean? = null )
+ LongParameterList:LunchmoneyApi.kt$LunchmoneyApi$( categoryId: Long, isIncome: Boolean, excludeFromBudget: Boolean, excludeFromTotals: Boolean, name: String? = null, description: String? = null, groupId: Long? = null )
+ LongParameterList:LunchmoneyApi.kt$LunchmoneyApi$( cryptoAssetId: Long, name: String? = null, displayName: String? = null, institutionName: String? = null, currency: String? = null, balance: BigDecimal? = null )
+ LongParameterList:LunchmoneyApi.kt$LunchmoneyApi$( date: LocalDate, payee: String, transactions: List<Long>, categoryId: Long? = null, notes: String? = null, tags: List<LunchmoneyTransactionTag>? = null )
+ LongParameterList:LunchmoneyApi.kt$LunchmoneyApi$( name: String, description: String? = null, isIncome: Boolean? = null, excludeFromBudget: Boolean? = null, excludeFromTotals: Boolean? = null, categoryIds: List<Long>? = null, newCategories: List<String>? = null )
+ LongParameterList:LunchmoneyApi.kt$LunchmoneyApi$( name: String, isIncome: Boolean, excludeFromBudget: Boolean, excludeFromTotals: Boolean, description: String? = null, groupId: Long? = null )
+ LongParameterList:LunchmoneyApi.kt$LunchmoneyApi$( name: String, typeName: LunchmoneyAssetType, balance: BigDecimal, subtypeName: String? = null, displayName: String? = null, balanceAsOf: Instant? = null, currency: Currency? = null, institutionName: String? = null, closedOn: LocalDate? = null, excludeTransactions: Boolean? = null )
+ LongParameterList:LunchmoneyApi.kt$LunchmoneyApi$( tagId: Long? = null, recurringId: Long? = null, plaidAccountId: Long? = null, categoryId: Long? = null, assetId: Long? = null, isGroup: Boolean? = null, status: LunchmoneyTransactionStatus? = null, startDate: LocalDate? = null, endDate: LocalDate? = null, debitAsNegative: Boolean? = null, pending: Boolean? = null, offset: Long? = null, limit: Long? = null, groupId: Long? = null )
+ LongParameterList:LunchmoneyApi.kt$LunchmoneyApi$( transactions: List<LunchmoneyInsertTransaction>, applyRules: Boolean? = null, skipDuplicates: Boolean? = null, checkForRecurring: Boolean? = null, debitAsNegative: Boolean? = null, skipBalanceUpdate: Boolean? = null )
+ MaxLineLength:GetSingleTransactionRequestTest.kt$GetSingleTransactionRequestTest$plaidMetadata = "{\"account_id\":\"fMKfypkyRXSXvpJor4vPTg6OP7wD4afmEjv6N\",\"account_owner\":\"1005\",\"amount\":-14.18,\"authorized_date\":\"2023-11-28\",\"authorized_datetime\":null,\"category\":[\"Shops\",\"Supermarkets and Groceries\"],\"category_id\":\"19047000\",\"check_number\":null,\"counterparties\":[{\"confidence_level\":\"VERY_HIGH\",\"entity_id\":\"O5W5j4dN9OR3E6ypQmjdkWZZRoXEzVMz2ByWM\",\"logo_url\":\"https://plaid-merchant-logos.plaid.com/walmart_1100.png\",\"name\":\"Walmart\",\"type\":\"merchant\",\"website\":\"walmart.com\"}],\"date\":\"2023-11-29\",\"datetime\":null,\"iso_currency_code\":\"USD\",\"location\":{\"address\":null,\"city\":null,\"country\":null,\"lat\":null,\"lon\":null,\"postal_code\":null,\"region\":null,\"store_number\":null},\"logo_url\":\"https://plaid-merchant-logos.plaid.com/walmart_1100.png\",\"merchant_entity_id\":\"O5W5j4dN9OR3E6ypQmjdkWZZRoXEzVMz2ByWM\",\"merchant_name\":\"Walmart\",\"name\":\"Walmart\",\"payment_channel\":\"other\",\"payment_meta\":{\"by_order_of\":null,\"payee\":null,\"payer\":null,\"payment_method\":null,\"payment_processor\":null,\"ppd_id\":null,\"reason\":null,\"reference_number\":\"320233330735688096\"},\"pending\":false,\"pending_transaction_id\":null,\"personal_finance_category\":{\"confidence_level\":\"VERY_HIGH\",\"detailed\":\"GENERAL_MERCHANDISE_SUPERSTORES\",\"primary\":\"GENERAL_MERCHANDISE\"},\"personal_finance_category_icon_url\":\"https://plaid-category-icons.plaid.com/PFC_GENERAL_MERCHANDISE.png\",\"transaction_code\":null,\"transaction_id\":\"rmQdnefvAndbfHN5mZ4y703C3vdjk7mozCw1OarL\",\"transaction_type\":\"place\",\"unofficial_currency_code\":null,\"website\":\"walmart.com\"}"
+ ReturnCount:StructuredApiErrorResponseSerializer.kt$StructuredApiErrorResponseSerializer$override fun selectDeserializer( element: JsonElement ): DeserializationStrategy<StructuredApiErrorResponse>
+ SwallowedException:RequestExecutor.kt$RequestExecutor$e: Exception
+ TooGenericExceptionCaught:RequestExecutor.kt$RequestExecutor$e: Exception
+ TooManyFunctions:LunchmoneyApi.kt$LunchmoneyApi : LunchmoneyApiInternal
+ UtilityClassWithPublicConstructor:TestBase.kt$TestBase
+
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/api/LunchmoneyApi.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/api/LunchmoneyApi.kt
index 8115d04..1736cc2 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/api/LunchmoneyApi.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/api/LunchmoneyApi.kt
@@ -3,7 +3,6 @@ package io.github.smaugfm.lunchmoney.api
import io.github.smaugfm.lunchmoney.model.LunchmoneyAsset
import io.github.smaugfm.lunchmoney.model.LunchmoneyBudget
import io.github.smaugfm.lunchmoney.model.LunchmoneyCategory
-import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryOld
import io.github.smaugfm.lunchmoney.model.LunchmoneyCrypto
import io.github.smaugfm.lunchmoney.model.LunchmoneyInsertTransaction
import io.github.smaugfm.lunchmoney.model.LunchmoneyPlaidAccount
@@ -36,6 +35,7 @@ import io.github.smaugfm.lunchmoney.request.category.UpdateCategoryRequest
import io.github.smaugfm.lunchmoney.request.category.params.AddToCategoryGroupsParams
import io.github.smaugfm.lunchmoney.request.category.params.CreateCategoryGroupRequestParams
import io.github.smaugfm.lunchmoney.request.category.params.CreateUpdateCategoryRequestParams
+import io.github.smaugfm.lunchmoney.request.category.params.GetAllCategoriesParams
import io.github.smaugfm.lunchmoney.request.crypto.GetAllCryptoRequest
import io.github.smaugfm.lunchmoney.request.crypto.UpdateManualCryptoAsset
import io.github.smaugfm.lunchmoney.request.crypto.params.UpdateManualCryptoParams
@@ -218,7 +218,7 @@ class LunchmoneyApi internal constructor(
groupId: Long,
categoryIds: List? = null,
newCategories: List? = null
- ): Mono = execute(
+ ): Mono = execute(
AddToCategoryGroupRequest(
groupId,
AddToCategoryGroupsParams(
@@ -256,7 +256,6 @@ class LunchmoneyApi internal constructor(
excludeFromBudget: Boolean,
excludeFromTotals: Boolean,
description: String? = null,
- categoryIds: List? = null,
groupId: Long? = null
): Mono = execute(
CreateCategoryRequest(
@@ -279,8 +278,18 @@ class LunchmoneyApi internal constructor(
ForceDeleteCategoryRequest(categoryId)
)
- fun getAllCategories(): Mono> = execute(
- GetAllCategoriesRequest()
+ fun getAllCategories(
+ isNested: Boolean = false
+ ): Mono> = execute(
+ GetAllCategoriesRequest(
+ GetAllCategoriesParams(
+ if (isNested) {
+ GetAllCategoriesParams.Format.Nested
+ } else {
+ GetAllCategoriesParams.Format.Flattened
+ }
+ )
+ )
).map { it.categories }
fun getSingleCategory(categoryId: Long): Mono = execute(
@@ -294,7 +303,6 @@ class LunchmoneyApi internal constructor(
excludeFromTotals: Boolean,
name: String? = null,
description: String? = null,
- categoryIds: List? = null,
groupId: Long? = null
): Mono = execute(
UpdateCategoryRequest(
@@ -384,15 +392,15 @@ class LunchmoneyApi internal constructor(
plaidAccountId: Long? = null,
categoryId: Long? = null,
assetId: Long? = null,
- groupId: Long? = null,
isGroup: Boolean? = null,
status: LunchmoneyTransactionStatus? = null,
- offset: Long? = null,
- limit: Long? = null,
startDate: LocalDate? = null,
endDate: LocalDate? = null,
debitAsNegative: Boolean? = null,
- pending: Boolean? = null
+ pending: Boolean? = null,
+ offset: Long? = null,
+ limit: Long? = null,
+ groupId: Long? = null
): Mono> = execute(
GetAllTransactionsRequest(
GetAllTransactionsParams(
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/api/RequestExecutor.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/api/RequestExecutor.kt
index 4ea804a..fa33331 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/api/RequestExecutor.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/api/RequestExecutor.kt
@@ -14,7 +14,6 @@ import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToStream
-import kotlinx.serialization.serializer
import mu.KotlinLogging
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono
@@ -49,7 +48,6 @@ internal class RequestExecutor(
.send(requestBodyToByteBuffer(paramsSerializer, request.body()))
.responseSingle { resp, byteBufMono ->
processResponse(resp, byteBufMono, responseSerializer)
- .doOnNext { log.debug { "Response (${resp.status()}): $it" } }
}.doOnSubscribe {
log.debug { "Performing Lunchmoney API request $request" }
}.let {
@@ -86,43 +84,39 @@ internal class RequestExecutor(
.asString()
.flatMap { body: String ->
deserializeResponseBody(serializer, resp.status().code(), body)
- .transformDeferred { mapUnknownError(it, body, resp.status().code()) }
- }.transformDeferred { errorOnEmptyResponse(it, resp) }
-
- private fun errorOnEmptyResponse(mono: Mono, resp: HttpClientResponse): Mono =
- mono.switchIfEmpty(
- if (isOkResponse(resp)) {
- Mono.empty()
- } else {
- Mono.error(LunchmoneyApiResponseException(resp.status().code()))
- }
- )
-
- private fun mapUnknownError(mono: Mono, body: String, statusCode: Int): Mono =
- mono.onErrorMap({ it !is LunchmoneyApiResponseException }) {
- LunchmoneyApiResponseException(
- body,
- it,
- statusCode
+ .doOnNext { log.debug { "Response (${resp.status()})\n$body\n$it" } }
+ }.switchIfEmpty(
+ if (isOkResponse(resp)) {
+ Mono.empty()
+ } else {
+ Mono.error(
+ LunchmoneyApiResponseException("Unknown empty response from Lunchmoney")
+ )
+ }
)
- }
private fun deserializeResponseBody(
serializer: KSerializer,
status: Int,
body: String
): Mono =
- doDeserialize(serializer, body)
+ Mono.fromCallable { json.decodeFromString(serializer, body) }
.onErrorResume(SerializationException::class.java) {
- deserializeApiError(body)
- .flatMap { Mono.error(it.toException(body, status)) }
+ val apiError = getApiError(body, status)
+ Mono.error(LunchmoneyApiResponseException(apiError.msg))
}
- private fun deserializeApiError(body: String) =
- doDeserialize(json.serializersModule.serializer(), body)
-
- private fun doDeserialize(serializer: KSerializer, body: String): Mono =
- Mono.fromCallable { json.decodeFromString(serializer, body) }
+ private fun getApiError(body: String, status: Int): ApiErrorResponse =
+ try {
+ json.decodeFromString(
+ ApiErrorResponse.StructuredApiErrorResponse.serializer(),
+ body
+ )
+ } catch (e: Exception) {
+ ApiErrorResponse.UnknownApiErrorResponse(
+ "Unknown Lunchmoney API error. HTTP status: $status, body: \n$body"
+ )
+ }
private fun serializeRequestBody(serializer: KSerializer, body: T): ByteArray {
val os = ByteArrayOutputStream()
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/exception/LunchmoneyApiResponseException.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/exception/LunchmoneyApiResponseException.kt
index a846b55..94be245 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/exception/LunchmoneyApiResponseException.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/exception/LunchmoneyApiResponseException.kt
@@ -1,54 +1,3 @@
package io.github.smaugfm.lunchmoney.exception
-import io.github.smaugfm.lunchmoney.response.ApiErrorResponse
-import io.netty.handler.codec.http.HttpResponseStatus
-
-@Suppress("MemberVisibilityCanBePrivate")
-class LunchmoneyApiResponseException : LunchmoneyApiException {
- val apiErrorResponse: ApiErrorResponse?
- val statusCode: Int
- val body: String
-
- constructor(
- body: String,
- cause: ApiErrorResponse,
- statusCode: Int
- ) : super(errorMessage(cause)) {
- this.body = body
- this.apiErrorResponse = cause
- this.statusCode = statusCode
- }
-
- constructor(
- body: String,
- cause: Throwable,
- statusCode: Int
- ) : super(cause) {
- this.body = body
- this.apiErrorResponse = null
- this.statusCode = statusCode
- }
-
- constructor(
- statusCode: Int
- ) : super(
- "Response body is empty but status is $statusCode " +
- HttpResponseStatus.valueOf(statusCode).reasonPhrase()
- ) {
- this.body = ""
- this.apiErrorResponse = null
- this.statusCode = statusCode
- }
-
- companion object {
- private fun errorMessage(apiErrorResponse: ApiErrorResponse): String {
- val msg = apiErrorResponse.message
- if (msg != null) {
- return msg
- }
-
- return apiErrorResponse.error?.joinToString(", ")
- ?: "Received erroneous response from Lunchmoney API"
- }
- }
-}
+class LunchmoneyApiResponseException(message: String) : LunchmoneyApiException(message)
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudget.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudget.kt
index 4aba98e..90d7dd2 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudget.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudget.kt
@@ -19,5 +19,7 @@ data class LunchmoneyBudget(
val excludeFromTotals: Boolean,
val data: Map? = null,
val config: LunchmoneyBudgetConfig? = null,
- val order: Int? = null
+ val order: Int? = null,
+ val archived: Boolean? = null,
+ val recurring: LunchmoneyBudgetRecurringItemList? = null
)
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudgetRecurringItem.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudgetRecurringItem.kt
new file mode 100644
index 0000000..95ea072
--- /dev/null
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudgetRecurringItem.kt
@@ -0,0 +1,21 @@
+@file:UseSerializers(
+ BigDecimalSerializer::class,
+ CurrencySerializer::class
+)
+
+package io.github.smaugfm.lunchmoney.model
+
+import io.github.smaugfm.lunchmoney.serializer.BigDecimalSerializer
+import io.github.smaugfm.lunchmoney.serializer.CurrencySerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+import java.math.BigDecimal
+import java.util.Currency
+
+@Serializable
+data class LunchmoneyBudgetRecurringItem(
+ val payee: String,
+ val amount: BigDecimal,
+ val currency: Currency,
+ val toBase: Double
+)
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudgetRecurringItemList.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudgetRecurringItemList.kt
new file mode 100644
index 0000000..d9f02b8
--- /dev/null
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyBudgetRecurringItemList.kt
@@ -0,0 +1,8 @@
+package io.github.smaugfm.lunchmoney.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class LunchmoneyBudgetRecurringItemList(
+ val list: List
+)
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyCategory.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyCategory.kt
index 3032665..cd4d9f6 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyCategory.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyCategory.kt
@@ -20,8 +20,8 @@ data class LunchmoneyCategory(
val updatedAt: Instant? = null,
val createdAt: Instant? = null,
val isGroup: Boolean? = null,
- val groupCategoryName: String? = null,
val groupId: Long? = null,
val order: Long? = null,
- val children: List? = null
+ val children: List? = null,
+ val groupCategoryName: String? = null
)
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyCategoryOld.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyCategoryOld.kt
deleted file mode 100644
index 92f8101..0000000
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyCategoryOld.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.github.smaugfm.lunchmoney.model
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class LunchmoneyCategoryOld(
- val id: Long,
- val name: String,
- val description: String? = null,
- val isIncome: Boolean,
- val excludeFromBudget: Boolean,
- val excludeFromTotals: Boolean,
- val isGroup: Boolean,
- val groupId: Long? = null,
- val groupCategoryName: String? = null,
- val children: List? = null
-)
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransaction.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransaction.kt
index 1966522..2cbc8bf 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransaction.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransaction.kt
@@ -80,5 +80,5 @@ data class LunchmoneyTransaction(
val accountDisplayName: String,
val tags: List? = null,
val children: List? = null,
- val externalId: String? = null,
+ val externalId: String? = null
)
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransactionChild.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransactionChild.kt
index 367be2c..76af100 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransactionChild.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransactionChild.kt
@@ -2,7 +2,7 @@
LocalDateSerializer::class,
BigDecimalSerializer::class,
CurrencySerializer::class,
- InstantSerializer::class,
+ InstantSerializer::class
)
package io.github.smaugfm.lunchmoney.model
@@ -21,7 +21,7 @@ import java.util.Currency
data class LunchmoneyTransactionChild(
val id: Long,
val date: LocalDate,
- val payee: String,
+ val payee: String?,
val amount: BigDecimal,
val currency: Currency,
val formattedDate: LocalDate,
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransactionTag.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransactionTag.kt
index 58f5b8d..07af38d 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransactionTag.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/model/LunchmoneyTransactionTag.kt
@@ -7,5 +7,5 @@ data class LunchmoneyTransactionTag(
val id: Long,
val name: String,
val description: String? = null,
- val archived: Boolean? = null,
+ val archived: Boolean? = null
)
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/request/category/AddToCategoryGroupRequest.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/request/category/AddToCategoryGroupRequest.kt
index 8c7a90a..0528f5a 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/request/category/AddToCategoryGroupRequest.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/request/category/AddToCategoryGroupRequest.kt
@@ -1,14 +1,14 @@
package io.github.smaugfm.lunchmoney.request.category
import io.github.smaugfm.lunchmoney.helper.PathAndQuery
-import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryOld
+import io.github.smaugfm.lunchmoney.model.LunchmoneyCategory
import io.github.smaugfm.lunchmoney.request.base.LunchmoneyAbstractPostRequest
import io.github.smaugfm.lunchmoney.request.category.params.AddToCategoryGroupsParams
internal class AddToCategoryGroupRequest(
groupId: Long,
params: AddToCategoryGroupsParams
-) : LunchmoneyAbstractPostRequest(
+) : LunchmoneyAbstractPostRequest(
PathAndQuery
.segment("categories")
.segment("group")
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/response/ApiErrorResponse.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/response/ApiErrorResponse.kt
index 4aa04a3..a43c52c 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/response/ApiErrorResponse.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/response/ApiErrorResponse.kt
@@ -1,19 +1,57 @@
package io.github.smaugfm.lunchmoney.response
-import io.github.smaugfm.lunchmoney.exception.LunchmoneyApiResponseException
import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryDeletionDependency
-import io.github.smaugfm.lunchmoney.serializer.StringOrStringArrayDeserializer
+import io.github.smaugfm.lunchmoney.serializer.CategoryDeletionErrorSerializer
+import io.github.smaugfm.lunchmoney.serializer.StructuredApiErrorResponseSerializer
import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
-@Serializable
-data class ApiErrorResponse(
- var name: String? = null,
- var message: String? = null,
- @Serializable(with = StringOrStringArrayDeserializer::class)
- var error: List? = null,
- var dependents: LunchmoneyCategoryDeletionDependency? = null
-) {
- fun toException(body: String, statusCode: Int): LunchmoneyApiResponseException {
- return LunchmoneyApiResponseException(body, this, statusCode)
+sealed class ApiErrorResponse {
+
+ abstract val msg: String
+
+ data class UnknownApiErrorResponse(override val msg: String) : ApiErrorResponse()
+
+ @Serializable(StructuredApiErrorResponseSerializer::class)
+ sealed class StructuredApiErrorResponse : ApiErrorResponse() {
+ @Serializable
+ data class AccessTokenError(val name: String, val message: String) : StructuredApiErrorResponse() {
+ override val msg = message
+ }
+
+ @Serializable
+ data class SingleError(val error: String) : StructuredApiErrorResponse() {
+ override val msg = error
+ }
+
+ @Serializable
+ data class MultipleErrors(val error: List) : StructuredApiErrorResponse() {
+ override val msg = error.joinToString("\n")
+ }
+
+ @Serializable(with = CategoryDeletionErrorSerializer::class)
+ sealed class CategoryDeletionError : StructuredApiErrorResponse() {
+
+ @Serializable
+ data class CategoryDeletionDependencySingle(
+ val dependents: LunchmoneyCategoryDeletionDependency
+ ) : CategoryDeletionError() {
+ override val msg: String
+ get() = json.encodeToString(dependents)
+ }
+
+ @Serializable
+ data class CategoryDeletionDependencyMultiple(
+ val dependents: List
+ ) : CategoryDeletionError() {
+ override val msg: String
+ get() = json.encodeToString(dependents)
+ }
+ }
+ }
+
+ companion object {
+ private val json = Json { prettyPrint = true }
}
}
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/response/GetAllTransactionsResponse.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/response/GetAllTransactionsResponse.kt
index 67febfa..2339d52 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/response/GetAllTransactionsResponse.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/response/GetAllTransactionsResponse.kt
@@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable
@Serializable
internal data class GetAllTransactionsResponse(
val transactions: List,
- val hasMore: Boolean,
+ val hasMore: Boolean
)
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/CategoryDeletionErrorSerializer.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/CategoryDeletionErrorSerializer.kt
new file mode 100644
index 0000000..6dc0c55
--- /dev/null
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/CategoryDeletionErrorSerializer.kt
@@ -0,0 +1,28 @@
+package io.github.smaugfm.lunchmoney.serializer
+
+import io.github.smaugfm.lunchmoney.response.ApiErrorResponse.StructuredApiErrorResponse.CategoryDeletionError
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonContentPolymorphicSerializer
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonObject
+
+internal object CategoryDeletionErrorSerializer :
+ JsonContentPolymorphicSerializer(CategoryDeletionError::class) {
+ override fun selectDeserializer(element: JsonElement): DeserializationStrategy {
+ val dependents = element.jsonObject["dependents"]!!
+ return when (dependents) {
+ is JsonObject -> {
+ CategoryDeletionError.CategoryDeletionDependencySingle.serializer()
+ }
+
+ is JsonArray -> {
+ CategoryDeletionError.CategoryDeletionDependencyMultiple.serializer()
+ }
+
+ else -> throw SerializationException("Could not select polymorphic class")
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/StringOrStringArrayDeserializer.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/StringOrStringArrayDeserializer.kt
deleted file mode 100644
index 644e339..0000000
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/StringOrStringArrayDeserializer.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package io.github.smaugfm.lunchmoney.serializer
-
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.SerializationException
-import kotlinx.serialization.builtins.ListSerializer
-import kotlinx.serialization.descriptors.PrimitiveKind
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-
-internal class StringOrStringArrayDeserializer : KSerializer> {
- private val serializer = ListSerializer(StringSerializer)
- override val descriptor = SerialDescriptor("PossibleStringArray", serializer.descriptor)
-
- @Suppress("SwallowedException")
- override fun deserialize(decoder: Decoder): List {
- return try {
- listOf(decoder.decodeString())
- } catch (e: SerializationException) {
- decoder.decodeSerializableValue(serializer)
- }
- }
-
- override fun serialize(encoder: Encoder, value: List) {
- throw UnsupportedOperationException()
- }
-
- companion object {
-
- object StringSerializer : KSerializer {
- override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor("custom.kotlin.String", PrimitiveKind.STRING)
-
- override fun serialize(encoder: Encoder, value: String): Unit =
- encoder.encodeString(value)
-
- override fun deserialize(decoder: Decoder): String = decoder.decodeString()
- }
- }
-}
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/StructuredApiErrorResponseSerializer.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/StructuredApiErrorResponseSerializer.kt
new file mode 100644
index 0000000..70bebb5
--- /dev/null
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/StructuredApiErrorResponseSerializer.kt
@@ -0,0 +1,40 @@
+package io.github.smaugfm.lunchmoney.serializer
+
+import io.github.smaugfm.lunchmoney.response.ApiErrorResponse.StructuredApiErrorResponse
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonContentPolymorphicSerializer
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonObject
+
+internal object StructuredApiErrorResponseSerializer :
+ JsonContentPolymorphicSerializer(
+ StructuredApiErrorResponse::class
+ ) {
+ override fun selectDeserializer(
+ element: JsonElement
+ ): DeserializationStrategy {
+ val obj = element.jsonObject
+ if (obj.containsKey("name")) {
+ return StructuredApiErrorResponse.AccessTokenError.serializer()
+ }
+ if (obj.containsKey("dependents")) {
+ return StructuredApiErrorResponse.CategoryDeletionError.serializer()
+ }
+ if (obj.containsKey("error")) {
+ val error = obj["error"]!!
+ when {
+ error is JsonPrimitive && error.isString -> {
+ return StructuredApiErrorResponse.SingleError.serializer()
+ }
+
+ error is JsonArray -> {
+ return StructuredApiErrorResponse.MultipleErrors.serializer()
+ }
+ }
+ }
+ throw SerializationException("Could not select polymorphic class")
+ }
+}
diff --git a/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/TransactionSourceSerializer.kt b/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/TransactionSourceSerializer.kt
index 1b903f6..7b00226 100644
--- a/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/TransactionSourceSerializer.kt
+++ b/src/main/kotlin/io/github/smaugfm/lunchmoney/serializer/TransactionSourceSerializer.kt
@@ -5,5 +5,4 @@ import io.github.smaugfm.lunchmoney.model.enumeration.LunchmoneyTransactionSourc
class TransactionSourceSerializer : LowercaseEnumSerializer(
"TransactionSource",
LunchmoneyTransactionSource.entries.toTypedArray()
-) {
-}
+)
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/AuthorizationTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/AuthorizationTest.kt
index 3468e9e..508fe80 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/AuthorizationTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/AuthorizationTest.kt
@@ -1,22 +1,19 @@
package io.github.smaugfm.lunchmoney
import assertk.assertFailure
-import assertk.assertThat
import assertk.assertions.cause
import assertk.assertions.contains
-import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotNull
import assertk.assertions.prop
import io.github.smaugfm.lunchmoney.exception.LunchmoneyApiResponseException
-import io.github.smaugfm.lunchmoney.model.LunchmoneyUser
import io.github.smaugfm.lunchmoney.request.user.GetCurrentUserRequest
import org.junit.jupiter.api.Test
internal class AuthorizationTest : TestMockServerBase() {
@Test
- fun whenNoAuthorizationHeader_ApiExceptionIsRaised() {
+ fun whenInvalidAuthorizationHeader_ApiExceptionIsRaised() {
val api = LunchmoneyTest(
"invalid",
BASE_URL,
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/JsonSchemaUpToDateTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/JsonSchemaUpToDateTest.kt
index 7b2cb20..a63b2b3 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/JsonSchemaUpToDateTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/JsonSchemaUpToDateTest.kt
@@ -1,24 +1,37 @@
package io.github.smaugfm.lunchmoney
+import assertk.assertFailure
import assertk.assertThat
+import assertk.assertions.cause
+import assertk.assertions.contains
import assertk.assertions.hasSize
import assertk.assertions.isBetween
import assertk.assertions.isEqualTo
-import assertk.assertions.isGreaterThan
+import assertk.assertions.isInstanceOf
+import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.isTrue
import assertk.assertions.prop
import assertk.assertions.size
import io.github.smaugfm.lunchmoney.api.LunchmoneyApi
+import io.github.smaugfm.lunchmoney.exception.LunchmoneyApiResponseException
+import io.github.smaugfm.lunchmoney.model.LunchmoneyAsset
+import io.github.smaugfm.lunchmoney.model.LunchmoneyBudget
+import io.github.smaugfm.lunchmoney.model.LunchmoneyBudgetData
import io.github.smaugfm.lunchmoney.model.LunchmoneyCategory
+import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryChild
import io.github.smaugfm.lunchmoney.model.LunchmoneyInsertTransaction
import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction
+import io.github.smaugfm.lunchmoney.model.LunchmoneyTransactionTag
import io.github.smaugfm.lunchmoney.model.LunchmoneyUpdateTransaction
import io.github.smaugfm.lunchmoney.model.LunchmoneyUser
+import io.github.smaugfm.lunchmoney.model.enumeration.LunchmoneyAssetType
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable
import java.math.BigDecimal
+import java.time.Instant
import java.time.LocalDate
+import java.util.Currency
@EnabledIfEnvironmentVariable(named = "LUNCHMONEY_TEST_TOKEN", matches = "\\w+")
class JsonSchemaUpToDateTest : TestBase() {
@@ -26,14 +39,93 @@ class JsonSchemaUpToDateTest : TestBase() {
private val api = LunchmoneyApi(System.getenv("LUNCHMONEY_TEST_TOKEN"))
@Test
- fun getUserTest() {
- val user = api.getCurrentUser().block()!!
- assertThat(user).prop(LunchmoneyUser::userName)
- .isEqualTo("Dmytro Marchuk")
- assertThat(user).prop(LunchmoneyUser::budgetName)
- .isEqualTo("test")
- assertThat(user).prop(LunchmoneyUser::apiKeyLabel)
- .isEqualTo("Github Actions")
+ fun invalidAuthTest() {
+ val api = LunchmoneyApi("invalid")
+ assertFailure {
+ api.getCurrentUser().block()
+ }.isInstanceOf(RuntimeException::class)
+ .cause()
+ .isNotNull()
+ .isInstanceOf(LunchmoneyApiResponseException::class)
+ .prop(LunchmoneyApiResponseException::message)
+ .isNotNull()
+ .contains("Access token does not exist.")
+ }
+
+ @Test
+ fun getAllAssetsTest() {
+ val assets = api.getAllAssets().block()!!
+ assertThat(assets)
+ .hasSize(4)
+ assertThat(assets[0])
+ .isEqualTo(
+ LunchmoneyAsset(
+ id = 55209,
+ typeName = LunchmoneyAssetType.CREDIT,
+ subtypeName = "credit card",
+ name = "Credit Card (..4977)",
+ displayName = "Penny's Visa Card",
+ balance = "-684.6300".toBigDecimal(),
+ balanceAsOf = Instant.parse("2023-07-04T10:17:14Z"),
+ closedOn = null,
+ currency = Currency.getInstance("UAH"),
+ institutionName = "Chase",
+ excludeTransactions = false,
+ createdAt = Instant.parse("2023-07-04T10:17:09.544Z")
+ )
+ )
+ }
+
+ @Test
+ fun getBudgetSummaryTest() {
+ val budgets = api.getBudgetSummary(
+ LocalDate.parse("2023-07-01"),
+ LocalDate.parse("2023-07-30")
+ ).block()!!
+ assertThat(budgets)
+ .hasSize(11)
+ assertThat(budgets[3])
+ .isEqualTo(
+ LunchmoneyBudget(
+ categoryName = "Food",
+ categoryId = 489285,
+ categoryGroupName = null,
+ groupId = null,
+ isGroup = true,
+ isIncome = false,
+ excludeFromBudget = false,
+ excludeFromTotals = false,
+ data = mapOf(
+ LocalDate.parse("2023-07-01") to
+ LunchmoneyBudgetData(
+ numTransactions = 9,
+ spendingToBase = 1381.6100000000001,
+ budgetToBase = 430.0,
+ budgetAmount = 430.0,
+ budgetCurrency = Currency.getInstance("UAH"),
+ isAutomated = null
+ )
+ ),
+ config = null,
+ order = 3,
+ archived = false,
+ recurring = null
+ )
+ )
+ }
+
+ @Test
+ fun crudBudgetTest() {
+ api.removeBudget(
+ LocalDate.parse("2023-07-01"),
+ 489285
+ ).block()!!
+ api.upsertBudget(
+ LocalDate.parse("2023-07-01"),
+ 489285,
+ 430.0,
+ Currency.getInstance("UAH")
+ ).block()
}
@Test
@@ -41,7 +133,85 @@ class JsonSchemaUpToDateTest : TestBase() {
val categories = api.getAllCategories().block()!!
assertThat(categories)
.size()
- .isBetween(11, 12)
+ .isEqualTo(11)
+ }
+
+ @Test
+ fun getAllCategoriesNestedTest() {
+ val categories = api.getAllCategories(true).block()!!
+ assertThat(categories)
+ .size()
+ .isEqualTo(7)
+ assertThat(categories[1])
+ .isEqualTo(
+ LunchmoneyCategory(
+ id = 489285,
+ name = "Food",
+ description = "Consumables",
+ isIncome = false,
+ excludeFromBudget = false,
+ excludeFromTotals = false,
+ archived = false,
+ archivedOn = null,
+ updatedAt = Instant.parse("2023-07-04T10:17:01.900Z"),
+ createdAt = Instant.parse("2023-07-04T10:17:01.900Z"),
+ isGroup = true,
+ groupId = null,
+ order = null,
+ children = listOf(
+ LunchmoneyCategoryChild(
+ id = 489276,
+ name = "Coffee Shops",
+ description = null,
+ createdAt = Instant.parse("2023-07-04T10:17:01.599Z")
+ ),
+ LunchmoneyCategoryChild(
+ id = 489279,
+ name = "Food Delivery",
+ description = null,
+ createdAt = Instant.parse("2023-07-04T10:17:01.609Z")
+ ),
+ LunchmoneyCategoryChild(
+ id = 489278,
+ name = "Groceries",
+ description = null,
+ createdAt = Instant.parse("2023-07-04T10:17:01.605Z")
+ ),
+ LunchmoneyCategoryChild(
+ id = 489281,
+ name = "Restaurants",
+ description = null,
+ createdAt = Instant.parse("2023-07-04T10:17:01.614Z")
+ )
+ ),
+ groupCategoryName = null
+ )
+ )
+ }
+
+ @Test
+ fun getSingleCategoryTest() {
+ val category = api.getSingleCategory(489275).block()!!
+ assertThat(category)
+ .isEqualTo(
+ LunchmoneyCategory(
+ id = 489275,
+ name = "Alcohol, Bars",
+ description = null,
+ isIncome = false,
+ excludeFromBudget = false,
+ excludeFromTotals = false,
+ archived = false,
+ archivedOn = null,
+ updatedAt = null,
+ createdAt = null,
+ isGroup = false,
+ groupId = null,
+ order = null,
+ children = null,
+ groupCategoryName = null
+ )
+ )
}
@Test
@@ -92,19 +262,42 @@ class JsonSchemaUpToDateTest : TestBase() {
}
}
+ @Test
+ fun getUserTest() {
+ val user = api.getCurrentUser().block()!!
+ assertThat(user).prop(LunchmoneyUser::userName)
+ .isEqualTo("Dmytro Marchuk")
+ assertThat(user).prop(LunchmoneyUser::budgetName)
+ .isEqualTo("test")
+ assertThat(user).prop(LunchmoneyUser::apiKeyLabel)
+ .isEqualTo("local testing")
+ }
+
@Test
fun getAllTagsTest() {
val tags = api.getAllTags().block()!!
assertThat(tags)
.hasSize(5)
+ assertThat(tags[0])
+ .isEqualTo(
+ LunchmoneyTransactionTag(
+ id = 54113,
+ name = "Penny's",
+ description = "",
+ archived = false
+ )
+ )
}
@Test
fun getAllTransactionsTest() {
- val transactions = api.getAllTransactions().block()!!
+ val transactions = api.getAllTransactions(
+ startDate = LocalDate.of(2023, 7, 1),
+ endDate = LocalDate.of(2023, 7, 30)
+ ).block()!!
assertThat(transactions)
.size()
- .isGreaterThan(23)
+ .isEqualTo(37)
}
@Test
@@ -129,7 +322,10 @@ class JsonSchemaUpToDateTest : TestBase() {
@Test
fun crudTransactionGroupTest() {
- val transactions = api.getAllTransactions().block()!!
+ val transactions = api.getAllTransactions(
+ startDate = LocalDate.of(2023, 7, 1),
+ endDate = LocalDate.of(2023, 7, 30)
+ ).block()!!
.filter { it.recurringId == null && it.groupId == null && !it.isGroup }
.subList(0, 2)
val id = api.createTransactionGroup(
@@ -153,7 +349,7 @@ class JsonSchemaUpToDateTest : TestBase() {
val from = LocalDate.of(2023, 6, 1)
val exp = api.getRecurringExpenses(from).block()!!
assertThat(exp)
- .hasSize(6)
+ .hasSize(4)
}
@Test
@@ -176,13 +372,6 @@ class JsonSchemaUpToDateTest : TestBase() {
assertThat(api.removeBudget(from, catId).block()!!).isTrue()
}
- @Test
- fun getAllAssetsTest() {
- val assets = api.getAllAssets().block()!!
- assertThat(assets)
- .hasSize(4)
- }
-
@Test
fun getAllPlaidAccounts() {
assertThat(api.getAllPlaidAccounts().block()!!).hasSize(0)
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/api/RequestExecutorTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/api/RequestExecutorTest.kt
index 07d798d..a0c1d8e 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/api/RequestExecutorTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/api/RequestExecutorTest.kt
@@ -2,13 +2,9 @@ package io.github.smaugfm.lunchmoney.api
import assertk.all
import assertk.assertFailure
-import assertk.assertThat
-import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
-import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotNull
-import assertk.assertions.isNull
import assertk.assertions.prop
import io.github.resilience4j.core.IntervalFunction
import io.github.resilience4j.kotlin.retry.RetryConfig
@@ -58,12 +54,8 @@ class RequestExecutorTest : TestMockServerBase() {
.isNotNull()
.isInstanceOf(LunchmoneyApiResponseException::class.java)
.all {
- prop(LunchmoneyApiResponseException::apiErrorResponse)
- .isNull()
- prop(LunchmoneyApiResponseException::body)
- .isEmpty()
- prop(LunchmoneyApiResponseException::statusCode)
- .isEqualTo(500)
+ prop(LunchmoneyApiResponseException::message)
+ .isEqualTo("Unknown empty response from Lunchmoney")
}
mockServer.verify(
request("/me")
@@ -85,7 +77,6 @@ class RequestExecutorTest : TestMockServerBase() {
.withMethod("GET")
).respond(
response()
- .withStatusCode(200)
.withContentType(MediaType.TEXT_HTML_UTF_8)
.withStatusCode(500)
.withBody(body)
@@ -95,12 +86,9 @@ class RequestExecutorTest : TestMockServerBase() {
.prop(Throwable::cause)
.transform { it as LunchmoneyApiResponseException }
.all {
- prop(LunchmoneyApiResponseException::statusCode)
- .isEqualTo(500)
- prop(LunchmoneyApiResponseException::body)
- .isEqualTo(body)
- prop(LunchmoneyApiResponseException::apiErrorResponse)
- .isNull()
+ prop(LunchmoneyApiResponseException::message)
+ .isNotNull()
+ .isEqualTo("Unknown Lunchmoney API error. HTTP status: 500, body: \n$body")
}
}
}
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/AddToCategoryGroupRequestTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/AddToCategoryGroupRequestTest.kt
index a965ce8..252f06d 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/AddToCategoryGroupRequestTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/AddToCategoryGroupRequestTest.kt
@@ -4,8 +4,8 @@ import assertk.assertThat
import assertk.assertions.isEqualTo
import io.github.smaugfm.lunchmoney.TestMockServerBase
import io.github.smaugfm.lunchmoney.Util.getResourceAsString
+import io.github.smaugfm.lunchmoney.model.LunchmoneyCategory
import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryChild
-import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryOld
import io.github.smaugfm.lunchmoney.request.category.params.AddToCategoryGroupsParams
import org.junit.jupiter.api.Test
import java.time.Instant
@@ -34,7 +34,7 @@ internal class AddToCategoryGroupRequestTest : TestMockServerBase() {
)
assertThat(api.execute(request).block())
.isEqualTo(
- LunchmoneyCategoryOld(
+ LunchmoneyCategory(
315358L,
"Food & Drink",
null,
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/CreateCategoryGroupRequestTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/CreateCategoryGroupRequestTest.kt
index c55a2dd..42c34c6 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/CreateCategoryGroupRequestTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/CreateCategoryGroupRequestTest.kt
@@ -5,7 +5,6 @@ import assertk.assertThat
import assertk.assertions.cause
import assertk.assertions.contains
import assertk.assertions.isEqualTo
-import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotNull
import assertk.assertions.prop
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/DeleteCategoryRequestTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/DeleteCategoryRequestTest.kt
index 4ced00f..31bb3c8 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/DeleteCategoryRequestTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/DeleteCategoryRequestTest.kt
@@ -4,7 +4,6 @@ import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.cause
import assertk.assertions.isEqualTo
-import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotNull
import assertk.assertions.prop
@@ -12,7 +11,7 @@ import io.github.smaugfm.lunchmoney.TestMockServerBase
import io.github.smaugfm.lunchmoney.Util.getResourceAsString
import io.github.smaugfm.lunchmoney.exception.LunchmoneyApiResponseException
import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryDeletionDependency
-import io.github.smaugfm.lunchmoney.response.ApiErrorResponse
+import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Test
import org.mockserver.model.HttpRequest.request
import org.mockserver.model.HttpResponse.response
@@ -58,17 +57,65 @@ internal class DeleteCategoryRequestTest : TestMockServerBase() {
.cause()
.isNotNull()
.isInstanceOf(LunchmoneyApiResponseException::class.java)
- .prop(LunchmoneyApiResponseException::apiErrorResponse)
+ .prop(LunchmoneyApiResponseException::message)
.isNotNull()
- .prop(ApiErrorResponse::dependents)
- .isEqualTo(
+ .transform {
+ Json.decodeFromString(it)
+ }.isEqualTo(
LunchmoneyCategoryDeletionDependency(
"Food & Drink",
4L,
0L,
43L,
7L,
- 0L
+ 0
+ )
+ )
+ }
+
+ @Test
+ fun deleteCategoryRequestTestWithDependentsMultiple() {
+ val id = 1234L
+ mockServer.`when`(
+ request("/categories/$id")
+ .withMethod("DELETE")
+ ).respond(
+ response()
+ .withStatusCode(200)
+ .withContentType(org.mockserver.model.MediaType.APPLICATION_JSON_UTF_8)
+ .withBody(getResourceAsString("response/deleteCategory-dependents-multiple.json"))
+
+ )
+ val deleteCategoryRequest = DeleteCategoryRequest(
+ id
+ )
+ assertFailure { api.execute(deleteCategoryRequest).block() }
+ .isInstanceOf(RuntimeException::class)
+ .cause()
+ .isNotNull()
+ .isInstanceOf(LunchmoneyApiResponseException::class.java)
+ .prop(LunchmoneyApiResponseException::message)
+ .isNotNull()
+ .transform {
+ Json.decodeFromString>(it)
+ }.isEqualTo(
+ listOf(
+ LunchmoneyCategoryDeletionDependency(
+ "Food & Drink",
+ 4L,
+ 0L,
+ 43L,
+ 7L,
+ 0
+ ),
+ LunchmoneyCategoryDeletionDependency(
+ "Food & Drink",
+ 4L,
+ 0L,
+ 43L,
+ 7L,
+ 0
+ )
)
)
}
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/GetAllCategoriesTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/GetAllCategoriesTest.kt
index 440f3f9..5945afe 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/GetAllCategoriesTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/GetAllCategoriesTest.kt
@@ -64,7 +64,7 @@ internal class GetAllCategoriesTest : TestMockServerBase() {
id = 315162,
name = "Alcohol, Bars",
description = null,
- createdAt = Instant.parse("2022-03-06T20:11:36.066Z"),
+ createdAt = Instant.parse("2022-03-06T20:11:36.066Z")
)
)
)
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/GetSingleCategoryRequestTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/GetSingleCategoryRequestTest.kt
index f2a4b3a..ab281c6 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/GetSingleCategoryRequestTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/category/GetSingleCategoryRequestTest.kt
@@ -6,7 +6,6 @@ import io.github.smaugfm.lunchmoney.TestMockServerBase
import io.github.smaugfm.lunchmoney.Util.getResourceAsString
import io.github.smaugfm.lunchmoney.model.LunchmoneyCategory
import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryChild
-import io.github.smaugfm.lunchmoney.model.LunchmoneyCategoryOld
import org.junit.jupiter.api.Test
import org.mockserver.model.HttpRequest.request
import org.mockserver.model.HttpResponse.response
@@ -39,8 +38,8 @@ internal class GetSingleCategoryRequestTest : TestMockServerBase() {
excludeFromTotals = false,
isGroup = false,
groupId = null,
- groupCategoryName = null,
- children = null
+ children = null,
+ groupCategoryName = null
)
)
}
@@ -70,7 +69,6 @@ internal class GetSingleCategoryRequestTest : TestMockServerBase() {
excludeFromTotals = false,
isGroup = true,
groupId = null,
- groupCategoryName = null,
children = listOf(
LunchmoneyCategoryChild(
427749L,
@@ -84,7 +82,8 @@ internal class GetSingleCategoryRequestTest : TestMockServerBase() {
null,
Instant.parse("2023-02-02T14:57:43.459Z")
)
- )
+ ),
+ groupCategoryName = null
)
)
}
@@ -114,8 +113,8 @@ internal class GetSingleCategoryRequestTest : TestMockServerBase() {
excludeFromTotals = false,
isGroup = false,
groupId = 427758L,
- groupCategoryName = "Food",
- children = null
+ children = null,
+ groupCategoryName = "Food"
)
)
}
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/GetAllTransactionsTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/GetAllTransactionsTest.kt
index 262f3c2..f33bf12 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/GetAllTransactionsTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/GetAllTransactionsTest.kt
@@ -127,7 +127,7 @@ internal class GetAllTransactionsTest : TestMockServerBase() {
toBase = -33.6
)
)
- ),
+ )
),
hasMore = true
)
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/GetSingleTransactionRequestTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/GetSingleTransactionRequestTest.kt
index 8dd81ea..bc8bddf 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/GetSingleTransactionRequestTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/GetSingleTransactionRequestTest.kt
@@ -5,6 +5,7 @@ import assertk.assertions.isEqualTo
import io.github.smaugfm.lunchmoney.TestMockServerBase
import io.github.smaugfm.lunchmoney.Util.getResourceAsString
import io.github.smaugfm.lunchmoney.model.LunchmoneyTransaction
+import io.github.smaugfm.lunchmoney.model.LunchmoneyTransactionChild
import io.github.smaugfm.lunchmoney.model.enumeration.LunchmoneyTransactionSource
import io.github.smaugfm.lunchmoney.model.enumeration.LunchmoneyTransactionStatus
import io.github.smaugfm.lunchmoney.request.transaction.params.GetSingleTransactionParams
@@ -157,4 +158,74 @@ internal class GetSingleTransactionRequestTest : TestMockServerBase() {
)
)
}
+
+ @Test
+ fun singleTransactionTestSimple2() {
+ val id = 602L
+ mockServer
+ .`when`(
+ request("/transactions/$id")
+ .withMethod("GET")
+ ).respond(
+ response()
+ .withStatusCode(200)
+ .withContentType(MediaType.APPLICATION_JSON_UTF_8)
+ .withBody(getResourceAsString("response/getSingleTransaction2.json"))
+ )
+ val request = GetSingleTransactionRequest(id)
+ assertThat(api.execute(request).block())
+ .isEqualTo(
+ LunchmoneyTransaction(
+ id = 2225874632,
+ date = LocalDate.parse("2024-08-10"),
+ amount = BigDecimal.valueOf(37.09),
+ currency = Currency.getInstance("UAH"),
+ toBase = 37.09,
+ payee = "vasa",
+ categoryId = null,
+ categoryName = null,
+ categoryGroupId = null,
+ categoryGroupName = null,
+ isIncome = false,
+ excludeFromBudget = false,
+ excludeFromTotals = false,
+ createdAt = Instant.parse("2024-08-10T14:49:15.465Z"),
+ updatedAt = Instant.parse("2024-08-10T14:49:15.465Z"),
+ status = LunchmoneyTransactionStatus.CLEARED,
+ isPending = false,
+ hasChildren = false,
+ isGroup = true,
+ source = LunchmoneyTransactionSource.Api,
+ displayName = "vasa",
+ accountDisplayName = "",
+ tags = listOf(),
+ children = listOf(
+ LunchmoneyTransactionChild(
+ id = 220527530,
+ payee = "Farmer's Market",
+ amount = BigDecimal.valueOf(36.09),
+ currency = Currency.getInstance("UAH"),
+ date = LocalDate.parse("2023-07-04"),
+ formattedDate = LocalDate.parse("2023-07-04"),
+ notes = "Jenny's Potluck",
+ assetId = 55209,
+ plaidAccountId = null,
+ toBase = 36.09
+ ),
+ LunchmoneyTransactionChild(
+ id = 220556457,
+ payee = null,
+ amount = BigDecimal.valueOf(1.0),
+ currency = Currency.getInstance("UAH"),
+ date = LocalDate.parse("2023-07-04"),
+ formattedDate = LocalDate.parse("2023-07-04"),
+ notes = null,
+ assetId = null,
+ plaidAccountId = null,
+ toBase = 1.0
+ )
+ )
+ )
+ )
+ }
}
diff --git a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/InsertTransactionsRequestTest.kt b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/InsertTransactionsRequestTest.kt
index fddbbf8..19a78a4 100644
--- a/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/InsertTransactionsRequestTest.kt
+++ b/src/test/kotlin/io/github/smaugfm/lunchmoney/request/transaction/InsertTransactionsRequestTest.kt
@@ -4,7 +4,6 @@ import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.cause
import assertk.assertions.isEqualTo
-import assertk.assertions.isFailure
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotNull
import assertk.assertions.prop
@@ -14,7 +13,6 @@ import io.github.smaugfm.lunchmoney.exception.LunchmoneyApiResponseException
import io.github.smaugfm.lunchmoney.model.LunchmoneyInsertTransaction
import io.github.smaugfm.lunchmoney.model.enumeration.LunchmoneyTransactionStatus
import io.github.smaugfm.lunchmoney.request.transaction.params.InsertTransactionRequestParams
-import io.github.smaugfm.lunchmoney.response.ApiErrorResponse
import io.github.smaugfm.lunchmoney.response.InsertTransactionsResponse
import org.junit.jupiter.api.Test
import org.mockserver.model.HttpRequest.request
@@ -102,15 +100,14 @@ internal class InsertTransactionsRequestTest : TestMockServerBase() {
.cause()
.isNotNull()
.isInstanceOf(LunchmoneyApiResponseException::class)
- .prop(LunchmoneyApiResponseException::apiErrorResponse)
+ .prop(LunchmoneyApiResponseException::message)
.isNotNull()
- .prop(ApiErrorResponse::error)
.isEqualTo(
listOf(
"Transaction 0 is missing date.",
"Transaction 0 is missing amount.",
"Transaction 1 status must be either cleared or uncleared: null"
- )
+ ).joinToString("\n")
)
}
}
diff --git a/src/test/resources/response/deleteCategory-dependents-multiple.json b/src/test/resources/response/deleteCategory-dependents-multiple.json
new file mode 100644
index 0000000..71670ba
--- /dev/null
+++ b/src/test/resources/response/deleteCategory-dependents-multiple.json
@@ -0,0 +1,20 @@
+{
+ "dependents": [
+ {
+ "category_name": "Food & Drink",
+ "budget": 4,
+ "category_rules": 0,
+ "transactions": 43,
+ "children": 7,
+ "recurring": 0
+ },
+ {
+ "category_name": "Food & Drink",
+ "budget": 4,
+ "category_rules": 0,
+ "transactions": 43,
+ "children": 7,
+ "recurring": 0
+ }
+ ]
+}
diff --git a/src/test/resources/response/getSingleTransaction2.json b/src/test/resources/response/getSingleTransaction2.json
new file mode 100644
index 0000000..738e679
--- /dev/null
+++ b/src/test/resources/response/getSingleTransaction2.json
@@ -0,0 +1,77 @@
+{
+ "id": 2225874632,
+ "date": "2024-08-10",
+ "amount": "37.09",
+ "currency": "uah",
+ "to_base": 37.09,
+ "payee": "vasa",
+ "category_id": null,
+ "category_name": null,
+ "category_group_id": null,
+ "category_group_name": null,
+ "is_income": false,
+ "exclude_from_budget": false,
+ "exclude_from_totals": false,
+ "created_at": "2024-08-10T14:49:15.465Z",
+ "updated_at": "2024-08-10T14:49:15.465Z",
+ "status": "cleared",
+ "is_pending": false,
+ "notes": null,
+ "original_name": null,
+ "recurring_id": null,
+ "recurring_payee": null,
+ "recurring_description": null,
+ "recurring_cadence": null,
+ "recurring_granularity": null,
+ "recurring_quantity": null,
+ "recurring_type": null,
+ "recurring_amount": null,
+ "recurring_currency": null,
+ "parent_id": null,
+ "has_children": false,
+ "group_id": null,
+ "is_group": true,
+ "asset_id": null,
+ "asset_institution_name": null,
+ "asset_name": null,
+ "asset_display_name": null,
+ "asset_status": null,
+ "plaid_account_id": null,
+ "plaid_account_name": null,
+ "plaid_account_mask": null,
+ "institution_name": null,
+ "plaid_account_display_name": null,
+ "plaid_metadata": null,
+ "source": "api",
+ "display_name": "vasa",
+ "display_notes": null,
+ "account_display_name": "",
+ "tags": [],
+ "children": [
+ {
+ "id": 220527530,
+ "payee": "Farmer's Market",
+ "amount": "36.09",
+ "currency": "uah",
+ "date": "2023-07-04",
+ "formatted_date": "2023-07-04",
+ "notes": "Jenny's Potluck",
+ "asset_id": 55209,
+ "plaid_account_id": null,
+ "to_base": 36.09
+ },
+ {
+ "id": 220556457,
+ "payee": null,
+ "amount": "1.0",
+ "currency": "uah",
+ "date": "2023-07-04",
+ "formatted_date": "2023-07-04",
+ "notes": null,
+ "asset_id": null,
+ "plaid_account_id": null,
+ "to_base": 1
+ }
+ ],
+ "external_id": null
+}