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:
parent
823eab2c11
commit
ad30b02ebc
3 changed files with 63 additions and 10 deletions
|
@ -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({
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue