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_CONVERTKIT_SECRET=
|
||||
|
||||
# Teller API keys (https://teller.io)
|
||||
NX_TELLER_SIGNING_SECRET=
|
||||
NX_TELLER_APP_ID=
|
||||
|
||||
NEXT_PUBLIC_ZAPIER_FEEDBACK_HOOK_URL=
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -53,4 +53,5 @@ Thumbs.db
|
|||
migrations.json
|
||||
|
||||
# Shouldn't happen, but backup since we have a script that generates these locally
|
||||
*.pem
|
||||
*.pem
|
||||
certs/
|
||||
|
|
|
@ -42,6 +42,8 @@ import {
|
|||
InstitutionProviderFactory,
|
||||
FinicityWebhookHandler,
|
||||
PlaidWebhookHandler,
|
||||
TellerService,
|
||||
TellerWebhookHandler,
|
||||
InsightService,
|
||||
SecurityPricingService,
|
||||
TransactionService,
|
||||
|
@ -55,6 +57,7 @@ import { SharedType } from '@maybe-finance/shared'
|
|||
import prisma from './prisma'
|
||||
import plaid, { getPlaidWebhookUrl } from './plaid'
|
||||
import finicity, { getFinicityTxPushUrl, getFinicityWebhookUrl } from './finicity'
|
||||
import teller, { getTellerWebhookUrl } from './teller'
|
||||
import stripe from './stripe'
|
||||
import postmark from './postmark'
|
||||
import defineAbilityFor from './ability'
|
||||
|
@ -142,6 +145,14 @@ const finicityService = new FinicityService(
|
|||
env.NX_FINICITY_ENV === 'sandbox'
|
||||
)
|
||||
|
||||
const tellerService = new TellerService(
|
||||
logger.child({ service: 'TellerService' }),
|
||||
prisma,
|
||||
teller,
|
||||
getTellerWebhookUrl(),
|
||||
true
|
||||
)
|
||||
|
||||
// account-connection
|
||||
|
||||
const accountConnectionProviderFactory = new AccountConnectionProviderFactory({
|
||||
|
@ -278,6 +289,13 @@ const stripeWebhooks = new StripeWebhookHandler(
|
|||
stripe
|
||||
)
|
||||
|
||||
const tellerWebhooks = new TellerWebhookHandler(
|
||||
logger.child({ service: 'TellerWebhookHandler' }),
|
||||
prisma,
|
||||
teller,
|
||||
accountConnectionService
|
||||
)
|
||||
|
||||
// helper function for parsing JWT and loading User record
|
||||
// TODO: update this with roles, identity, and metadata
|
||||
async function getCurrentUser(jwt: NonNullable<Request['user']>) {
|
||||
|
@ -334,6 +352,8 @@ export async function createContext(req: Request) {
|
|||
finicityService,
|
||||
finicityWebhooks,
|
||||
stripeWebhooks,
|
||||
tellerService,
|
||||
tellerWebhooks,
|
||||
insightService,
|
||||
marketDataService,
|
||||
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-plaid-jwt'
|
||||
export * from './validate-finicity-signature'
|
||||
export * from './validate-teller-signature'
|
||||
export { default as maintenance } from './maintenance'
|
||||
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 { z } from 'zod'
|
||||
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 stripe from '../lib/stripe'
|
||||
import env from '../../env'
|
||||
import type { TellerTypes } from '@maybe-finance/teller-api'
|
||||
|
||||
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
|
||||
|
|
|
@ -42,6 +42,9 @@ const envSchema = z.object({
|
|||
NX_FINICITY_PARTNER_SECRET: z.string(),
|
||||
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_ENV: z.string().optional(),
|
||||
|
||||
|
|
|
@ -15,6 +15,9 @@ const envSchema = z.object({
|
|||
NX_FINICITY_PARTNER_SECRET: z.string(),
|
||||
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_ENV: z.string().optional(),
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from './plaid'
|
||||
export * from './finicity'
|
||||
export * from './teller'
|
||||
export * from './vehicle'
|
||||
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 {
|
||||
encrypt(plainText: string): string
|
||||
|
@ -6,13 +6,32 @@ export interface 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) {
|
||||
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) {
|
||||
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",
|
||||
"license": "MIT",
|
||||
"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:all": "COMPOSE_PROFILES=services,ngrok,stripe docker-compose up",
|
||||
"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/shared": ["libs/server/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"]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -390,6 +390,30 @@
|
|||
},
|
||||
"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": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"root": "apps/workers",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue