diff --git a/graphql-contracts b/graphql-contracts index 2080dce..5ff41d5 160000 --- a/graphql-contracts +++ b/graphql-contracts @@ -1 +1 @@ -Subproject commit 2080dce5b4851b474cead1b2dc3f95882019bbf8 +Subproject commit 5ff41d56f8097a59842b65574cadbc3b697aba39 diff --git a/prisma/migrations/1_link_kyc_to_team/migration.sql b/prisma/migrations/1_link_kyc_to_team/migration.sql new file mode 100644 index 0000000..7454928 --- /dev/null +++ b/prisma/migrations/1_link_kyc_to_team/migration.sql @@ -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"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6afdb60..611c7fd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,6 +10,7 @@ model KYCApplication { id Int @id @default(autoincrement()) uuid String @unique @default(uuid()) 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) countryCode String @default("") @map("country_code") @db.VarChar(2) workflowStatus String @default("pending") @map("workflow_status") @db.VarChar(20) @@ -25,6 +26,7 @@ model KYCApplication { updatedAt DateTime @updatedAt @map("updated_at") @@index([userId]) + @@index([teamUuid]) @@map("kyc_requests") } @@ -32,6 +34,7 @@ model KYCProfile { id Int @id @default(autoincrement()) uuid String @unique @default(uuid()) 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) countryCode String @default("") @map("country_code") @db.VarChar(2) workflowStatus String @default("pending") @map("workflow_status") @db.VarChar(20) @@ -47,6 +50,7 @@ model KYCProfile { updatedAt DateTime @updatedAt @map("updated_at") @@index([userId]) + @@index([teamUuid]) @@map("kyc_monitoring") } diff --git a/src/schemas/m2m.ts b/src/schemas/m2m.ts index c16a2c1..0c57975 100644 --- a/src/schemas/m2m.ts +++ b/src/schemas/m2m.ts @@ -3,7 +3,7 @@ import { prisma } from '../db.js' export const m2mTypeDefs = `#graphql type KycPricingContext { - userId: String! + teamUuid: String! profileUuid: String! countryCode: String! resolvedAt: String! @@ -17,19 +17,19 @@ export const m2mTypeDefs = `#graphql type Query { health: String! - pricingContextForUser(userId: String!): KycPricingContext + pricingContextForTeam(teamUuid: String!): KycPricingContext } type Mutation { createKycProfile(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') - .update(`kyc:demo-pricing-context:${userId}`) + .update(`kyc:demo-pricing-context:${teamUuid}`) .digest('hex') .slice(0, 32) .split('') @@ -48,16 +48,19 @@ function normalizedCountryCode(countryCode: string): string { } function mapPricingContext(profile: { - userId: string + teamUuid: string | null uuid: string countryCode: string 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) { throw new Error(`KYC pricing profile ${profile.uuid} has no valid country code`) } return { - userId: profile.userId, + teamUuid: profile.teamUuid, profileUuid: profile.uuid, countryCode: profile.countryCode.toUpperCase(), resolvedAt: profile.updatedAt.toISOString(), @@ -65,51 +68,50 @@ function mapPricingContext(profile: { } async function createProfile(_: unknown, args: { kycApplicationId: string }) { - try { - const app = await prisma.kYCApplication.findUnique({ - where: { uuid: args.kycApplicationId }, - }) + 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) } + if (!app) { + return { success: false, profileUuid: '', message: `KYCApplication not found: ${args.kycApplicationId}` } } + 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({ where: { - userId: args.userId, + teamUuid: args.teamUuid, workflowStatus: { in: ['active', 'approved', 'demo'], }, @@ -122,13 +124,15 @@ async function pricingContextForUser(_: unknown, args: { userId: string }) { return mapPricingContext(profile) } -async function ensureDemoPricingContextForUser( +async function ensureDemoPricingContextForTeam( _: 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({ where: { - userId: args.userId, + teamUuid, workflowStatus: { in: ['active', 'approved', 'demo'], }, @@ -140,18 +144,20 @@ async function ensureDemoPricingContextForUser( if (existing !== null) return mapPricingContext(existing) const countryCode = normalizedCountryCode(args.countryCode) - const profileUuid = demoProfileUuid(args.userId) + const profileUuid = demoProfileUuid(teamUuid) const profile = await prisma.kYCProfile.upsert({ where: { uuid: profileUuid }, create: { uuid: profileUuid, userId: args.userId, + teamUuid, teamName: 'Demo workspace', countryCode, workflowStatus: 'demo', }, update: { userId: args.userId, + teamUuid, teamName: 'Demo workspace', countryCode, workflowStatus: 'demo', @@ -163,11 +169,11 @@ async function ensureDemoPricingContextForUser( export const m2mResolvers = { Query: { health: () => 'ok', - pricingContextForUser, + pricingContextForTeam, }, Mutation: { createKycProfile: createProfile, createKycMonitoring: createProfile, - ensureDemoPricingContextForUser, + ensureDemoPricingContextForTeam, }, } diff --git a/src/schemas/manager.ts b/src/schemas/manager.ts index 0ecd6d3..1f3f7bc 100644 --- a/src/schemas/manager.ts +++ b/src/schemas/manager.ts @@ -7,6 +7,7 @@ export const managerTypeDefs = `#graphql id: Int! uuid: String! userId: String! + teamUuid: String teamName: String countryCode: String workflowStatus: String! diff --git a/src/schemas/user.ts b/src/schemas/user.ts index 5fb30f2..48e80d2 100644 --- a/src/schemas/user.ts +++ b/src/schemas/user.ts @@ -7,6 +7,7 @@ export const userTypeDefs = `#graphql id: Int! uuid: String! userId: String! + teamUuid: String teamName: String countryCode: String workflowStatus: String! @@ -21,6 +22,7 @@ export const userTypeDefs = `#graphql } input KYCApplicationRussiaInput { + teamUuid: String! companyName: String! companyFullName: String! inn: String! @@ -81,6 +83,7 @@ async function getCountryData(app: { objectId: number | null }) { } interface RussiaInput { + teamUuid: string companyName: string companyFullName: string inn: string @@ -97,6 +100,8 @@ interface RussiaInput { async function createApplicationRussia(_: unknown, args: { input: RussiaInput }, ctx: AuthContext) { 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({ data: { @@ -117,6 +122,7 @@ async function createApplicationRussia(_: unknown, args: { input: RussiaInput }, const app = await prisma.kYCApplication.create({ data: { userId: ctx.userId, + teamUuid, teamName: args.input.companyName, countryCode: 'RU', contactPerson: args.input.contactPerson,