Migrate KYC backend from Django to Express + Apollo Server + Prisma
All checks were successful
Build Docker Image / build (push) Successful in 2m12s
All checks were successful
Build Docker Image / build (push) Successful in 2m12s
Replace Python/Django/Graphene with TypeScript/Express/Apollo Server. Same 3 endpoints (public/user/m2m), same JWT auth via Logto. Prisma replaces Django ORM. MongoDB, Temporal and SurrealDB integrations preserved.
This commit is contained in:
47
src/auth.ts
Normal file
47
src/auth.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import type { Request } from 'express'
|
||||
|
||||
const LOGTO_JWKS_URL = process.env.LOGTO_JWKS_URL || 'https://auth.optovia.ru/oidc/jwks'
|
||||
const LOGTO_ISSUER = process.env.LOGTO_ISSUER || 'https://auth.optovia.ru/oidc'
|
||||
|
||||
const jwks = createRemoteJWKSet(new URL(LOGTO_JWKS_URL))
|
||||
|
||||
export interface AuthContext {
|
||||
userId?: string
|
||||
scopes: string[]
|
||||
isM2M?: boolean
|
||||
}
|
||||
|
||||
function getBearerToken(req: Request): string | null {
|
||||
const auth = req.headers.authorization || ''
|
||||
if (!auth.startsWith('Bearer ')) return null
|
||||
const token = auth.slice(7)
|
||||
if (!token || token === 'undefined') return null
|
||||
return token
|
||||
}
|
||||
|
||||
export async function publicContext(req: Request): Promise<AuthContext> {
|
||||
// Optional auth - try to extract userId if token present
|
||||
const token = getBearerToken(req)
|
||||
if (!token) return { scopes: [] }
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, jwks, { issuer: LOGTO_ISSUER })
|
||||
return { userId: payload.sub, scopes: [] }
|
||||
} catch {
|
||||
return { scopes: [] }
|
||||
}
|
||||
}
|
||||
|
||||
export async function userContext(req: Request): Promise<AuthContext> {
|
||||
const token = getBearerToken(req)
|
||||
if (!token) {
|
||||
throw new GraphQLError('Unauthorized', { extensions: { code: 'UNAUTHENTICATED' } })
|
||||
}
|
||||
const { payload } = await jwtVerify(token, jwks, { issuer: LOGTO_ISSUER })
|
||||
return { userId: payload.sub, scopes: [] }
|
||||
}
|
||||
|
||||
export async function m2mContext(): Promise<AuthContext> {
|
||||
return { scopes: [], isM2M: true }
|
||||
}
|
||||
3
src/db.ts
Normal file
3
src/db.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
export const prisma = new PrismaClient()
|
||||
80
src/index.ts
Normal file
80
src/index.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
import { expressMiddleware } from '@apollo/server/express4'
|
||||
import * as Sentry from '@sentry/node'
|
||||
import { publicTypeDefs, publicResolvers } from './schemas/public.js'
|
||||
import { userTypeDefs, userResolvers } from './schemas/user.js'
|
||||
import { m2mTypeDefs, m2mResolvers } from './schemas/m2m.js'
|
||||
import { publicContext, userContext, m2mContext, type AuthContext } from './auth.js'
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '8000', 10)
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || ''
|
||||
|
||||
if (SENTRY_DSN) {
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 0.01,
|
||||
release: process.env.RELEASE_VERSION || '1.0.0',
|
||||
environment: process.env.ENVIRONMENT || 'production',
|
||||
})
|
||||
}
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(cors({ origin: ['https://optovia.ru'], credentials: true }))
|
||||
|
||||
const publicServer = new ApolloServer<AuthContext>({
|
||||
typeDefs: publicTypeDefs,
|
||||
resolvers: publicResolvers,
|
||||
introspection: true,
|
||||
})
|
||||
|
||||
const userServer = new ApolloServer<AuthContext>({
|
||||
typeDefs: userTypeDefs,
|
||||
resolvers: userResolvers,
|
||||
introspection: true,
|
||||
})
|
||||
|
||||
const m2mServer = new ApolloServer<AuthContext>({
|
||||
typeDefs: m2mTypeDefs,
|
||||
resolvers: m2mResolvers,
|
||||
introspection: true,
|
||||
})
|
||||
|
||||
await Promise.all([publicServer.start(), userServer.start(), m2mServer.start()])
|
||||
|
||||
app.use(
|
||||
'/graphql/public',
|
||||
express.json(),
|
||||
expressMiddleware(publicServer, {
|
||||
context: async ({ req }) => publicContext(req as unknown as import('express').Request),
|
||||
}) as unknown as express.RequestHandler,
|
||||
)
|
||||
|
||||
app.use(
|
||||
'/graphql/user',
|
||||
express.json(),
|
||||
expressMiddleware(userServer, {
|
||||
context: async ({ req }) => userContext(req as unknown as import('express').Request),
|
||||
}) as unknown as express.RequestHandler,
|
||||
)
|
||||
|
||||
app.use(
|
||||
'/graphql/m2m',
|
||||
express.json(),
|
||||
expressMiddleware(m2mServer, {
|
||||
context: async () => m2mContext(),
|
||||
}) as unknown as express.RequestHandler,
|
||||
)
|
||||
|
||||
app.get('/health', (_, res) => {
|
||||
res.json({ status: 'ok' })
|
||||
})
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`KYC server ready on port ${PORT}`)
|
||||
console.log(` /graphql/public - public (optional auth)`)
|
||||
console.log(` /graphql/user - id token auth`)
|
||||
console.log(` /graphql/m2m - internal services (no auth)`)
|
||||
})
|
||||
70
src/schemas/m2m.ts
Normal file
70
src/schemas/m2m.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { prisma } from '../db.js'
|
||||
|
||||
export const m2mTypeDefs = `#graphql
|
||||
type CreateKycProfileResult {
|
||||
success: Boolean!
|
||||
profileUuid: String
|
||||
message: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
health: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createKycProfile(kycApplicationId: String!): CreateKycProfileResult
|
||||
createKycMonitoring(kycApplicationId: String!): CreateKycProfileResult
|
||||
}
|
||||
`
|
||||
|
||||
async function createProfile(_: unknown, args: { kycApplicationId: string }) {
|
||||
try {
|
||||
const app = await prisma.kYCApplication.findUnique({
|
||||
where: { uuid: args.kycApplicationId },
|
||||
})
|
||||
|
||||
if (!app) {
|
||||
return { success: false, profileUuid: '', message: `KYCApplication not found: ${args.kycApplicationId}` }
|
||||
}
|
||||
|
||||
// Check if profile already exists
|
||||
const existing = await prisma.kYCProfile.findFirst({
|
||||
where: { userId: app.userId, teamName: app.teamName },
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
return { success: true, profileUuid: existing.uuid, message: 'Profile already exists' }
|
||||
}
|
||||
|
||||
const profile = await prisma.kYCProfile.create({
|
||||
data: {
|
||||
userId: app.userId,
|
||||
teamName: app.teamName,
|
||||
countryCode: app.countryCode,
|
||||
workflowStatus: 'active',
|
||||
score: app.score,
|
||||
contactPerson: app.contactPerson,
|
||||
contactEmail: app.contactEmail,
|
||||
contactPhone: app.contactPhone,
|
||||
contentTypeId: app.contentTypeId,
|
||||
objectId: app.objectId,
|
||||
approvedBy: app.approvedBy,
|
||||
approvedAt: app.approvedAt,
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true, profileUuid: profile.uuid, message: 'Profile created' }
|
||||
} catch (e) {
|
||||
return { success: false, profileUuid: '', message: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
export const m2mResolvers = {
|
||||
Query: {
|
||||
health: () => 'ok',
|
||||
},
|
||||
Mutation: {
|
||||
createKycProfile: createProfile,
|
||||
createKycMonitoring: createProfile,
|
||||
},
|
||||
}
|
||||
89
src/schemas/public.ts
Normal file
89
src/schemas/public.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { prisma } from '../db.js'
|
||||
import { getCompanyDocuments, aggregateCompanyData } from '../services/mongodb.js'
|
||||
import type { AuthContext } from '../auth.js'
|
||||
|
||||
export const publicTypeDefs = `#graphql
|
||||
type CompanyTeaser {
|
||||
companyType: String
|
||||
registrationYear: Int
|
||||
isActive: Boolean
|
||||
sourcesCount: Int
|
||||
}
|
||||
|
||||
type CompanyFull {
|
||||
inn: String
|
||||
ogrn: String
|
||||
name: String
|
||||
companyType: String
|
||||
registrationYear: Int
|
||||
isActive: Boolean
|
||||
address: String
|
||||
director: String
|
||||
capital: String
|
||||
activities: [String]
|
||||
sources: [String]
|
||||
lastUpdated: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
kycProfileTeaser(profileUuid: String!): CompanyTeaser
|
||||
kycProfileFull(profileUuid: String!): CompanyFull
|
||||
health: String!
|
||||
}
|
||||
`
|
||||
|
||||
async function getInnByProfileUuid(profileUuid: string): Promise<string | null> {
|
||||
const profile = await prisma.kYCProfile.findUnique({ where: { uuid: profileUuid } })
|
||||
if (!profile || !profile.objectId) return null
|
||||
|
||||
const details = await prisma.kYCDetailsRussia.findUnique({ where: { id: profile.objectId } })
|
||||
return details?.inn ?? null
|
||||
}
|
||||
|
||||
export const publicResolvers = {
|
||||
Query: {
|
||||
health: () => 'ok',
|
||||
|
||||
kycProfileTeaser: async (_: unknown, args: { profileUuid: string }) => {
|
||||
const inn = await getInnByProfileUuid(args.profileUuid)
|
||||
if (!inn) return null
|
||||
|
||||
const docs = await getCompanyDocuments(inn)
|
||||
if (docs.length === 0) return null
|
||||
|
||||
const summary = aggregateCompanyData(docs as Record<string, unknown>[])
|
||||
return {
|
||||
companyType: summary.companyType,
|
||||
registrationYear: summary.registrationYear,
|
||||
isActive: summary.isActive,
|
||||
sourcesCount: summary.sources.length,
|
||||
}
|
||||
},
|
||||
|
||||
kycProfileFull: async (_: unknown, args: { profileUuid: string }, ctx: AuthContext) => {
|
||||
if (!ctx.userId) return null
|
||||
|
||||
const inn = await getInnByProfileUuid(args.profileUuid)
|
||||
if (!inn) return null
|
||||
|
||||
const docs = await getCompanyDocuments(inn)
|
||||
if (docs.length === 0) return null
|
||||
|
||||
const summary = aggregateCompanyData(docs as Record<string, unknown>[])
|
||||
return {
|
||||
inn: summary.inn,
|
||||
ogrn: summary.ogrn,
|
||||
name: summary.name,
|
||||
companyType: summary.companyType,
|
||||
registrationYear: summary.registrationYear,
|
||||
isActive: summary.isActive,
|
||||
address: summary.address,
|
||||
director: summary.director,
|
||||
capital: summary.capital,
|
||||
activities: summary.activities,
|
||||
sources: summary.sources,
|
||||
lastUpdated: summary.lastUpdated,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
181
src/schemas/user.ts
Normal file
181
src/schemas/user.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { prisma } from '../db.js'
|
||||
import { startKycWorkflow } from '../services/temporal.js'
|
||||
import type { AuthContext } from '../auth.js'
|
||||
|
||||
export const userTypeDefs = `#graphql
|
||||
type KYCApplication {
|
||||
id: Int!
|
||||
uuid: String!
|
||||
userId: String!
|
||||
teamName: String
|
||||
countryCode: String
|
||||
workflowStatus: String!
|
||||
score: Int!
|
||||
contactPerson: String
|
||||
contactEmail: String
|
||||
contactPhone: String
|
||||
countryData: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
input KYCApplicationRussiaInput {
|
||||
companyName: String!
|
||||
companyFullName: String!
|
||||
inn: String!
|
||||
kpp: String
|
||||
ogrn: String
|
||||
address: String!
|
||||
bankName: String!
|
||||
bik: String!
|
||||
correspondentAccount: String
|
||||
contactPerson: String!
|
||||
contactEmail: String!
|
||||
contactPhone: String!
|
||||
}
|
||||
|
||||
type CreateKYCApplicationResult {
|
||||
kycApplication: KYCApplication
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type Query {
|
||||
kycApplications: [KYCApplication]
|
||||
kycApplication(uuid: String!): KYCApplication
|
||||
kycRequests: [KYCApplication]
|
||||
kycRequest(uuid: String!): KYCApplication
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createKycApplicationRussia(input: KYCApplicationRussiaInput!): CreateKYCApplicationResult
|
||||
createKycRequestRussia(input: KYCApplicationRussiaInput!): CreateKYCApplicationResult
|
||||
}
|
||||
`
|
||||
|
||||
async function getApplications(ctx: AuthContext) {
|
||||
if (!ctx.userId) return []
|
||||
return prisma.kYCApplication.findMany({ where: { userId: ctx.userId } })
|
||||
}
|
||||
|
||||
async function getApplication(ctx: AuthContext, uuid: string) {
|
||||
if (!ctx.userId) return null
|
||||
return prisma.kYCApplication.findFirst({ where: { uuid, userId: ctx.userId } })
|
||||
}
|
||||
|
||||
async function getCountryData(app: { objectId: number | null }) {
|
||||
if (!app.objectId) return null
|
||||
const details = await prisma.kYCDetailsRussia.findUnique({ where: { id: app.objectId } })
|
||||
if (!details) return null
|
||||
return JSON.stringify({
|
||||
company_name: details.companyName,
|
||||
company_full_name: details.companyFullName,
|
||||
inn: details.inn,
|
||||
kpp: details.kpp,
|
||||
ogrn: details.ogrn,
|
||||
address: details.address,
|
||||
bank_name: details.bankName,
|
||||
bik: details.bik,
|
||||
correspondent_account: details.correspondentAccount,
|
||||
})
|
||||
}
|
||||
|
||||
interface RussiaInput {
|
||||
companyName: string
|
||||
companyFullName: string
|
||||
inn: string
|
||||
kpp?: string
|
||||
ogrn?: string
|
||||
address: string
|
||||
bankName: string
|
||||
bik: string
|
||||
correspondentAccount?: string
|
||||
contactPerson: string
|
||||
contactEmail: string
|
||||
contactPhone: string
|
||||
}
|
||||
|
||||
async function createApplicationRussia(_: unknown, args: { input: RussiaInput }, ctx: AuthContext) {
|
||||
if (!ctx.userId) throw new GraphQLError('Not authenticated')
|
||||
|
||||
const details = await prisma.kYCDetailsRussia.create({
|
||||
data: {
|
||||
companyName: args.input.companyName,
|
||||
companyFullName: args.input.companyFullName,
|
||||
inn: args.input.inn,
|
||||
kpp: args.input.kpp || '',
|
||||
ogrn: args.input.ogrn || '',
|
||||
address: args.input.address,
|
||||
bankName: args.input.bankName,
|
||||
bik: args.input.bik,
|
||||
correspondentAccount: args.input.correspondentAccount || '',
|
||||
},
|
||||
})
|
||||
|
||||
// Django ContentType ID for kyc_details_russia - need to look this up
|
||||
// For compatibility, we store the objectId but skip content_type_id
|
||||
const app = await prisma.kYCApplication.create({
|
||||
data: {
|
||||
userId: ctx.userId,
|
||||
teamName: args.input.companyName,
|
||||
countryCode: 'RU',
|
||||
contactPerson: args.input.contactPerson,
|
||||
contactEmail: args.input.contactEmail,
|
||||
contactPhone: args.input.contactPhone,
|
||||
objectId: details.id,
|
||||
},
|
||||
})
|
||||
|
||||
// Start Temporal workflow
|
||||
try {
|
||||
await startKycWorkflow({
|
||||
kyc_request_id: app.uuid,
|
||||
team_name: app.teamName || args.input.companyName,
|
||||
owner_id: ctx.userId,
|
||||
owner_email: args.input.contactEmail,
|
||||
country_code: 'RU',
|
||||
country_data: {
|
||||
company_name: details.companyName,
|
||||
company_full_name: details.companyFullName,
|
||||
inn: details.inn,
|
||||
kpp: details.kpp,
|
||||
ogrn: details.ogrn,
|
||||
address: details.address,
|
||||
bank_name: details.bankName,
|
||||
bik: details.bik,
|
||||
correspondent_account: details.correspondentAccount,
|
||||
},
|
||||
})
|
||||
await prisma.kYCApplication.update({
|
||||
where: { id: app.id },
|
||||
data: { workflowStatus: 'active' },
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to start KYC workflow:', e)
|
||||
await prisma.kYCApplication.update({
|
||||
where: { id: app.id },
|
||||
data: { workflowStatus: 'error' },
|
||||
})
|
||||
}
|
||||
|
||||
const updated = await prisma.kYCApplication.findUnique({ where: { id: app.id } })
|
||||
return { kycApplication: updated, success: true }
|
||||
}
|
||||
|
||||
export const userResolvers = {
|
||||
Query: {
|
||||
kycApplications: (_: unknown, __: unknown, ctx: AuthContext) => getApplications(ctx),
|
||||
kycApplication: (_: unknown, args: { uuid: string }, ctx: AuthContext) => getApplication(ctx, args.uuid),
|
||||
kycRequests: (_: unknown, __: unknown, ctx: AuthContext) => getApplications(ctx),
|
||||
kycRequest: (_: unknown, args: { uuid: string }, ctx: AuthContext) => getApplication(ctx, args.uuid),
|
||||
},
|
||||
Mutation: {
|
||||
createKycApplicationRussia: createApplicationRussia,
|
||||
createKycRequestRussia: createApplicationRussia,
|
||||
},
|
||||
KYCApplication: {
|
||||
countryData: async (parent: { objectId: number | null }) => getCountryData(parent),
|
||||
createdAt: (parent: { createdAt: Date }) => parent.createdAt.toISOString(),
|
||||
updatedAt: (parent: { updatedAt: Date }) => parent.updatedAt.toISOString(),
|
||||
},
|
||||
}
|
||||
65
src/services/mongodb.ts
Normal file
65
src/services/mongodb.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { MongoClient } from 'mongodb'
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || ''
|
||||
const MONGODB_DB = process.env.MONGODB_DB || 'kyc'
|
||||
|
||||
export async function getCompanyDocuments(inn: string) {
|
||||
if (!MONGODB_URI) return []
|
||||
const client = new MongoClient(MONGODB_URI)
|
||||
try {
|
||||
await client.connect()
|
||||
const db = client.db(MONGODB_DB)
|
||||
return await db.collection('company_documents').find({ inn }).toArray()
|
||||
} finally {
|
||||
await client.close()
|
||||
}
|
||||
}
|
||||
|
||||
interface CompanySummary {
|
||||
inn?: string
|
||||
ogrn?: string
|
||||
name?: string
|
||||
companyType?: string
|
||||
registrationYear?: number
|
||||
isActive: boolean
|
||||
address?: string
|
||||
director?: string
|
||||
capital?: string
|
||||
activities: string[]
|
||||
sources: string[]
|
||||
lastUpdated?: string
|
||||
}
|
||||
|
||||
export function aggregateCompanyData(documents: Record<string, unknown>[]): CompanySummary {
|
||||
const summary: CompanySummary = {
|
||||
isActive: true,
|
||||
activities: [],
|
||||
sources: [],
|
||||
}
|
||||
|
||||
for (const doc of documents) {
|
||||
const source = (doc.source as string) || 'unknown'
|
||||
summary.sources.push(source)
|
||||
|
||||
const data = (doc.data as Record<string, unknown>) || {}
|
||||
|
||||
if (!summary.inn) summary.inn = doc.inn as string
|
||||
if (!summary.ogrn && data.ogrn) summary.ogrn = data.ogrn as string
|
||||
if (!summary.name && data.name) summary.name = data.name as string
|
||||
|
||||
if (!summary.companyType && summary.name) {
|
||||
const name = summary.name.toUpperCase()
|
||||
if (name.includes('ООО') || name.includes('ОБЩЕСТВО С ОГРАНИЧЕННОЙ')) summary.companyType = 'ООО'
|
||||
else if (name.includes('ПАО')) summary.companyType = 'ПАО'
|
||||
else if (name.includes('АО') || name.includes('АКЦИОНЕРНОЕ ОБЩЕСТВО')) summary.companyType = 'АО'
|
||||
else if (name.includes('ИП') || name.includes('ИНДИВИДУАЛЬНЫЙ ПРЕДПРИНИМАТЕЛЬ')) summary.companyType = 'ИП'
|
||||
}
|
||||
|
||||
const collectedAt = doc.collected_at as string | undefined
|
||||
if (collectedAt && (!summary.lastUpdated || collectedAt > summary.lastUpdated)) {
|
||||
summary.lastUpdated = collectedAt
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
47
src/services/surrealdb.ts
Normal file
47
src/services/surrealdb.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
const SURREALDB_URL = process.env.SURREALDB_URL || ''
|
||||
const SURREALDB_NS = process.env.SURREALDB_NS || 'optovia'
|
||||
const SURREALDB_DB = process.env.SURREALDB_DB || 'events'
|
||||
const SURREALDB_USER = process.env.SURREALDB_USER || ''
|
||||
const SURREALDB_PASS = process.env.SURREALDB_PASS || ''
|
||||
|
||||
export async function logKycEvent(
|
||||
kycId: string,
|
||||
userId: string,
|
||||
event: string,
|
||||
description: string,
|
||||
): Promise<boolean> {
|
||||
if (!SURREALDB_URL) return false
|
||||
|
||||
const payload = {
|
||||
kyc_id: kycId,
|
||||
user_id: userId,
|
||||
event,
|
||||
description,
|
||||
created_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const query = `CREATE kyc_event CONTENT ${JSON.stringify(payload)};`
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'text/plain',
|
||||
Accept: 'application/json',
|
||||
NS: SURREALDB_NS,
|
||||
DB: SURREALDB_DB,
|
||||
}
|
||||
|
||||
if (SURREALDB_USER && SURREALDB_PASS) {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${SURREALDB_USER}:${SURREALDB_PASS}`).toString('base64')}`
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${SURREALDB_URL.replace(/\/$/, '')}/sql`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: query,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
return res.ok
|
||||
} catch (e) {
|
||||
console.error('Failed to log KYC event:', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
28
src/services/temporal.ts
Normal file
28
src/services/temporal.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Client, Connection } from '@temporalio/client'
|
||||
|
||||
const TEMPORAL_HOST = process.env.TEMPORAL_HOST || 'temporal:7233'
|
||||
const TEMPORAL_NAMESPACE = process.env.TEMPORAL_NAMESPACE || 'default'
|
||||
const TEMPORAL_TASK_QUEUE = process.env.TEMPORAL_TASK_QUEUE || 'kyc-task-queue'
|
||||
|
||||
interface KycWorkflowData {
|
||||
kyc_request_id: string
|
||||
team_name: string
|
||||
owner_id: string
|
||||
owner_email: string
|
||||
country_code: string
|
||||
country_data: Record<string, string>
|
||||
}
|
||||
|
||||
export async function startKycWorkflow(data: KycWorkflowData): Promise<string> {
|
||||
const connection = await Connection.connect({ address: TEMPORAL_HOST })
|
||||
const client = new Client({ connection, namespace: TEMPORAL_NAMESPACE })
|
||||
|
||||
const handle = await client.workflow.start('kyc_application', {
|
||||
args: [data],
|
||||
taskQueue: TEMPORAL_TASK_QUEUE,
|
||||
workflowId: data.kyc_request_id,
|
||||
})
|
||||
|
||||
console.log(`KYC workflow started: ${handle.workflowId}`)
|
||||
return handle.workflowId
|
||||
}
|
||||
Reference in New Issue
Block a user