Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove customization from Plaid client code #96

Merged
merged 16 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
package net.djvk.fireflyPlaidConnector2.api.firefly.models

import com.fasterxml.jackson.annotation.JsonProperty
import net.djvk.fireflyPlaidConnector2.transactions.SortableTransaction
import java.time.OffsetDateTime
import java.time.ZoneId

Expand Down Expand Up @@ -50,30 +49,3 @@ data class TransactionRead(
val links: ObjectLink

)

// : SortableTransaction {
// override val transactionId: String
// get() = id
//
// override val amount: Double
// get() {
// if (attributes.transactions.size != 1) {
// throw IllegalArgumentException(
// "Cannot resolve transaction id for Transaction with " +
// " ${attributes.transactions.size} splits"
// )
// }
// return attributes.transactions.first().amount.toDouble()
// }
//
// override fun getTimestamp(zoneId: ZoneId): OffsetDateTime {
// if (attributes.transactions.size != 1) {
// throw IllegalArgumentException(
// "Cannot resolve transaction id for Transaction with " +
// " ${attributes.transactions.size} splits"
// )
// }
// return attributes.transactions.first().date
// }
//}

Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package net.djvk.fireflyPlaidConnector2.api.plaid

import io.ktor.client.call.*
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.plugins.*
import io.ktor.http.*
import kotlinx.coroutines.delay
import net.djvk.fireflyPlaidConnector2.api.plaid.apis.PlaidApi
import net.djvk.fireflyPlaidConnector2.api.plaid.infrastructure.clientIdHeader
import net.djvk.fireflyPlaidConnector2.api.plaid.infrastructure.secretHeader
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import kotlin.time.Duration.Companion.minutes

typealias PlaidTransactionId = String

const val clientIdHeader = "PLAID-CLIENT-ID"
const val secretHeader = "PLAID-SECRET"

