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:
parent
97dc37fad1
commit
2575ccf311
8 changed files with 45 additions and 44 deletions
|
@ -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=
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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: () => {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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 ?? ''}
|
||||||
)`
|
)`
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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[]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue