Link KYC profiles to teams
Build and deploy Docker image / build (push) Successful in 55s

This commit is contained in:
Ruslan Bakiev
2026-08-24 21:10:09 +07:00
parent 8a376dfa53
commit 501b2aa134
6 changed files with 81 additions and 53 deletions
@@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "kyc_requests" ADD COLUMN "team_uuid" VARCHAR(100);
-- AlterTable
ALTER TABLE "kyc_monitoring" ADD COLUMN "team_uuid" VARCHAR(100);
-- CreateIndex
CREATE INDEX "kyc_requests_team_uuid_idx" ON "kyc_requests"("team_uuid");
-- CreateIndex
CREATE INDEX "kyc_monitoring_team_uuid_idx" ON "kyc_monitoring"("team_uuid");
+4
View File
@@ -10,6 +10,7 @@ model KYCApplication {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
uuid String @unique @default(uuid()) uuid String @unique @default(uuid())
userId String @map("user_id") @db.VarChar(255) userId String @map("user_id") @db.VarChar(255)
teamUuid String? @map("team_uuid") @db.VarChar(100)
teamName String @default("") @map("team_name") @db.VarChar(200) teamName String @default("") @map("team_name") @db.VarChar(200)
countryCode String @default("") @map("country_code") @db.VarChar(2) countryCode String @default("") @map("country_code") @db.VarChar(2)
workflowStatus String @default("pending") @map("workflow_status") @db.VarChar(20) workflowStatus String @default("pending") @map("workflow_status") @db.VarChar(20)
@@ -25,6 +26,7 @@ model KYCApplication {
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@index([userId]) @@index([userId])
@@index([teamUuid])
@@map("kyc_requests") @@map("kyc_requests")
} }
@@ -32,6 +34,7 @@ model KYCProfile {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
uuid String @unique @default(uuid()) uuid String @unique @default(uuid())
userId String @map("user_id") @db.VarChar(255) userId String @map("user_id") @db.VarChar(255)
teamUuid String? @map("team_uuid") @db.VarChar(100)
teamName String @default("") @map("team_name") @db.VarChar(200) teamName String @default("") @map("team_name") @db.VarChar(200)
countryCode String @default("") @map("country_code") @db.VarChar(2) countryCode String @default("") @map("country_code") @db.VarChar(2)
workflowStatus String @default("pending") @map("workflow_status") @db.VarChar(20) workflowStatus String @default("pending") @map("workflow_status") @db.VarChar(20)
@@ -47,6 +50,7 @@ model KYCProfile {
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@index([userId]) @@index([userId])
@@index([teamUuid])
@@map("kyc_monitoring") @@map("kyc_monitoring")
} }
+58 -52
View File
@@ -3,7 +3,7 @@ import { prisma } from '../db.js'
export const m2mTypeDefs = `#graphql export const m2mTypeDefs = `#graphql
type KycPricingContext { type KycPricingContext {
userId: String! teamUuid: String!
profileUuid: String! profileUuid: String!
countryCode: String! countryCode: String!
resolvedAt: String! resolvedAt: String!
@@ -17,19 +17,19 @@ export const m2mTypeDefs = `#graphql
type Query { type Query {
health: String! health: String!
pricingContextForUser(userId: String!): KycPricingContext pricingContextForTeam(teamUuid: String!): KycPricingContext
} }
type Mutation { type Mutation {
createKycProfile(kycApplicationId: String!): CreateKycProfileResult createKycProfile(kycApplicationId: String!): CreateKycProfileResult
createKycMonitoring(kycApplicationId: String!): CreateKycProfileResult createKycMonitoring(kycApplicationId: String!): CreateKycProfileResult
ensureDemoPricingContextForUser(userId: String!, countryCode: String!): KycPricingContext! ensureDemoPricingContextForTeam(teamUuid: String!, userId: String!, countryCode: String!): KycPricingContext!
} }
` `
function demoProfileUuid(userId: string): string { function demoProfileUuid(teamUuid: string): string {
const hex = createHash('sha256') const hex = createHash('sha256')
.update(`kyc:demo-pricing-context:${userId}`) .update(`kyc:demo-pricing-context:${teamUuid}`)
.digest('hex') .digest('hex')
.slice(0, 32) .slice(0, 32)
.split('') .split('')
@@ -48,16 +48,19 @@ function normalizedCountryCode(countryCode: string): string {
} }
function mapPricingContext(profile: { function mapPricingContext(profile: {
userId: string teamUuid: string | null
uuid: string uuid: string
countryCode: string countryCode: string
updatedAt: Date updatedAt: Date
}) { }) {
if (profile.teamUuid === null || profile.teamUuid.length === 0) {
throw new Error(`KYC pricing profile ${profile.uuid} has no team`)
}
if (profile.countryCode.length !== 2) { if (profile.countryCode.length !== 2) {
throw new Error(`KYC pricing profile ${profile.uuid} has no valid country code`) throw new Error(`KYC pricing profile ${profile.uuid} has no valid country code`)
} }
return { return {
userId: profile.userId, teamUuid: profile.teamUuid,
profileUuid: profile.uuid, profileUuid: profile.uuid,
countryCode: profile.countryCode.toUpperCase(), countryCode: profile.countryCode.toUpperCase(),
resolvedAt: profile.updatedAt.toISOString(), resolvedAt: profile.updatedAt.toISOString(),
@@ -65,51 +68,50 @@ function mapPricingContext(profile: {
} }
async function createProfile(_: unknown, args: { kycApplicationId: string }) { async function createProfile(_: unknown, args: { kycApplicationId: string }) {
try { const app = await prisma.kYCApplication.findUnique({
const app = await prisma.kYCApplication.findUnique({ where: { uuid: args.kycApplicationId },
where: { uuid: args.kycApplicationId }, })
})
if (!app) { if (!app) {
return { success: false, profileUuid: '', message: `KYCApplication not found: ${args.kycApplicationId}` } 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) }
} }
if (app.teamUuid === null || app.teamUuid.length === 0) {
throw new Error(`KYCApplication ${app.uuid} has no team`)
}
const existing = await prisma.kYCProfile.findFirst({
where: { teamUuid: app.teamUuid },
})
if (existing) {
return { success: true, profileUuid: existing.uuid, message: 'Profile already exists' }
}
const profile = await prisma.kYCProfile.create({
data: {
userId: app.userId,
teamUuid: app.teamUuid,
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' }
} }
async function pricingContextForUser(_: unknown, args: { userId: string }) { async function pricingContextForTeam(_: unknown, args: { teamUuid: string }) {
const profile = await prisma.kYCProfile.findFirst({ const profile = await prisma.kYCProfile.findFirst({
where: { where: {
userId: args.userId, teamUuid: args.teamUuid,
workflowStatus: { workflowStatus: {
in: ['active', 'approved', 'demo'], in: ['active', 'approved', 'demo'],
}, },
@@ -122,13 +124,15 @@ async function pricingContextForUser(_: unknown, args: { userId: string }) {
return mapPricingContext(profile) return mapPricingContext(profile)
} }
async function ensureDemoPricingContextForUser( async function ensureDemoPricingContextForTeam(
_: unknown, _: unknown,
args: { userId: string; countryCode: string }, args: { teamUuid: string; userId: string; countryCode: string },
) { ) {
const teamUuid = args.teamUuid.trim()
if (teamUuid.length === 0) throw new Error('Demo pricing team is required')
const existing = await prisma.kYCProfile.findFirst({ const existing = await prisma.kYCProfile.findFirst({
where: { where: {
userId: args.userId, teamUuid,
workflowStatus: { workflowStatus: {
in: ['active', 'approved', 'demo'], in: ['active', 'approved', 'demo'],
}, },
@@ -140,18 +144,20 @@ async function ensureDemoPricingContextForUser(
if (existing !== null) return mapPricingContext(existing) if (existing !== null) return mapPricingContext(existing)
const countryCode = normalizedCountryCode(args.countryCode) const countryCode = normalizedCountryCode(args.countryCode)
const profileUuid = demoProfileUuid(args.userId) const profileUuid = demoProfileUuid(teamUuid)
const profile = await prisma.kYCProfile.upsert({ const profile = await prisma.kYCProfile.upsert({
where: { uuid: profileUuid }, where: { uuid: profileUuid },
create: { create: {
uuid: profileUuid, uuid: profileUuid,
userId: args.userId, userId: args.userId,
teamUuid,
teamName: 'Demo workspace', teamName: 'Demo workspace',
countryCode, countryCode,
workflowStatus: 'demo', workflowStatus: 'demo',
}, },
update: { update: {
userId: args.userId, userId: args.userId,
teamUuid,
teamName: 'Demo workspace', teamName: 'Demo workspace',
countryCode, countryCode,
workflowStatus: 'demo', workflowStatus: 'demo',
@@ -163,11 +169,11 @@ async function ensureDemoPricingContextForUser(
export const m2mResolvers = { export const m2mResolvers = {
Query: { Query: {
health: () => 'ok', health: () => 'ok',
pricingContextForUser, pricingContextForTeam,
}, },
Mutation: { Mutation: {
createKycProfile: createProfile, createKycProfile: createProfile,
createKycMonitoring: createProfile, createKycMonitoring: createProfile,
ensureDemoPricingContextForUser, ensureDemoPricingContextForTeam,
}, },
} }
+1
View File
@@ -7,6 +7,7 @@ export const managerTypeDefs = `#graphql
id: Int! id: Int!
uuid: String! uuid: String!
userId: String! userId: String!
teamUuid: String
teamName: String teamName: String
countryCode: String countryCode: String
workflowStatus: String! workflowStatus: String!
+6
View File
@@ -7,6 +7,7 @@ export const userTypeDefs = `#graphql
id: Int! id: Int!
uuid: String! uuid: String!
userId: String! userId: String!
teamUuid: String
teamName: String teamName: String
countryCode: String countryCode: String
workflowStatus: String! workflowStatus: String!
@@ -21,6 +22,7 @@ export const userTypeDefs = `#graphql
} }
input KYCApplicationRussiaInput { input KYCApplicationRussiaInput {
teamUuid: String!
companyName: String! companyName: String!
companyFullName: String! companyFullName: String!
inn: String! inn: String!
@@ -81,6 +83,7 @@ async function getCountryData(app: { objectId: number | null }) {
} }
interface RussiaInput { interface RussiaInput {
teamUuid: string
companyName: string companyName: string
companyFullName: string companyFullName: string
inn: string inn: string
@@ -97,6 +100,8 @@ interface RussiaInput {
async function createApplicationRussia(_: unknown, args: { input: RussiaInput }, ctx: AuthContext) { async function createApplicationRussia(_: unknown, args: { input: RussiaInput }, ctx: AuthContext) {
if (!ctx.userId) throw new GraphQLError('Not authenticated') if (!ctx.userId) throw new GraphQLError('Not authenticated')
const teamUuid = args.input.teamUuid.trim()
if (teamUuid.length === 0) throw new GraphQLError('Team is required')
const details = await prisma.kYCDetailsRussia.create({ const details = await prisma.kYCDetailsRussia.create({
data: { data: {
@@ -117,6 +122,7 @@ async function createApplicationRussia(_: unknown, args: { input: RussiaInput },
const app = await prisma.kYCApplication.create({ const app = await prisma.kYCApplication.create({
data: { data: {
userId: ctx.userId, userId: ctx.userId,
teamUuid,
teamName: args.input.companyName, teamName: args.input.companyName,
countryCode: 'RU', countryCode: 'RU',
contactPerson: args.input.contactPerson, contactPerson: args.input.contactPerson,