1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

add fix etl and test Teller sandbox

This commit is contained in:
Tyler Myracle 2024-01-16 22:03:50 -06:00
parent 97dc37fad1
commit 2575ccf311
8 changed files with 45 additions and 44 deletions

View file

@ -57,4 +57,4 @@ NX_POSTMARK_API_TOKEN=
######################################################################## ########################################################################
NX_PLAID_SECRET= NX_PLAID_SECRET=
NX_FINICITY_APP_KEY= NX_FINICITY_APP_KEY=
NX_FINICITY_PARTNER_SECRET= NX_FINICITY_PARTNER_SECRET=

View file

@ -140,7 +140,7 @@ syncInstitutionQueue.add(
'sync-teller-institutions', 'sync-teller-institutions',
{}, {},
{ {
repeat: { cron: '* */24 * * *' }, // Run every 24 hours repeat: { cron: '0 0 */1 * *' }, // Run every 24 hours
} }
) )

View file

@ -6,6 +6,7 @@ import type { SharedType } from '@maybe-finance/shared'
import { invalidateAccountQueries } from '../utils' import { invalidateAccountQueries } from '../utils'
import type { AxiosInstance } from 'axios' import type { AxiosInstance } from 'axios'
import type { TellerTypes } from '@maybe-finance/teller-api' import type { TellerTypes } from '@maybe-finance/teller-api'
import { useAccountConnectionApi } from './useAccountConnectionApi'
type TellerInstitution = { type TellerInstitution = {
name: string name: string
@ -30,6 +31,9 @@ export function useTellerApi() {
const { axios } = useAxiosWithAuth() const { axios } = useAxiosWithAuth()
const api = useMemo(() => TellerApi(axios), [axios]) const api = useMemo(() => TellerApi(axios), [axios])
const { useSyncConnection } = useAccountConnectionApi()
const syncConnection = useSyncConnection()
const addConnectionToState = (connection: SharedType.AccountConnection) => { const addConnectionToState = (connection: SharedType.AccountConnection) => {
const accountsData = queryClient.getQueryData<SharedType.AccountsResponse>(['accounts']) const accountsData = queryClient.getQueryData<SharedType.AccountsResponse>(['accounts'])
if (!accountsData) if (!accountsData)
@ -50,6 +54,7 @@ export function useTellerApi() {
useMutation(api.handleEnrollment, { useMutation(api.handleEnrollment, {
onSuccess: (_connection) => { onSuccess: (_connection) => {
addConnectionToState(_connection) addConnectionToState(_connection)
syncConnection.mutate(_connection.id)
toast.success(`Account connection added!`) toast.success(`Account connection added!`)
}, },
onSettled: () => { onSettled: () => {

View file

@ -58,7 +58,7 @@ export const useTellerConnect = (options: TellerConnectOptions, logger: Logger)
{ {
...options, ...options,
onSuccess: async (enrollment: TellerConnectEnrollment) => { onSuccess: async (enrollment: TellerConnectEnrollment) => {
logger.debug(`User enrolled successfully`, enrollment) logger.debug('User enrolled successfully')
try { try {
await handleEnrollment.mutateAsync({ await handleEnrollment.mutateAsync({
institution: { institution: {

View file

@ -1,6 +1,6 @@
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, type SharedType } from '@maybe-finance/shared'
import type { TellerApi, TellerTypes } from '@maybe-finance/teller-api' import type { TellerApi, TellerTypes } from '@maybe-finance/teller-api'
import { DbUtil, TellerUtil, type IETL, type ICryptoService } 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'
@ -117,8 +117,6 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
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: {
@ -132,12 +130,13 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
categoryProvider: TellerUtil.tellerTypesToCategory(tellerAccount.type), categoryProvider: TellerUtil.tellerTypesToCategory(tellerAccount.type),
subcategoryProvider: tellerAccount.subtype ?? 'other', subcategoryProvider: tellerAccount.subtype ?? 'other',
accountConnectionId: connection.id, accountConnectionId: connection.id,
userId: connection.userId,
tellerAccountId: tellerAccount.id, tellerAccountId: tellerAccount.id,
name: tellerAccount.name, name: tellerAccount.name,
tellerType: tellerAccount.type, tellerType: tellerAccount.type,
tellerSubtype: tellerAccount.subtype, tellerSubtype: tellerAccount.subtype,
mask: tellerAccount.last_four, mask: tellerAccount.last_four,
...TellerUtil.getAccountBalanceData(tellerAccount, classification), ...TellerUtil.getAccountBalanceData(tellerAccount),
}, },
update: { update: {
type: TellerUtil.getType(tellerAccount.type), type: TellerUtil.getType(tellerAccount.type),
@ -145,7 +144,7 @@ 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(TellerUtil.getAccountBalanceData(tellerAccount, classification), [ ..._.omit(TellerUtil.getAccountBalanceData(tellerAccount), [
'currentBalanceStrategy', 'currentBalanceStrategy',
'availableBalanceStrategy', 'availableBalanceStrategy',
]), ]),
@ -226,13 +225,13 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
} AND teller_account_id = ${account_id.toString()}), } AND teller_account_id = ${account_id.toString()}),
${id}, ${id},
${date}::date, ${date}::date,
${[description].filter(Boolean).join(' ')}, ${description},
${DbUtil.toDecimal(-amount)}, ${DbUtil.toDecimal(-amount)},
${status === 'pending'}, ${status === 'pending'},
${'USD'}, ${'USD'},
${details.counterparty.name ?? ''}, ${details.counterparty.name ?? ''},
${type}, ${type},
${details.category ?? ''}, ${details.category ?? ''}
)` )`
}) })
)} )}

View file

@ -45,6 +45,7 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr
where: { id: connection.id }, where: { id: connection.id },
data: { data: {
status: 'OK', status: 'OK',
syncStatus: 'IDLE',
}, },
}) })
break break
@ -157,21 +158,6 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr
throw new Error('USD_ONLY') throw new Error('USD_ONLY')
} }
// Create account connection on exchange; accounts + txns will sync later with webhook
const [accountConnection] = await this.prisma.$transaction([
this.prisma.accountConnection.create({
data: {
name: enrollment.enrollment.institution.name,
type: 'teller' as SharedType.AccountConnectionType,
tellerEnrollmentId: enrollment.enrollment.id,
tellerInstitutionId: institution.id,
tellerAccessToken: this.crypto.encrypt(enrollment.accessToken),
userId,
syncStatus: 'PENDING',
},
}),
])
await this.prisma.user.update({ await this.prisma.user.update({
where: { id: userId }, where: { id: userId },
data: { data: {
@ -179,6 +165,28 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr
}, },
}) })
const accountConnection = await this.prisma.accountConnection.create({
data: {
name: enrollment.enrollment.institution.name,
type: 'teller' as SharedType.AccountConnectionType,
tellerEnrollmentId: enrollment.enrollment.id,
tellerInstitutionId: institution.id,
tellerAccessToken: this.crypto.encrypt(enrollment.accessToken),
userId,
syncStatus: 'PENDING',
},
})
await this.sync(accountConnection, { type: 'teller', initialSync: true })
await this.prisma.accountConnection.update({
where: { id: accountConnection.id },
data: {
status: 'OK',
syncStatus: 'IDLE',
},
})
return accountConnection return accountConnection
} }
} }

View file

@ -1,10 +1,4 @@
import { import { Prisma, AccountCategory, AccountType, type Account } from '@prisma/client'
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'
@ -13,10 +7,10 @@ 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( export function getAccountBalanceData({
{ balances, currency }: Pick<TellerTypes.AccountWithBalances, 'balances' | 'currency'>, balance,
classification: AccountClassification currency,
): Pick< }: Pick<TellerTypes.AccountWithBalances, 'balance' | 'currency'>): Pick<
Account, Account,
| 'currentBalanceProvider' | 'currentBalanceProvider'
| 'currentBalanceStrategy' | 'currentBalanceStrategy'
@ -24,16 +18,11 @@ export function getAccountBalanceData(
| 'availableBalanceStrategy' | 'availableBalanceStrategy'
| 'currencyCode' | 'currencyCode'
> { > {
// Flip balance values to positive for liabilities
const sign = classification === 'liability' ? -1 : 1
return { return {
currentBalanceProvider: new Prisma.Decimal( currentBalanceProvider: new Prisma.Decimal(balance.ledger ? Number(balance.ledger) : 0),
balances.ledger ? sign * Number(balances.ledger) : 0
),
currentBalanceStrategy: 'current', currentBalanceStrategy: 'current',
availableBalanceProvider: new Prisma.Decimal( availableBalanceProvider: new Prisma.Decimal(
balances.available ? sign * Number(balances.available) : 0 balance.available ? Number(balance.available) : 0
), ),
availableBalanceStrategy: 'available', availableBalanceStrategy: 'available',
currencyCode: currency, currencyCode: currency,

View file

@ -50,7 +50,7 @@ interface CreditAccount extends BaseAccount {
export type Account = DepositoryAccount | CreditAccount export type Account = DepositoryAccount | CreditAccount
export type AccountWithBalances = Account & { export type AccountWithBalances = Account & {
balances: AccountBalance balance: AccountBalance
} }
export type GetAccountsResponse = Account[] export type GetAccountsResponse = Account[]