/**
* A wrapper for Plaid API calls that provides additional services:
* - rate limiting
Expand All @@ -20,14 +24,18 @@ import kotlin.time.Duration.Companion.minutes
*/
@Component
class PlaidApiWrapper(
private val plaidApi: PlaidApi,
@Value("\${fireflyPlaidConnector2.plaid.url}")
private val baseUrl: String,
@Value("\${fireflyPlaidConnector2.plaid.maxRetries:3}")
private val maxRetries: Int,
@Value("\${fireflyPlaidConnector2.plaid.clientId}")
private val plaidClientId: String,
@Value("\${fireflyPlaidConnector2.plaid.secret}")
private val plaidSecret: String,
httpClientEngine: HttpClientEngine? = null,
httpClientConfig: ((HttpClientConfig<*>) -> Unit)? = null,
) {
private val plaidApi = PlaidApi(baseUrl, httpClientEngine, httpClientConfig)
private val logger = LoggerFactory.getLogger(this::class.java)

init {
Expand Down Expand Up @@ -62,4 +70,4 @@ class PlaidApiWrapper(
return executeRequest(request, logString, remainingRetries - 1)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import java.io.File

@Component
open class PlaidApi(
@Value("\${fireflyPlaidConnector2.plaid.url}")
baseUrl: String = ApiClient.BASE_URL,
httpClientEngine: HttpClientEngine? = null,
httpClientConfig: ((HttpClientConfig<*>) -> Unit)? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,4 @@ data class PersonalFinanceCategory(
/* A granular category conveying the transaction's intent. This field can also be used as a unique identifier for the category. */
@field:JsonProperty("detailed")
val detailed: kotlin.String
) : kotlin.collections.HashMap<String, kotlin.Any>() {
constructor(enum: PersonalFinanceCategoryEnum) : this(enum.primary.name, enum.name)

fun toEnum(): PersonalFinanceCategoryEnum {
// Special case to handle what I believe is a Plaid bug I saw in the wild
if (primary == PersonalFinanceCategoryEnum.Primary.TRAVEL.name &&
detailed == PersonalFinanceCategoryEnum.TRANSPORTATION_PUBLIC_TRANSIT.name) {
return PersonalFinanceCategoryEnum.TRANSPORTATION_PUBLIC_TRANSIT
}
// Business as usual
return PersonalFinanceCategoryEnum.values().find {
it.primary.name == primary && it.name == detailed
}
?: throw IllegalArgumentException("Failed to convert personal finance category $this to enum")
}
}

) : kotlin.collections.HashMap<String, kotlin.Any>()
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,9 @@
package net.djvk.fireflyPlaidConnector2.api.plaid.models

import com.fasterxml.jackson.annotation.JsonProperty
import net.djvk.fireflyPlaidConnector2.constants.Direction
import net.djvk.fireflyPlaidConnector2.transactions.FireflyAccountId
import net.djvk.fireflyPlaidConnector2.transactions.PlaidAccountId
import net.djvk.fireflyPlaidConnector2.transactions.SortableTransaction
import net.djvk.fireflyPlaidConnector2.transactions.TransactionConverter
import java.time.OffsetDateTime
import java.time.ZoneId

typealias PlaidTransactionId = String
dvankley marked this conversation as resolved.
Show resolved Hide resolved

/**
* A representation of a transaction
*
Expand Down Expand Up @@ -94,7 +87,7 @@ data class Transaction(

/* The settled value of the transaction, denominated in the transactions's currency, as stated in `iso_currency_code` or `unofficial_currency_code`. Positive values when money moves out of the account; negative values when money moves in. For example, debit card purchases are positive; credit card payments, direct deposits, and refunds are negative. */
@field:JsonProperty("amount")
override val amount: kotlin.Double,
val amount: kotlin.Double,

/* The ISO-4217 currency code of the transaction. Always `null` if `unofficial_currency_code` is non-null. */
@field:JsonProperty("iso_currency_code")
Expand All @@ -114,7 +107,7 @@ data class Transaction(

/* The unique ID of the transaction. Like all Plaid identifiers, the `transaction_id` is case sensitive. */
@field:JsonProperty("transaction_id")
override val transactionId: PlaidTransactionId,
val transactionId: kotlin.String,

/* The channel used to make a payment. `online:` transactions that took place online. `in store:` transactions that were made at a physical location. `other:` transactions that relate to banks, e.g. fees or deposits. This field replaces the `transaction_type` field. */
@field:JsonProperty("payment_channel")
Expand Down Expand Up @@ -158,7 +151,22 @@ data class Transaction(
*/
@field:JsonProperty("personal_finance_category")
val personalFinanceCategory: PersonalFinanceCategory?
) : SortableTransaction {
) {

/**
* The channel used to make a payment. `online:` transactions that took place online. `in store:` transactions that were made at a physical location. `other:` transactions that relate to banks, e.g. fees or deposits. This field replaces the `transaction_type` field.
*
* Values: online,inStore,other
*/
enum class PaymentChannel(val value: kotlin.String) {
@JsonProperty(value = "online")
online("online"),
@JsonProperty(value = "in store")
inStore("in store"),
@JsonProperty(value = "other")
other("other");
}

/**
* Please use the `payment_channel` field, `transaction_type` will be deprecated in the future. `digital:` transactions that took place online. `place:` transactions that were made at a physical location. `special:` transactions that relate to banks, e.g. fees or deposits. `unresolved:` transactions that do not fit into the other three types.
*
Expand All @@ -174,25 +182,5 @@ data class Transaction(
@JsonProperty(value = "unresolved")
unresolved("unresolved");
}

fun getDirection(): Direction {
return if (amount > 0) {
Direction.OUT
} else {
Direction.IN
}
}

override fun getTimestamp(zoneId: ZoneId): OffsetDateTime {
return datetime
?: authorizedDatetime
?: TransactionConverter.getOffsetDateTimeForDate(zoneId, date)
}

override fun getFireflyAccountId(accountMap: Map<PlaidAccountId, FireflyAccountId>): FireflyAccountId {
return accountMap[accountId]
?: throw IllegalArgumentException("SortableTransaction.getFireflyAccountId can't be called on a Plaid " +
"transaction with an account id that isn't mapped to a Firefly account id")
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,19 @@ data class TransactionAllOf(

@field:JsonProperty("personal_finance_category")
val personalFinanceCategory: PersonalFinanceCategory? = null
)
) {

/**
* The channel used to make a payment. `online:` transactions that took place online. `in store:` transactions that were made at a physical location. `other:` transactions that relate to banks, e.g. fees or deposits. This field replaces the `transaction_type` field.
*
* Values: online,inStore,other
*/
enum class PaymentChannel(val value: kotlin.String) {
@JsonProperty(value = "online")
online("online"),
@JsonProperty(value = "in store")
inStore("in store"),
@JsonProperty(value = "other")
other("other");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ class BatchSyncRunner(
private val setInitialBalance: Boolean,
@Value("\${fireflyPlaidConnector2.plaid.batchSize}")
private val plaidBatchSize: Int,
@Value("\${fireflyPlaidConnector2.timeZone}")
private val timeZoneString: String,

private val plaidApiWrapper: PlaidApiWrapper,
private val syncHelper: SyncHelper,
Expand All @@ -44,7 +42,6 @@ class BatchSyncRunner(

) : Runner {
private val logger = LoggerFactory.getLogger(this::class.java)
private val timeZone = TimeZone.getTimeZone(timeZoneString)

override fun run() {
val allPlaidTxs = mutableMapOf<PlaidAccessToken, MutableList<Transaction>>()
Expand Down Expand Up @@ -202,7 +199,7 @@ class BatchSyncRunner(
}

val earliestTimestamp = txs.fold(OffsetDateTime.now()) { acc, tx ->
val ts = tx.getTimestamp(timeZone.toZoneId())
val ts = converter.getTxTimestamp(tx)
if (ts < acc) {
ts
} else {
Expand Down Expand Up @@ -235,4 +232,4 @@ class BatchSyncRunner(
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import net.djvk.fireflyPlaidConnector2.api.firefly.apis.TransactionsApi
import net.djvk.fireflyPlaidConnector2.api.firefly.models.TransactionRead
import net.djvk.fireflyPlaidConnector2.api.firefly.models.TransactionTypeFilter
import net.djvk.fireflyPlaidConnector2.api.plaid.PlaidApiWrapper
import net.djvk.fireflyPlaidConnector2.api.plaid.PlaidTransactionId
import net.djvk.fireflyPlaidConnector2.api.plaid.models.*
import net.djvk.fireflyPlaidConnector2.transactions.TransactionConverter
import org.slf4j.LoggerFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,13 @@ data class FireflyTransactionDto(
*/
val id: FireflyTransactionId?,
val tx: TransactionSplit,
) : SortableTransaction {
override val transactionId: String
) {
val transactionId: String
get() = id ?: throw RuntimeException("Can't use a Firefly transaction without an id for sorting")

override val amount: Double
val amount: Double
get() = TransactionConverter.getPlaidAmount(this)

override fun getTimestamp(zoneId: ZoneId): OffsetDateTime {
return tx.date
}

override fun getFireflyAccountId(accountMap: Map<PlaidAccountId, FireflyAccountId>): FireflyAccountId {
return when (tx.type) {
TransactionTypeProperty.deposit -> tx.destinationId?.toInt()
?: throw IllegalArgumentException("SortableTransaction.getFireflyAccountId can't be called on a deposit " +
"with a null destination id")
TransactionTypeProperty.withdrawal -> tx.sourceId?.toInt()
?: throw IllegalArgumentException("SortableTransaction.getFireflyAccountId can't be called on a withdrawal " +
"with a null source id")
else -> throw IllegalArgumentException("SortableTransaction.getFireflyAccountId isn't valid to call on " +
"FireflyTransactionDtos that are not withdrawals or deposits")
}
}

fun toTransactionStore(): TransactionStore {
return TransactionStore(
listOf(tx),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package net.djvk.fireflyPlaidConnector2.transactions

import net.djvk.fireflyPlaidConnector2.api.firefly.apis.FireflyExternalId
import net.djvk.fireflyPlaidConnector2.api.firefly.apis.FireflyTransactionId
import net.djvk.fireflyPlaidConnector2.api.firefly.models.TransactionRead
import net.djvk.fireflyPlaidConnector2.api.plaid.models.PlaidTransactionId
import net.djvk.fireflyPlaidConnector2.api.plaid.PlaidTransactionId

class FireflyTransactionExternalIdIndexer(
existingFireflyTxs: List<TransactionRead>,
Expand All @@ -30,8 +29,8 @@ class FireflyTransactionExternalIdIndexer(
}

companion object {
fun getExternalId(txId: PlaidTransactionId): String {
fun getExternalId(txId: String): PlaidTransactionId {
return "plaid-${txId}"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.djvk.fireflyPlaidConnector2.api.plaid.models
package net.djvk.fireflyPlaidConnector2.transactions

import net.djvk.fireflyPlaidConnector2.api.plaid.models.PersonalFinanceCategory
import net.djvk.fireflyPlaidConnector2.constants.Direction

/**
Expand Down Expand Up @@ -152,6 +153,25 @@ enum class PersonalFinanceCategoryEnum(val primary: Primary, val detailed: Detai
RENT_AND_UTILITIES_OTHER_UTILITIES(Primary.RENT_AND_UTILITIES, RentAndUtilitiesDetailed.OTHER_UTILITIES),
OTHER(Primary.OTHER, OtherDetailed.OTHER);

companion object {
fun from(categoryModel: PersonalFinanceCategory): PersonalFinanceCategoryEnum {
// Special case to handle what I believe is a Plaid bug I saw in the wild
if (categoryModel.primary == Primary.TRAVEL.name &&
categoryModel.detailed == TRANSPORTATION_PUBLIC_TRANSIT.name) {
return TRANSPORTATION_PUBLIC_TRANSIT
}
// Business as usual
return values().find {
it.primary.name == categoryModel.primary && it.name == categoryModel.detailed
}
?: throw IllegalArgumentException("Failed to convert personal finance category $categoryModel to enum")
}
}

fun toPersonalFinanceCategory(): PersonalFinanceCategory {
return PersonalFinanceCategory(this.primary.name, this.name)
}

interface Detailed {
val description: String
val name: String
Expand Down Expand Up @@ -333,5 +353,3 @@ enum class PersonalFinanceCategoryEnum(val primary: Primary, val detailed: Detai
OTHER("Other"),
}
}


Loading