diff --git a/prisma/migrations/0013_add_user_bonus_access/migration.sql b/prisma/migrations/0013_add_user_bonus_access/migration.sql new file mode 100644 index 0000000..e6702df --- /dev/null +++ b/prisma/migrations/0013_add_user_bonus_access/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "bonusProgramEnabled" BOOLEAN NOT NULL DEFAULT false; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 91dbccf..1333651 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -60,6 +60,7 @@ model User { email String @unique fullName String role UserRole + bonusProgramEnabled Boolean @default(false) companyId String? company Company? @relation(fields: [companyId], references: [id]) counterpartyProfile CounterpartyProfile? diff --git a/scripts/seed-demo.js b/scripts/seed-demo.js index b80bb26..cad4e98 100644 --- a/scripts/seed-demo.js +++ b/scripts/seed-demo.js @@ -161,12 +161,14 @@ async function upsertClient(index) { update: { fullName: fullNameForIndex(index), role: 'CLIENT', + bonusProgramEnabled: index % 2 === 1, companyId: company.id, }, create: { email: buildClientEmail(index), fullName: fullNameForIndex(index), role: 'CLIENT', + bonusProgramEnabled: index % 2 === 1, companyId: company.id, }, }); diff --git a/scripts/seed.js b/scripts/seed.js index ce88c20..ffd5c33 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -104,11 +104,16 @@ const manager = await prisma.user.upsert({ await prisma.user.upsert({ where: { email: clientEmail }, - update: { fullName: 'Demo Client', companyId: company.id }, + update: { + fullName: 'Demo Client', + companyId: company.id, + bonusProgramEnabled: true, + }, create: { email: clientEmail, fullName: 'Demo Client', role: 'CLIENT', + bonusProgramEnabled: true, companyId: company.id, }, }); diff --git a/src/resolvers.js b/src/resolvers.js index 19868e7..34bffac 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -148,10 +148,61 @@ function mapManagerReferralLink(link) { }; } +const managerUserInclude = { + messengerConnections: { + where: { + type: 'TELEGRAM', + isActive: true, + }, + orderBy: { createdAt: 'desc' }, + take: 1, + }, + counterpartyProfile: { + select: { + companyName: true, + inn: true, + }, + }, + clientOrders: { + select: { + createdAt: true, + }, + orderBy: { createdAt: 'desc' }, + take: 1, + }, + _count: { + select: { + clientOrders: true, + }, + }, +}; + +function mapManagerUser(user) { + return { + id: user.id, + email: user.email, + fullName: user.fullName, + role: user.role, + bonusProgramEnabled: user.bonusProgramEnabled, + companyName: user.counterpartyProfile?.companyName ?? null, + inn: user.counterpartyProfile?.inn ?? null, + createdAt: user.createdAt, + orderCount: user._count.clientOrders, + lastOrderAt: user.clientOrders[0]?.createdAt ?? null, + telegramConnection: user.messengerConnections[0] ?? null, + }; +} + async function createReferralBonusTransaction(prisma, order) { const referralLink = await prisma.referralLink.findFirst({ where: { refereeId: order.customerId, + referrer: { + bonusProgramEnabled: true, + }, + referee: { + bonusProgramEnabled: true, + }, }, include: { referrer: { @@ -1099,49 +1150,11 @@ export const resolvers = { const managedUsersWhere = await getManagedClientUserWhere(context.prisma, manager); const users = await context.prisma.user.findMany({ where: managedUsersWhere, - include: { - messengerConnections: { - where: { - type: 'TELEGRAM', - isActive: true, - }, - orderBy: { createdAt: 'desc' }, - take: 1, - }, - counterpartyProfile: { - select: { - companyName: true, - inn: true, - }, - }, - clientOrders: { - select: { - createdAt: true, - }, - orderBy: { createdAt: 'desc' }, - take: 1, - }, - _count: { - select: { - clientOrders: true, - }, - }, - }, + include: managerUserInclude, orderBy: { createdAt: 'desc' }, }); - return users.map((user) => ({ - id: user.id, - email: user.email, - fullName: user.fullName, - role: user.role, - companyName: user.counterpartyProfile?.companyName ?? null, - inn: user.counterpartyProfile?.inn ?? null, - createdAt: user.createdAt, - orderCount: user._count.clientOrders, - lastOrderAt: user.clientOrders[0]?.createdAt ?? null, - telegramConnection: user.messengerConnections[0] ?? null, - })); + return users.map(mapManagerUser); }, managerOrders: async (_, { status, customerId }, context) => { @@ -1197,7 +1210,11 @@ export const resolvers = { const [users, transactionsAgg, pendingWithdrawalsAgg] = await Promise.all([ context.prisma.user.findMany({ - where: managedUsersWhere, + where: { + ...managedUsersWhere, + role: 'CLIENT', + bonusProgramEnabled: true, + }, include: { counterpartyProfile: { select: { @@ -1241,6 +1258,7 @@ export const resolvers = { email: user.email, fullName: user.fullName, companyName: user.counterpartyProfile?.companyName ?? null, + bonusProgramEnabled: user.bonusProgramEnabled, balance: (tx?.balance ?? 0) - pendingWithdrawalAmount, pendingWithdrawalAmount, transactionsCount: tx?.transactionsCount ?? 0, @@ -1328,7 +1346,11 @@ export const resolvers = { const managedUsersWhere = await getManagedClientUserWhere(context.prisma, manager); const users = await context.prisma.user.findMany({ - where: managedUsersWhere, + where: { + ...managedUsersWhere, + role: 'CLIENT', + bonusProgramEnabled: true, + }, select: { id: true, email: true, @@ -2224,6 +2246,7 @@ export const resolvers = { select: { id: true, role: true, + bonusProgramEnabled: true, }, }); @@ -2235,6 +2258,10 @@ export const resolvers = { throw new Error('Referral links can only be created between client accounts.'); } + if (users.some((user) => !user.bonusProgramEnabled)) { + throw new Error('Bonus program must be enabled for both selected clients.'); + } + const existingRefereeLink = await context.prisma.referralLink.findFirst({ where: { refereeId: refereeUserId, @@ -2259,10 +2286,56 @@ export const resolvers = { }); }, + setClientBonusProgramEnabled: async (_, { userId, enabled }, context) => { + const manager = requireManagerAccess(context); + await assertManagerCanAccessUser(context.prisma, manager, userId); + + const existingUser = await context.prisma.user.findUnique({ + where: { id: userId }, + select: { + role: true, + }, + }); + + if (!existingUser) { + throw new Error('User was not found.'); + } + + if (existingUser.role !== 'CLIENT') { + throw new Error('Bonus program can only be configured for client accounts.'); + } + + const user = await context.prisma.user.update({ + where: { id: userId }, + data: { + bonusProgramEnabled: enabled, + }, + include: managerUserInclude, + }); + + return mapManagerUser(user); + }, + createBonusProgramLink: async (_, { userId }, context) => { const manager = requireManagerAccess(context); await assertManagerCanAccessUser(context.prisma, manager, userId); + const user = await context.prisma.user.findUnique({ + where: { id: userId }, + select: { + role: true, + bonusProgramEnabled: true, + }, + }); + + if (!user) { + throw new Error('User was not found.'); + } + + if (user.role !== 'CLIENT' || !user.bonusProgramEnabled) { + throw new Error('Bonus program is not enabled for this client.'); + } + const issued = issueBonusProgramLinkToken({ userId }); return { @@ -2276,6 +2349,22 @@ export const resolvers = { addBonusTransaction: async (_, { input }, context) => { const manager = requireManagerAccess(context); await assertManagerCanAccessUser(context.prisma, manager, input.userId); + const bonusUser = await context.prisma.user.findUnique({ + where: { id: input.userId }, + select: { + role: true, + bonusProgramEnabled: true, + }, + }); + + if (!bonusUser) { + throw new Error('User was not found.'); + } + + if (bonusUser.role !== 'CLIENT' || !bonusUser.bonusProgramEnabled) { + throw new Error('Bonus program is not enabled for this client.'); + } + const transaction = await context.prisma.bonusTransaction.create({ data: { userId: input.userId, @@ -2319,6 +2408,10 @@ export const resolvers = { requestRewardWithdrawal: (_, { input }, context) => { const client = requireUser(context); + if (!client.bonusProgramEnabled) { + throw new Error('Bonus program is not enabled for this client.'); + } + if (input.amount < 100) { throw new Error('Minimum withdrawal amount is 100.'); } diff --git a/src/schema.graphql b/src/schema.graphql index 245548a..1c14b12 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -63,6 +63,7 @@ type User { email: String! fullName: String! role: UserRole! + bonusProgramEnabled: Boolean! company: Company } @@ -142,6 +143,7 @@ type ManagerUser { email: String! fullName: String! role: UserRole! + bonusProgramEnabled: Boolean! companyName: String inn: String createdAt: DateTime! @@ -382,6 +384,7 @@ type ManagerBonusBalance { email: String! fullName: String! companyName: String + bonusProgramEnabled: Boolean! balance: Float! pendingWithdrawalAmount: Float! transactionsCount: Int! @@ -607,6 +610,7 @@ type Mutation { clientReviewOrder(orderId: ID!, decision: Decision!): Order! createReferral(input: CreateReferralInput!): ReferralLink! + setClientBonusProgramEnabled(userId: ID!, enabled: Boolean!): ManagerUser! createBonusProgramLink(userId: ID!): BonusProgramLink! addBonusTransaction(input: AddBonusTransactionInput!): BonusTransaction! requestRewardWithdrawal(input: RequestRewardWithdrawalInput!): RewardWithdrawalRequest!