1
0
Fork 0
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:
Tyler Myracle 2024-01-15 19:00:47 -06:00
parent 48d167da96
commit d8fab13d85
3 changed files with 35 additions and 44 deletions

View file

@ -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,

View file

@ -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

View file

@ -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