From ad30b02ebcf2a7c2eea95efc46431b7e7fdaf582 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Mon, 15 Jan 2024 20:28:35 -0600 Subject: [PATCH] finish initial pass on teller etl service --- .../src/providers/teller/teller.etl.ts | 33 ++++++++++++----- libs/server/shared/src/utils/teller-utils.ts | 35 ++++++++++++++++++- libs/teller-api/src/types/accounts.ts | 5 +++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/libs/server/features/src/providers/teller/teller.etl.ts b/libs/server/features/src/providers/teller/teller.etl.ts index 52443779..241934bb 100644 --- a/libs/server/features/src/providers/teller/teller.etl.ts +++ b/libs/server/features/src/providers/teller/teller.etl.ts @@ -14,7 +14,7 @@ export type TellerRawData = { } export type TellerData = { - accounts: TellerTypes.Account[] + accounts: TellerTypes.AccountWithBalances[] transactions: TellerTypes.Transaction[] transactionsDateRange: SharedType.DateRange } @@ -28,7 +28,10 @@ export class TellerETL implements IETL { public constructor( private readonly logger: Logger, private readonly prisma: PrismaClient, - private readonly teller: Pick, + private readonly teller: Pick< + TellerApi, + 'getAccounts' | 'getTransactions' | 'getAccountBalances' + >, private readonly crypto: ICryptoService ) {} @@ -97,13 +100,25 @@ export class TellerETL implements IETL { } 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) { return [ // upsert accounts ...accounts.map((tellerAccount) => { + const type = TellerUtil.getType(tellerAccount.type) + const classification = AccountUtil.getClassification(type) return this.prisma.account.upsert({ where: { accountConnectionId_tellerAccountId: { @@ -122,7 +137,7 @@ export class TellerETL implements IETL { tellerType: tellerAccount.type, tellerSubtype: tellerAccount.subtype, mask: tellerAccount.last_four, - ...TellerUtil.getAccountBalanceData(tellerAccount, tellerAccount.type), + ...TellerUtil.getAccountBalanceData(tellerAccount, classification), }, update: { type: TellerUtil.getType(tellerAccount.type), @@ -130,10 +145,10 @@ export class TellerETL implements IETL { subcategoryProvider: tellerAccount.subtype ?? 'other', tellerType: tellerAccount.type, tellerSubtype: tellerAccount.subtype, - ..._.omit( - TellerUtil.getAccountBalanceData(tellerAccount, tellerAccount.type), - ['currentBalanceStrategy', 'availableBalanceStrategy'] - ), + ..._.omit(TellerUtil.getAccountBalanceData(tellerAccount, classification), [ + 'currentBalanceStrategy', + 'availableBalanceStrategy', + ]), }, }) }), @@ -158,7 +173,7 @@ export class TellerETL implements IETL { accountIds.map((accountId) => SharedUtil.paginate({ pageSize: 1000, // TODO: Check with Teller on max page size - fetchData: async (offset, count) => { + fetchData: async () => { const transactions = await SharedUtil.withRetry( () => this.teller.getTransactions({ diff --git a/libs/server/shared/src/utils/teller-utils.ts b/libs/server/shared/src/utils/teller-utils.ts index 58ce8b88..a8e8e3ee 100644 --- a/libs/server/shared/src/utils/teller-utils.ts +++ b/libs/server/shared/src/utils/teller-utils.ts @@ -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 { Duration } from 'luxon' @@ -7,6 +13,33 @@ import { Duration } from 'luxon' */ export const TELLER_WINDOW_MAX = Duration.fromObject({ years: 1 }) +export function getAccountBalanceData( + { balances, currency }: Pick, + 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 { switch (type) { case 'depository': diff --git a/libs/teller-api/src/types/accounts.ts b/libs/teller-api/src/types/accounts.ts index 594fb6a5..5df29953 100644 --- a/libs/teller-api/src/types/accounts.ts +++ b/libs/teller-api/src/types/accounts.ts @@ -1,4 +1,5 @@ // https://teller.io/docs/api/accounts +import type { AccountBalance } from './account-balance' import type { AuthenticatedRequest } from './authentication' export type AccountTypes = 'depository' | 'credit' @@ -48,6 +49,10 @@ interface CreditAccount extends BaseAccount { export type Account = DepositoryAccount | CreditAccount +export type AccountWithBalances = Account & { + balances: AccountBalance +} + export type GetAccountsResponse = Account[] export type GetAccountResponse = Account export type DeleteAccountResponse = void