1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-10 07:55:21 +02:00

finish initial pass on teller etl service

This commit is contained in:
Tyler Myracle 2024-01-15 20:28:35 -06:00
parent 823eab2c11
commit ad30b02ebc
3 changed files with 63 additions and 10 deletions

View file

@ -14,7 +14,7 @@ export type TellerRawData = {
} }
export type TellerData = { export type TellerData = {
accounts: TellerTypes.Account[] accounts: TellerTypes.AccountWithBalances[]
transactions: TellerTypes.Transaction[] transactions: TellerTypes.Transaction[]
transactionsDateRange: SharedType.DateRange<DateTime> transactionsDateRange: SharedType.DateRange<DateTime>
} }
@ -28,7 +28,10 @@ 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' | 'getAccountBalances'
>,
private readonly crypto: ICryptoService private readonly crypto: ICryptoService
) {} ) {}
@ -97,13 +100,25 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
} }
private async _extractAccounts(accessToken: string) { private async _extractAccounts(accessToken: string) {
return await this.teller.getAccounts({ accessToken }) const accounts = await this.teller.getAccounts({ accessToken })
const accountsWithBalances = await Promise.all(
accounts.map(async (a) => {
const balance = await this.teller.getAccountBalances({
accountId: a.id,
accessToken,
})
return { ...a, balance }
})
)
return accountsWithBalances
} }
private _loadAccounts(connection: Connection, { accounts }: Pick<TellerData, 'accounts'>) { private _loadAccounts(connection: Connection, { accounts }: Pick<TellerData, 'accounts'>) {
return [ return [
// upsert accounts // upsert accounts
...accounts.map((tellerAccount) => { ...accounts.map((tellerAccount) => {
const type = TellerUtil.getType(tellerAccount.type)
const classification = AccountUtil.getClassification(type)
return this.prisma.account.upsert({ return this.prisma.account.upsert({
where: { where: {
accountConnectionId_tellerAccountId: { accountConnectionId_tellerAccountId: {
@ -122,7 +137,7 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
tellerType: tellerAccount.type, tellerType: tellerAccount.type,
tellerSubtype: tellerAccount.subtype, tellerSubtype: tellerAccount.subtype,
mask: tellerAccount.last_four, mask: tellerAccount.last_four,
...TellerUtil.getAccountBalanceData(tellerAccount, tellerAccount.type), ...TellerUtil.getAccountBalanceData(tellerAccount, classification),
}, },
update: { update: {
type: TellerUtil.getType(tellerAccount.type), type: TellerUtil.getType(tellerAccount.type),
@ -130,10 +145,10 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
subcategoryProvider: tellerAccount.subtype ?? 'other', subcategoryProvider: tellerAccount.subtype ?? 'other',
tellerType: tellerAccount.type, tellerType: tellerAccount.type,
tellerSubtype: tellerAccount.subtype, tellerSubtype: tellerAccount.subtype,
..._.omit( ..._.omit(TellerUtil.getAccountBalanceData(tellerAccount, classification), [
TellerUtil.getAccountBalanceData(tellerAccount, tellerAccount.type), 'currentBalanceStrategy',
['currentBalanceStrategy', 'availableBalanceStrategy'] 'availableBalanceStrategy',
), ]),
}, },
}) })
}), }),
@ -158,7 +173,7 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
accountIds.map((accountId) => accountIds.map((accountId) =>
SharedUtil.paginate({ SharedUtil.paginate({
pageSize: 1000, // TODO: Check with Teller on max page size pageSize: 1000, // TODO: Check with Teller on max page size
fetchData: async (offset, count) => { fetchData: async () => {
const transactions = await SharedUtil.withRetry( const transactions = await SharedUtil.withRetry(
() => () =>
this.teller.getTransactions({ this.teller.getTransactions({

View file

@ -1,4 +1,10 @@
import { AccountCategory, AccountType } from '@prisma/client' import {
Prisma,
AccountCategory,
AccountType,
type AccountClassification,
type Account,
} from '@prisma/client'
import type { TellerTypes } from '@maybe-finance/teller-api' import type { TellerTypes } from '@maybe-finance/teller-api'
import { Duration } from 'luxon' import { Duration } from 'luxon'
@ -7,6 +13,33 @@ import { Duration } from 'luxon'
*/ */
export const TELLER_WINDOW_MAX = Duration.fromObject({ years: 1 }) export const TELLER_WINDOW_MAX = Duration.fromObject({ years: 1 })
export function getAccountBalanceData(
{ balances, currency }: Pick<TellerTypes.AccountWithBalances, 'balances' | 'currency'>,
classification: AccountClassification
): Pick<
Account,
| 'currentBalanceProvider'
| 'currentBalanceStrategy'
| 'availableBalanceProvider'
| 'availableBalanceStrategy'
| 'currencyCode'
> {
// Flip balance values to positive for liabilities
const sign = classification === 'liability' ? -1 : 1
return {
currentBalanceProvider: new Prisma.Decimal(
balances.ledger ? sign * Number(balances.ledger) : 0
),
currentBalanceStrategy: 'current',
availableBalanceProvider: new Prisma.Decimal(
balances.available ? sign * Number(balances.available) : 0
),
availableBalanceStrategy: 'available',
currencyCode: currency,
}
}
export function getType(type: TellerTypes.AccountTypes): AccountType { export function getType(type: TellerTypes.AccountTypes): AccountType {
switch (type) { switch (type) {
case 'depository': case 'depository':

View file

@ -1,4 +1,5 @@
// https://teller.io/docs/api/accounts // https://teller.io/docs/api/accounts
import type { AccountBalance } from './account-balance'
import type { AuthenticatedRequest } from './authentication' import type { AuthenticatedRequest } from './authentication'
export type AccountTypes = 'depository' | 'credit' export type AccountTypes = 'depository' | 'credit'
@ -48,6 +49,10 @@ interface CreditAccount extends BaseAccount {
export type Account = DepositoryAccount | CreditAccount export type Account = DepositoryAccount | CreditAccount
export type AccountWithBalances = Account & {
balances: AccountBalance
}
export type GetAccountsResponse = Account[] export type GetAccountsResponse = Account[]
export type GetAccountResponse = Account export type GetAccountResponse = Account
export type DeleteAccountResponse = void export type DeleteAccountResponse = void