mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-10 07:55:21 +02:00
more work
This commit is contained in:
parent
48d167da96
commit
d8fab13d85
3 changed files with 35 additions and 44 deletions
|
@ -1,9 +1,8 @@
|
||||||
import type { AccountConnection, PrismaClient } from '@prisma/client'
|
import type { AccountConnection, PrismaClient } from '@prisma/client'
|
||||||
import type { Logger } from 'winston'
|
import type { Logger } from 'winston'
|
||||||
import { SharedUtil, AccountUtil, type SharedType } from '@maybe-finance/shared'
|
import { SharedUtil, AccountUtil, type SharedType } from '@maybe-finance/shared'
|
||||||
import type { FinicityApi, FinicityTypes } from '@maybe-finance/finicity-api'
|
|
||||||
import type { TellerApi, TellerTypes } from '@maybe-finance/teller-api'
|
import type { TellerApi, TellerTypes } from '@maybe-finance/teller-api'
|
||||||
import { DbUtil, TellerUtil, type IETL } from '@maybe-finance/server/shared'
|
import { DbUtil, TellerUtil, type IETL, type ICryptoService } from '@maybe-finance/server/shared'
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
|
@ -20,19 +19,28 @@ export type TellerData = {
|
||||||
transactionsDateRange: SharedType.DateRange<DateTime>
|
transactionsDateRange: SharedType.DateRange<DateTime>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Connection = Pick<AccountConnection, 'id' | 'userId' | 'tellerInstitutionId'>
|
type Connection = Pick<
|
||||||
|
AccountConnection,
|
||||||
|
'id' | 'userId' | 'tellerInstitutionId' | 'tellerAccessToken'
|
||||||
|
>
|
||||||
|
|
||||||
export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
private readonly prisma: PrismaClient,
|
private readonly prisma: PrismaClient,
|
||||||
private readonly teller: Pick<TellerApi, 'getAccounts' | 'getTransactions'>
|
private readonly teller: Pick<TellerApi, 'getAccounts' | 'getTransactions'>,
|
||||||
|
private readonly crypto: ICryptoService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async extract(connection: Connection): Promise<TellerRawData> {
|
async extract(connection: Connection): Promise<TellerRawData> {
|
||||||
if (!connection.tellerInstitutionId) {
|
if (!connection.tellerInstitutionId) {
|
||||||
throw new Error(`connection ${connection.id} is missing tellerInstitutionId`)
|
throw new Error(`connection ${connection.id} is missing tellerInstitutionId`)
|
||||||
}
|
}
|
||||||
|
if (!connection.tellerAccessToken) {
|
||||||
|
throw new Error(`connection ${connection.id} is missing tellerAccessToken`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = this.crypto.decrypt(connection.tellerAccessToken)
|
||||||
|
|
||||||
const user = await this.prisma.user.findUniqueOrThrow({
|
const user = await this.prisma.user.findUniqueOrThrow({
|
||||||
where: { id: connection.userId },
|
where: { id: connection.userId },
|
||||||
|
@ -52,12 +60,11 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
||||||
end: DateTime.now(),
|
end: DateTime.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = await this._extractAccounts(user.tellerUserId)
|
const accounts = await this._extractAccounts(accessToken)
|
||||||
|
|
||||||
const transactions = await this._extractTransactions(
|
const transactions = await this._extractTransactions(
|
||||||
user.tellerUserId,
|
accessToken,
|
||||||
accounts.map((a) => a.id),
|
accounts.map((a) => a.id)
|
||||||
transactionsDateRange
|
|
||||||
)
|
)
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
|
@ -89,12 +96,8 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _extractAccounts(tellerUserId: string) {
|
private async _extractAccounts(accessToken: string) {
|
||||||
const { accounts } = await this.teller.getAccounts({ accessToken: undefined })
|
return await this.teller.getAccounts({ accessToken })
|
||||||
|
|
||||||
return accounts.filter(
|
|
||||||
(a) => a.institutionLoginId.toString() === institutionLoginId && a.currency === 'USD'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _loadAccounts(connection: Connection, { accounts }: Pick<TellerData, 'accounts'>) {
|
private _loadAccounts(connection: Connection, { accounts }: Pick<TellerData, 'accounts'>) {
|
||||||
|
@ -111,36 +114,30 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
||||||
create: {
|
create: {
|
||||||
type: TellerUtil.getType(tellerAccount.type),
|
type: TellerUtil.getType(tellerAccount.type),
|
||||||
provider: 'teller',
|
provider: 'teller',
|
||||||
categoryProvider: PlaidUtil.plaidTypesToCategory(plaidAccount.type),
|
categoryProvider: TellerUtil.tellerTypesToCategory(tellerAccount.type),
|
||||||
subcategoryProvider: plaidAccount.subtype ?? 'other',
|
subcategoryProvider: tellerAccount.subtype ?? 'other',
|
||||||
accountConnectionId: connection.id,
|
accountConnectionId: connection.id,
|
||||||
plaidAccountId: plaidAccount.account_id,
|
tellerAccountId: tellerAccount.id,
|
||||||
name: tellerAccount.name,
|
name: tellerAccount.name,
|
||||||
plaidType: tellerAccount.type,
|
tellerType: tellerAccount.type,
|
||||||
plaidSubtype: tellerAccount.subtype,
|
tellerSubtype: tellerAccount.subtype,
|
||||||
mask: plaidAccount.mask,
|
mask: tellerAccount.last_four,
|
||||||
...PlaidUtil.getAccountBalanceData(
|
...TellerUtil.getAccountBalanceData(tellerAccount, tellerAccount.type),
|
||||||
plaidAccount.balances,
|
|
||||||
plaidAccount.type
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
type: TellerUtil.getType(tellerAccount.type),
|
type: TellerUtil.getType(tellerAccount.type),
|
||||||
categoryProvider: PlaidUtil.plaidTypesToCategory(tellerAccount.type),
|
categoryProvider: TellerUtil.tellerTypesToCategory(tellerAccount.type),
|
||||||
subcategoryProvider: tellerAccount.subtype ?? 'other',
|
subcategoryProvider: tellerAccount.subtype ?? 'other',
|
||||||
plaidType: tellerAccount.type,
|
tellerType: tellerAccount.type,
|
||||||
plaidSubtype: tellerAccount.subtype,
|
tellerSubtype: tellerAccount.subtype,
|
||||||
..._.omit(
|
..._.omit(
|
||||||
PlaidUtil.getAccountBalanceData(
|
TellerUtil.getAccountBalanceData(tellerAccount, tellerAccount.type),
|
||||||
plaidAccount.balances,
|
|
||||||
plaidAccount.type
|
|
||||||
),
|
|
||||||
['currentBalanceStrategy', 'availableBalanceStrategy']
|
['currentBalanceStrategy', 'availableBalanceStrategy']
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
// any accounts that are no longer in Plaid should be marked inactive
|
// any accounts that are no longer in Teller should be marked inactive
|
||||||
this.prisma.account.updateMany({
|
this.prisma.account.updateMany({
|
||||||
where: {
|
where: {
|
||||||
accountConnectionId: connection.id,
|
accountConnectionId: connection.id,
|
||||||
|
@ -156,25 +153,17 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _extractTransactions(
|
private async _extractTransactions(accessToken: string, accountIds: string[]) {
|
||||||
customerId: string,
|
|
||||||
accountIds: string[],
|
|
||||||
dateRange: SharedType.DateRange<DateTime>
|
|
||||||
) {
|
|
||||||
const accountTransactions = await Promise.all(
|
const accountTransactions = await Promise.all(
|
||||||
accountIds.map((accountId) =>
|
accountIds.map((accountId) =>
|
||||||
SharedUtil.paginate({
|
SharedUtil.paginate({
|
||||||
pageSize: 1000, // https://api-reference.finicity.com/#/rest/api-endpoints/transactions/get-customer-account-transactions
|
pageSize: 1000, // TODO: Check with Teller on max page size
|
||||||
fetchData: async (offset, count) => {
|
fetchData: async (offset, count) => {
|
||||||
const transactions = await SharedUtil.withRetry(
|
const transactions = await SharedUtil.withRetry(
|
||||||
() =>
|
() =>
|
||||||
this.teller.getTransactions({
|
this.teller.getTransactions({
|
||||||
accountId,
|
accountId,
|
||||||
accessToken: undefined,
|
accessToken: accessToken,
|
||||||
fromDate: dateRange.start.toUnixInteger(),
|
|
||||||
toDate: dateRange.end.toUnixInteger(),
|
|
||||||
start: offset + 1,
|
|
||||||
limit: count,
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
maxRetries: 3,
|
maxRetries: 3,
|
||||||
|
|
|
@ -48,7 +48,7 @@ interface CreditAccount extends BaseAccount {
|
||||||
|
|
||||||
export type Account = DepositoryAccount | CreditAccount
|
export type Account = DepositoryAccount | CreditAccount
|
||||||
|
|
||||||
export type GetAccountsResponse = { accounts: Account[] }
|
export type GetAccountsResponse = Account[]
|
||||||
export type GetAccountResponse = Account
|
export type GetAccountResponse = Account
|
||||||
export type DeleteAccountResponse = void
|
export type DeleteAccountResponse = void
|
||||||
|
|
||||||
|
|
|
@ -109,6 +109,7 @@ enum AccountProvider {
|
||||||
user
|
user
|
||||||
plaid
|
plaid
|
||||||
finicity
|
finicity
|
||||||
|
teller
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AccountBalanceStrategy {
|
enum AccountBalanceStrategy {
|
||||||
|
@ -162,6 +163,7 @@ model Account {
|
||||||
// teller data
|
// teller data
|
||||||
tellerAccountId String? @map("teller_account_id")
|
tellerAccountId String? @map("teller_account_id")
|
||||||
tellerType String? @map("teller_type")
|
tellerType String? @map("teller_type")
|
||||||
|
tellerSubtype String? @map("teller_subtype")
|
||||||
|
|
||||||
// manual account data
|
// manual account data
|
||||||
vehicleMeta Json? @map("vehicle_meta") @db.JsonB
|
vehicleMeta Json? @map("vehicle_meta") @db.JsonB
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue