Add client bonus access flag

This commit is contained in:
Ruslan Bakiev
2026-05-16 17:16:31 +07:00
parent c641a3dd23
commit fcc2eb7450
6 changed files with 151 additions and 43 deletions

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "bonusProgramEnabled" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -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?

View File

@@ -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,
},
});

View File

@@ -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,
},
});

View File

@@ -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.');
}

View File

@@ -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!