mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 07:25:19 +02:00
stub teller backend
This commit is contained in:
parent
ee0dc9d4be
commit
53736fdafb
32 changed files with 614 additions and 7 deletions
|
@ -21,4 +21,8 @@ NX_FINICITY_APP_KEY=
|
||||||
NX_FINICITY_PARTNER_SECRET=
|
NX_FINICITY_PARTNER_SECRET=
|
||||||
NX_CONVERTKIT_SECRET=
|
NX_CONVERTKIT_SECRET=
|
||||||
|
|
||||||
|
# Teller API keys (https://teller.io)
|
||||||
|
NX_TELLER_SIGNING_SECRET=
|
||||||
|
NX_TELLER_APP_ID=
|
||||||
|
|
||||||
NEXT_PUBLIC_ZAPIER_FEEDBACK_HOOK_URL=
|
NEXT_PUBLIC_ZAPIER_FEEDBACK_HOOK_URL=
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -53,4 +53,5 @@ Thumbs.db
|
||||||
migrations.json
|
migrations.json
|
||||||
|
|
||||||
# Shouldn't happen, but backup since we have a script that generates these locally
|
# Shouldn't happen, but backup since we have a script that generates these locally
|
||||||
*.pem
|
*.pem
|
||||||
|
certs/
|
||||||
|
|
|
@ -42,6 +42,8 @@ import {
|
||||||
InstitutionProviderFactory,
|
InstitutionProviderFactory,
|
||||||
FinicityWebhookHandler,
|
FinicityWebhookHandler,
|
||||||
PlaidWebhookHandler,
|
PlaidWebhookHandler,
|
||||||
|
TellerService,
|
||||||
|
TellerWebhookHandler,
|
||||||
InsightService,
|
InsightService,
|
||||||
SecurityPricingService,
|
SecurityPricingService,
|
||||||
TransactionService,
|
TransactionService,
|
||||||
|
@ -55,6 +57,7 @@ import { SharedType } from '@maybe-finance/shared'
|
||||||
import prisma from './prisma'
|
import prisma from './prisma'
|
||||||
import plaid, { getPlaidWebhookUrl } from './plaid'
|
import plaid, { getPlaidWebhookUrl } from './plaid'
|
||||||
import finicity, { getFinicityTxPushUrl, getFinicityWebhookUrl } from './finicity'
|
import finicity, { getFinicityTxPushUrl, getFinicityWebhookUrl } from './finicity'
|
||||||
|
import teller, { getTellerWebhookUrl } from './teller'
|
||||||
import stripe from './stripe'
|
import stripe from './stripe'
|
||||||
import postmark from './postmark'
|
import postmark from './postmark'
|
||||||
import defineAbilityFor from './ability'
|
import defineAbilityFor from './ability'
|
||||||
|
@ -142,6 +145,14 @@ const finicityService = new FinicityService(
|
||||||
env.NX_FINICITY_ENV === 'sandbox'
|
env.NX_FINICITY_ENV === 'sandbox'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tellerService = new TellerService(
|
||||||
|
logger.child({ service: 'TellerService' }),
|
||||||
|
prisma,
|
||||||
|
teller,
|
||||||
|
getTellerWebhookUrl(),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
// account-connection
|
// account-connection
|
||||||
|
|
||||||
const accountConnectionProviderFactory = new AccountConnectionProviderFactory({
|
const accountConnectionProviderFactory = new AccountConnectionProviderFactory({
|
||||||
|
@ -278,6 +289,13 @@ const stripeWebhooks = new StripeWebhookHandler(
|
||||||
stripe
|
stripe
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tellerWebhooks = new TellerWebhookHandler(
|
||||||
|
logger.child({ service: 'TellerWebhookHandler' }),
|
||||||
|
prisma,
|
||||||
|
teller,
|
||||||
|
accountConnectionService
|
||||||
|
)
|
||||||
|
|
||||||
// helper function for parsing JWT and loading User record
|
// helper function for parsing JWT and loading User record
|
||||||
// TODO: update this with roles, identity, and metadata
|
// TODO: update this with roles, identity, and metadata
|
||||||
async function getCurrentUser(jwt: NonNullable<Request['user']>) {
|
async function getCurrentUser(jwt: NonNullable<Request['user']>) {
|
||||||
|
@ -334,6 +352,8 @@ export async function createContext(req: Request) {
|
||||||
finicityService,
|
finicityService,
|
||||||
finicityWebhooks,
|
finicityWebhooks,
|
||||||
stripeWebhooks,
|
stripeWebhooks,
|
||||||
|
tellerService,
|
||||||
|
tellerWebhooks,
|
||||||
insightService,
|
insightService,
|
||||||
marketDataService,
|
marketDataService,
|
||||||
planService,
|
planService,
|
||||||
|
|
11
apps/server/src/app/lib/teller.ts
Normal file
11
apps/server/src/app/lib/teller.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { TellerApi } from '@maybe-finance/teller-api'
|
||||||
|
import { getWebhookUrl } from './webhook'
|
||||||
|
|
||||||
|
const teller = new TellerApi()
|
||||||
|
|
||||||
|
export default teller
|
||||||
|
|
||||||
|
export async function getTellerWebhookUrl() {
|
||||||
|
const webhookUrl = await getWebhookUrl()
|
||||||
|
return `${webhookUrl}/v1/teller/webhook`
|
||||||
|
}
|
|
@ -5,5 +5,6 @@ export * from './superjson'
|
||||||
export * from './validate-auth-jwt'
|
export * from './validate-auth-jwt'
|
||||||
export * from './validate-plaid-jwt'
|
export * from './validate-plaid-jwt'
|
||||||
export * from './validate-finicity-signature'
|
export * from './validate-finicity-signature'
|
||||||
|
export * from './validate-teller-signature'
|
||||||
export { default as maintenance } from './maintenance'
|
export { default as maintenance } from './maintenance'
|
||||||
export * from './identify-user'
|
export * from './identify-user'
|
||||||
|
|
50
apps/server/src/app/middleware/validate-teller-signature.ts
Normal file
50
apps/server/src/app/middleware/validate-teller-signature.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import type { RequestHandler } from 'express'
|
||||||
|
import type { TellerTypes } from '@maybe-finance/teller-api'
|
||||||
|
import env from '../../env'
|
||||||
|
|
||||||
|
// https://teller.io/docs/api/webhooks#verifying-messages
|
||||||
|
export const validateTellerSignature: RequestHandler = (req, res, next) => {
|
||||||
|
const signatureHeader = req.headers['teller-signature'] as string | undefined
|
||||||
|
|
||||||
|
if (!signatureHeader) {
|
||||||
|
return res.status(401).send('No Teller-Signature header found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timestamp, signatures } = parseTellerSignatureHeader(signatureHeader)
|
||||||
|
const threeMinutesAgo = Math.floor(Date.now() / 1000) - 3 * 60
|
||||||
|
|
||||||
|
if (parseInt(timestamp) < threeMinutesAgo) {
|
||||||
|
return res.status(408).send('Signature timestamp is too old')
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedMessage = `${timestamp}.${JSON.stringify(req.body as TellerTypes.WebhookData)}`
|
||||||
|
const expectedSignature = createHmacSha256(signedMessage, env.NX_TELLER_SIGNING_SECRET)
|
||||||
|
|
||||||
|
if (!signatures.includes(expectedSignature)) {
|
||||||
|
return res.status(401).send('Invalid webhook signature')
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseTellerSignatureHeader = (
|
||||||
|
header: string
|
||||||
|
): { timestamp: string; signatures: string[] } => {
|
||||||
|
const parts = header.split(',')
|
||||||
|
const timestampPart = parts.find((p) => p.startsWith('t='))
|
||||||
|
const signatureParts = parts.filter((p) => p.startsWith('v1='))
|
||||||
|
|
||||||
|
if (!timestampPart) {
|
||||||
|
throw new Error('No timestamp in Teller-Signature header')
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = timestampPart.split('=')[1]
|
||||||
|
const signatures = signatureParts.map((p) => p.split('=')[1])
|
||||||
|
|
||||||
|
return { timestamp, signatures }
|
||||||
|
}
|
||||||
|
|
||||||
|
const createHmacSha256 = (message: string, secret: string): string => {
|
||||||
|
return crypto.createHmac('sha256', secret).update(message).digest('hex')
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import type { FinicityTypes } from '@maybe-finance/finicity-api'
|
import type { FinicityTypes } from '@maybe-finance/finicity-api'
|
||||||
import { validatePlaidJwt, validateFinicitySignature } from '../middleware'
|
import { validatePlaidJwt, validateFinicitySignature, validateTellerSignature } from '../middleware'
|
||||||
import endpoint from '../lib/endpoint'
|
import endpoint from '../lib/endpoint'
|
||||||
import stripe from '../lib/stripe'
|
import stripe from '../lib/stripe'
|
||||||
import env from '../../env'
|
import env from '../../env'
|
||||||
|
import type { TellerTypes } from '@maybe-finance/teller-api'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
@ -131,4 +132,42 @@ router.post(
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/teller/webhook',
|
||||||
|
process.env.NODE_ENV !== 'development' ? validateTellerSignature : (_req, _res, next) => next(),
|
||||||
|
endpoint.create({
|
||||||
|
input: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
payload: z.object({
|
||||||
|
enrollment_id: z.string(),
|
||||||
|
reason: z.string(),
|
||||||
|
}),
|
||||||
|
timestamp: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
async resolve({ input, ctx }) {
|
||||||
|
const { type, id, payload } = input
|
||||||
|
|
||||||
|
ctx.logger.info(
|
||||||
|
`rx[teller_webhook] event eventType=${type} eventId=${id} enrollmentId=${payload.enrollment_id}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// May contain sensitive info, only print at the debug level
|
||||||
|
ctx.logger.debug(`rx[teller_webhook] event payload`, input)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('handling webhook')
|
||||||
|
await ctx.tellerWebhooks.handleWebhook(input as TellerTypes.WebhookData)
|
||||||
|
} catch (err) {
|
||||||
|
// record error but don't throw, otherwise Finicity Connect behaves weird
|
||||||
|
ctx.logger.error(`[finicity_webhook] error handling webhook`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 'ok' }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -42,6 +42,9 @@ const envSchema = z.object({
|
||||||
NX_FINICITY_PARTNER_SECRET: z.string(),
|
NX_FINICITY_PARTNER_SECRET: z.string(),
|
||||||
NX_FINICITY_ENV: z.string().default('sandbox'),
|
NX_FINICITY_ENV: z.string().default('sandbox'),
|
||||||
|
|
||||||
|
NX_TELLER_SIGNING_SECRET: z.string().default('REPLACE_THIS'),
|
||||||
|
NX_TELLER_APP_ID: z.string().default('REPLACE_THIS'),
|
||||||
|
|
||||||
NX_SENTRY_DSN: z.string().optional(),
|
NX_SENTRY_DSN: z.string().optional(),
|
||||||
NX_SENTRY_ENV: z.string().optional(),
|
NX_SENTRY_ENV: z.string().optional(),
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,9 @@ const envSchema = z.object({
|
||||||
NX_FINICITY_PARTNER_SECRET: z.string(),
|
NX_FINICITY_PARTNER_SECRET: z.string(),
|
||||||
NX_FINICITY_ENV: z.string().default('sandbox'),
|
NX_FINICITY_ENV: z.string().default('sandbox'),
|
||||||
|
|
||||||
|
NX_TELLER_SIGNING_SECRET: z.string().default('REPLACE_THIS'),
|
||||||
|
NX_TELLER_APP_ID: z.string().default('REPLACE_THIS'),
|
||||||
|
|
||||||
NX_SENTRY_DSN: z.string().optional(),
|
NX_SENTRY_DSN: z.string().optional(),
|
||||||
NX_SENTRY_ENV: z.string().optional(),
|
NX_SENTRY_ENV: z.string().optional(),
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './plaid'
|
export * from './plaid'
|
||||||
export * from './finicity'
|
export * from './finicity'
|
||||||
|
export * from './teller'
|
||||||
export * from './vehicle'
|
export * from './vehicle'
|
||||||
export * from './property'
|
export * from './property'
|
||||||
|
|
2
libs/server/features/src/providers/teller/index.ts
Normal file
2
libs/server/features/src/providers/teller/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './teller.webhook'
|
||||||
|
export * from './teller.service'
|
33
libs/server/features/src/providers/teller/teller.service.ts
Normal file
33
libs/server/features/src/providers/teller/teller.service.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import type { Logger } from 'winston'
|
||||||
|
import type { AccountConnection, PrismaClient, User } from '@prisma/client'
|
||||||
|
import type { IETL, SyncConnectionOptions } from '@maybe-finance/server/shared'
|
||||||
|
import type { IInstitutionProvider } from '../../institution'
|
||||||
|
import type {
|
||||||
|
AccountConnectionSyncEvent,
|
||||||
|
IAccountConnectionProvider,
|
||||||
|
} from '../../account-connection'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { SharedUtil } from '@maybe-finance/shared'
|
||||||
|
import { etl } from '@maybe-finance/server/shared'
|
||||||
|
import type { TellerApi } from '@maybe-finance/teller-api'
|
||||||
|
|
||||||
|
export interface ITellerConnect {
|
||||||
|
generateConnectUrl(userId: User['id'], institutionId: string): Promise<{ link: string }>
|
||||||
|
|
||||||
|
generateFixConnectUrl(
|
||||||
|
userId: User['id'],
|
||||||
|
accountConnectionId: AccountConnection['id']
|
||||||
|
): Promise<{ link: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TellerService {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: Logger,
|
||||||
|
private readonly prisma: PrismaClient,
|
||||||
|
private readonly teller: TellerApi,
|
||||||
|
private readonly webhookUrl: string | Promise<string>,
|
||||||
|
private readonly testMode: boolean
|
||||||
|
) {}
|
||||||
|
}
|
34
libs/server/features/src/providers/teller/teller.webhook.ts
Normal file
34
libs/server/features/src/providers/teller/teller.webhook.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import type { Logger } from 'winston'
|
||||||
|
import type { PrismaClient } from '@prisma/client'
|
||||||
|
import type { TellerApi, TellerTypes } from '@maybe-finance/teller-api'
|
||||||
|
import type { IAccountConnectionService } from '../../account-connection'
|
||||||
|
|
||||||
|
export interface ITellerWebhookHandler {
|
||||||
|
handleWebhook(data: TellerTypes.WebhookData): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TellerWebhookHandler implements ITellerWebhookHandler {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: Logger,
|
||||||
|
private readonly prisma: PrismaClient,
|
||||||
|
private readonly teller: TellerApi,
|
||||||
|
private readonly accountConnectionService: IAccountConnectionService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process Teller webhooks. These handlers should execute as quick as possible and
|
||||||
|
* long-running operations should be performed in the background.
|
||||||
|
*/
|
||||||
|
async handleWebhook(data: TellerTypes.WebhookData) {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'webhook.test': {
|
||||||
|
this.logger.info('Received Teller webhook test')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
this.logger.warn('Unhandled Teller webhook', { data })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import CryptoJS from 'crypto-js'
|
import crypto from 'crypto'
|
||||||
|
|
||||||
export interface ICryptoService {
|
export interface ICryptoService {
|
||||||
encrypt(plainText: string): string
|
encrypt(plainText: string): string
|
||||||
|
@ -6,13 +6,32 @@ export interface ICryptoService {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CryptoService implements ICryptoService {
|
export class CryptoService implements ICryptoService {
|
||||||
constructor(private readonly secret: string) {}
|
private key: Buffer
|
||||||
|
private ivLength = 16 // Initialization vector length. For AES, this is always 16
|
||||||
|
|
||||||
|
constructor(private readonly secret: string) {
|
||||||
|
// Ensure the key length is suitable for AES-256
|
||||||
|
this.key = crypto.createHash('sha256').update(String(this.secret)).digest()
|
||||||
|
}
|
||||||
|
|
||||||
encrypt(plainText: string) {
|
encrypt(plainText: string) {
|
||||||
return CryptoJS.AES.encrypt(plainText, this.secret).toString()
|
const iv = crypto.randomBytes(this.ivLength)
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv)
|
||||||
|
let encrypted = cipher.update(plainText, 'utf8', 'hex')
|
||||||
|
encrypted += cipher.final('hex')
|
||||||
|
|
||||||
|
// Include the IV at the start of the encrypted result
|
||||||
|
return iv.toString('hex') + ':' + encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypt(encrypted: string) {
|
decrypt(encrypted: string) {
|
||||||
return CryptoJS.AES.decrypt(encrypted, this.secret).toString(CryptoJS.enc.Utf8)
|
const textParts = encrypted.split(':')
|
||||||
|
const iv = Buffer.from(textParts.shift()!, 'hex')
|
||||||
|
const encryptedText = textParts.join(':')
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv)
|
||||||
|
let decrypted = decipher.update(encryptedText, 'hex', 'utf8')
|
||||||
|
decrypted += decipher.final('utf8')
|
||||||
|
|
||||||
|
return decrypted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
libs/teller-api/.eslintrc.json
Normal file
18
libs/teller-api/.eslintrc.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"extends": ["../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
libs/teller-api/jest.config.ts
Normal file
16
libs/teller-api/jest.config.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
export default {
|
||||||
|
displayName: 'teller-api',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testEnvironment: 'node',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.[tj]sx?$': 'ts-jest',
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
|
coverageDirectory: '../../coverage/libs/teller-api',
|
||||||
|
}
|
2
libs/teller-api/src/index.ts
Normal file
2
libs/teller-api/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './teller-api'
|
||||||
|
export * as TellerTypes from './types'
|
83
libs/teller-api/src/teller-api.ts
Normal file
83
libs/teller-api/src/teller-api.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||||
|
import type {
|
||||||
|
Account,
|
||||||
|
AccountBalance,
|
||||||
|
AccountDetails,
|
||||||
|
Identity,
|
||||||
|
Transaction,
|
||||||
|
GetAccountResponse,
|
||||||
|
GetAccountsResponse,
|
||||||
|
GetAccountBalancesResponse,
|
||||||
|
GetIdentityResponse,
|
||||||
|
GetTransactionResponse,
|
||||||
|
GetTransactionsResponse,
|
||||||
|
DeleteAccountResponse,
|
||||||
|
GetAccountDetailsResponse,
|
||||||
|
WebhookData,
|
||||||
|
} from './types'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
import axios from 'axios'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as https from 'https'
|
||||||
|
|
||||||
|
const is2xx = (status: number): boolean => status >= 200 && status < 300
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic typed mapping for Teller API
|
||||||
|
*/
|
||||||
|
export class TellerApi {
|
||||||
|
private api: AxiosInstance | null = null
|
||||||
|
|
||||||
|
private async getApi(): Promise<AxiosInstance> {
|
||||||
|
const cert = fs.readFileSync('../../../certs/teller-certificate.pem', 'utf8')
|
||||||
|
const key = fs.readFileSync('../../../certs/teller-private-key.pem', 'utf8')
|
||||||
|
|
||||||
|
const agent = new https.Agent({
|
||||||
|
cert,
|
||||||
|
key,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.api) {
|
||||||
|
this.api = axios.create({
|
||||||
|
httpsAgent: agent,
|
||||||
|
baseURL: `https://api.teller.io`,
|
||||||
|
timeout: 30_000,
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.api
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generic API GET request method */
|
||||||
|
private async get<TResponse>(
|
||||||
|
path: string,
|
||||||
|
params?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<TResponse> {
|
||||||
|
const api = await this.getApi()
|
||||||
|
return api.get<TResponse>(path, { params, ...config }).then(({ data }) => data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generic API POST request method */
|
||||||
|
private async post<TResponse>(
|
||||||
|
path: string,
|
||||||
|
body?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<TResponse> {
|
||||||
|
const api = await this.getApi()
|
||||||
|
return api.post<TResponse>(path, body, config).then(({ data }) => data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generic API DELETE request method */
|
||||||
|
private async delete<TResponse>(
|
||||||
|
path: string,
|
||||||
|
params?: any,
|
||||||
|
config?: AxiosRequestConfig
|
||||||
|
): Promise<TResponse> {
|
||||||
|
const api = await this.getApi()
|
||||||
|
return api.delete<TResponse>(path, { params, ...config }).then(({ data }) => data)
|
||||||
|
}
|
||||||
|
}
|
13
libs/teller-api/src/types/account-balance.ts
Normal file
13
libs/teller-api/src/types/account-balance.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// https://teller.io/docs/api/account/balances
|
||||||
|
|
||||||
|
export type AccountBalance = {
|
||||||
|
account_id: string
|
||||||
|
ledger: string
|
||||||
|
available: string
|
||||||
|
links: {
|
||||||
|
self: string
|
||||||
|
account: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetAccountBalancesResponse = AccountBalance
|
17
libs/teller-api/src/types/account-details.ts
Normal file
17
libs/teller-api/src/types/account-details.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// https://teller.io/docs/api/account/details
|
||||||
|
|
||||||
|
export type AccountDetails = {
|
||||||
|
account_id: string
|
||||||
|
account_number: string
|
||||||
|
links: {
|
||||||
|
account: string
|
||||||
|
self: string
|
||||||
|
}
|
||||||
|
routing_numbers: {
|
||||||
|
ach?: string
|
||||||
|
wire?: string
|
||||||
|
bacs?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetAccountDetailsResponse = AccountDetails
|
47
libs/teller-api/src/types/accounts.ts
Normal file
47
libs/teller-api/src/types/accounts.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// https://teller.io/docs/api/accounts
|
||||||
|
|
||||||
|
export type AccountTypes = 'depository' | 'credit'
|
||||||
|
|
||||||
|
export type DepositorySubtypes =
|
||||||
|
| 'checking'
|
||||||
|
| 'savings'
|
||||||
|
| 'money market'
|
||||||
|
| 'certificate of deposit'
|
||||||
|
| 'treasury'
|
||||||
|
| 'sweep'
|
||||||
|
|
||||||
|
export type CreditSubtype = 'credit_card'
|
||||||
|
|
||||||
|
interface BaseAccount {
|
||||||
|
enrollment_id: string
|
||||||
|
links: {
|
||||||
|
balances: string
|
||||||
|
self: string
|
||||||
|
transactions: string
|
||||||
|
}
|
||||||
|
institution: {
|
||||||
|
name: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
name: string
|
||||||
|
currency: string
|
||||||
|
id: string
|
||||||
|
last_four: string
|
||||||
|
status: 'open' | 'closed'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DepositoryAccount extends BaseAccount {
|
||||||
|
type: 'depository'
|
||||||
|
subtype: DepositorySubtypes
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreditAccount extends BaseAccount {
|
||||||
|
type: 'credit'
|
||||||
|
subtype: CreditSubtype
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Account = DepositoryAccount | CreditAccount
|
||||||
|
|
||||||
|
export type GetAccountsResponse = { accounts: Account[] }
|
||||||
|
export type GetAccountResponse = Account
|
||||||
|
export type DeleteAccountResponse = void
|
5
libs/teller-api/src/types/authentication.ts
Normal file
5
libs/teller-api/src/types/authentication.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// https://teller.io/docs/api/authentication
|
||||||
|
|
||||||
|
export type AuthenticationResponse = {
|
||||||
|
token: string
|
||||||
|
}
|
39
libs/teller-api/src/types/identity.ts
Normal file
39
libs/teller-api/src/types/identity.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// https://teller.io/docs/api/identity
|
||||||
|
|
||||||
|
import type { Account } from './accounts'
|
||||||
|
|
||||||
|
export type Identity = {
|
||||||
|
type: 'person' | 'business'
|
||||||
|
names: Name[]
|
||||||
|
data: string
|
||||||
|
addresses: Address[]
|
||||||
|
phone_numbers: PhoneNumber[]
|
||||||
|
emails: Email[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Name = {
|
||||||
|
type: 'name' | 'alias'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Address = {
|
||||||
|
primary: boolean
|
||||||
|
street: string
|
||||||
|
city: string
|
||||||
|
region: string
|
||||||
|
postal_code: string
|
||||||
|
country_code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Email = {
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PhoneNumber = {
|
||||||
|
type: 'mobile' | 'home' | 'work' | 'unknown'
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetIdentityResponse = {
|
||||||
|
account: Account
|
||||||
|
owners: Identity[]
|
||||||
|
}[]
|
7
libs/teller-api/src/types/index.ts
Normal file
7
libs/teller-api/src/types/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export * from './accounts'
|
||||||
|
export * from './account-balance'
|
||||||
|
export * from './account-details'
|
||||||
|
export * from './authentication'
|
||||||
|
export * from './identity'
|
||||||
|
export * from './transactions'
|
||||||
|
export * from './webhooks'
|
59
libs/teller-api/src/types/transactions.ts
Normal file
59
libs/teller-api/src/types/transactions.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// https://teller.io/docs/api/account/transactions
|
||||||
|
|
||||||
|
type DetailCategory =
|
||||||
|
| 'accommodation'
|
||||||
|
| 'advertising'
|
||||||
|
| 'bar'
|
||||||
|
| 'charity'
|
||||||
|
| 'clothing'
|
||||||
|
| 'dining'
|
||||||
|
| 'education'
|
||||||
|
| 'electronics'
|
||||||
|
| 'entertainment'
|
||||||
|
| 'fuel'
|
||||||
|
| 'general'
|
||||||
|
| 'groceries'
|
||||||
|
| 'health'
|
||||||
|
| 'home'
|
||||||
|
| 'income'
|
||||||
|
| 'insurance'
|
||||||
|
| 'investment'
|
||||||
|
| 'loan'
|
||||||
|
| 'office'
|
||||||
|
| 'phone'
|
||||||
|
| 'service'
|
||||||
|
| 'shopping'
|
||||||
|
| 'software'
|
||||||
|
| 'sport'
|
||||||
|
| 'tax'
|
||||||
|
| 'transport'
|
||||||
|
| 'transportation'
|
||||||
|
| 'utilities'
|
||||||
|
|
||||||
|
type DetailProcessingStatus = 'pending' | 'complete'
|
||||||
|
|
||||||
|
export type Transaction = {
|
||||||
|
details: {
|
||||||
|
category?: DetailCategory
|
||||||
|
processing_status: DetailProcessingStatus
|
||||||
|
counterparty: {
|
||||||
|
name?: string
|
||||||
|
type?: 'organization' | 'person'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
running_balance: string | null
|
||||||
|
description: string
|
||||||
|
id: string
|
||||||
|
date: string
|
||||||
|
account_id: string
|
||||||
|
links: {
|
||||||
|
self: string
|
||||||
|
account: string
|
||||||
|
}
|
||||||
|
amount: string
|
||||||
|
status: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetTransactionsResponse = Transaction[]
|
||||||
|
export type GetTransactionResponse = Transaction
|
11
libs/teller-api/src/types/webhooks.ts
Normal file
11
libs/teller-api/src/types/webhooks.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// https://teller.io/docs/api/webhooks
|
||||||
|
|
||||||
|
export type WebhookData = {
|
||||||
|
id: string
|
||||||
|
payload: {
|
||||||
|
enrollment_id: string
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
timestamp: string
|
||||||
|
type: string
|
||||||
|
}
|
13
libs/teller-api/tsconfig.json
Normal file
13
libs/teller-api/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
11
libs/teller-api/tsconfig.lib.json
Normal file
11
libs/teller-api/tsconfig.lib.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"declaration": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"exclude": ["jest.config.ts", "**/*.spec.ts", "s**/*.test.ts"],
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
20
libs/teller-api/tsconfig.spec.json
Normal file
20
libs/teller-api/tsconfig.spec.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"jest.config.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/*.test.jsx",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nx run-many --target serve --projects=client,advisor,server,workers --parallel --host 0.0.0.0 --nx-bail=true --maxParallel=100",
|
"dev": "nx run-many --target serve --projects=client,server,workers --parallel --host 0.0.0.0 --nx-bail=true --maxParallel=100",
|
||||||
"dev:services": "COMPOSE_PROFILES=services docker-compose up -d",
|
"dev:services": "COMPOSE_PROFILES=services docker-compose up -d",
|
||||||
"dev:services:all": "COMPOSE_PROFILES=services,ngrok,stripe docker-compose up",
|
"dev:services:all": "COMPOSE_PROFILES=services,ngrok,stripe docker-compose up",
|
||||||
"dev:workers:test": "nx test workers --skip-nx-cache --runInBand",
|
"dev:workers:test": "nx test workers --skip-nx-cache --runInBand",
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"@maybe-finance/server/features": ["libs/server/features/src/index.ts"],
|
"@maybe-finance/server/features": ["libs/server/features/src/index.ts"],
|
||||||
"@maybe-finance/server/shared": ["libs/server/shared/src/index.ts"],
|
"@maybe-finance/server/shared": ["libs/server/shared/src/index.ts"],
|
||||||
"@maybe-finance/shared": ["libs/shared/src/index.ts"],
|
"@maybe-finance/shared": ["libs/shared/src/index.ts"],
|
||||||
|
"@maybe-finance/teller-api": ["libs/teller-api/src/index.ts"],
|
||||||
"@maybe-finance/trpc": ["apps/server/src/app/trpc.ts"]
|
"@maybe-finance/trpc": ["apps/server/src/app/trpc.ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -390,6 +390,30 @@
|
||||||
},
|
},
|
||||||
"tags": ["scope:shared"]
|
"tags": ["scope:shared"]
|
||||||
},
|
},
|
||||||
|
"teller-api": {
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"root": "libs/teller-api",
|
||||||
|
"sourceRoot": "libs/teller-api/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"targets": {
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"outputs": ["{options.outputFile}"],
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/teller-api/**/*.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/teller-api/jest.config.ts",
|
||||||
|
"passWithNoTests": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["scope:shared"]
|
||||||
|
},
|
||||||
"workers": {
|
"workers": {
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
"root": "apps/workers",
|
"root": "apps/workers",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue