Add client referral bonus links

This commit is contained in:
Ruslan Bakiev
2026-04-04 14:59:02 +07:00
parent 1bec782edd
commit 6c5b9ef98e
4 changed files with 280 additions and 22 deletions

View File

@@ -72,6 +72,79 @@ async function appendOrderEvent(prisma, orderId, status, actorUserId, note = nul
});
}
function formatPercent(value) {
return Number(value).toFixed(2).replace(/\.?0+$/, '');
}
async function createReferralBonusTransaction(prisma, order) {
const referralLink = await prisma.referralLink.findFirst({
where: {
refereeId: order.customerId,
},
include: {
referrer: {
select: {
fullName: true,
},
},
referee: {
select: {
fullName: true,
},
},
},
orderBy: { createdAt: 'desc' },
});
if (!referralLink) {
return null;
}
const totalPrice = Number(order.totalPrice ?? 0);
const bonusPercent = Number(referralLink.bonusPercent ?? 0);
if (!Number.isFinite(totalPrice) || totalPrice <= 0 || !Number.isFinite(bonusPercent) || bonusPercent <= 0) {
return null;
}
const amount = roundMoney((totalPrice * bonusPercent) / 100);
if (amount <= 0) {
return null;
}
const existingTransaction = await prisma.bonusTransaction.findFirst({
where: {
orderId: order.id,
referralLinkId: referralLink.id,
},
});
if (existingTransaction) {
return {
transaction: existingTransaction,
referralLink,
isNew: false,
};
}
const transaction = await prisma.bonusTransaction.create({
data: {
userId: referralLink.referrerId,
amount,
reason: `Бонус ${formatPercent(bonusPercent)}% за заказ ${order.code} клиента ${referralLink.referee.fullName}`,
orderId: order.id,
referralLinkId: referralLink.id,
},
});
return {
transaction,
referralLink,
isNew: true,
};
}
function orderCode() {
return `FR-${Date.now()}-${crypto.randomInt(1000, 9999)}`;
}
@@ -628,6 +701,49 @@ export const resolvers = {
});
},
managerReferralLinks: async (_, __, context) => {
requireManagerAccess(context);
const links = await context.prisma.referralLink.findMany({
include: {
referrer: {
include: {
counterpartyProfile: {
select: {
companyName: true,
},
},
},
},
referee: {
include: {
counterpartyProfile: {
select: {
companyName: true,
},
},
},
},
},
orderBy: { createdAt: 'desc' },
});
return links.map((link) => ({
id: link.id,
referrerId: link.referrerId,
referrerName: link.referrer.fullName,
referrerEmail: link.referrer.email,
referrerCompanyName: link.referrer.counterpartyProfile?.companyName ?? null,
refereeId: link.refereeId,
refereeName: link.referee.fullName,
refereeEmail: link.referee.email,
refereeCompanyName: link.referee.counterpartyProfile?.companyName ?? null,
createdById: link.createdById,
bonusPercent: Number(link.bonusPercent),
createdAt: link.createdAt,
}));
},
managerBonusBalances: async (_, __, context) => {
const manager = requireManagerAccess(context);
const managedUsersWhere = await getManagedClientUserWhere(context.prisma, manager);
@@ -1624,29 +1740,102 @@ export const resolvers = {
throw new Error('Only in-progress order can be completed.');
}
const updated = await context.prisma.order.update({
where: { id: orderId },
data: {
managerId: manager.id,
status: 'COMPLETED',
},
const { updated, referralBonus } = await context.prisma.$transaction(async (tx) => {
const updatedOrder = await tx.order.update({
where: { id: orderId },
data: {
managerId: manager.id,
status: 'COMPLETED',
},
});
const createdReferralBonus = await createReferralBonusTransaction(tx, updatedOrder);
return {
updated: updatedOrder,
referralBonus: createdReferralBonus,
};
});
await appendOrderEvent(context.prisma, updated.id, 'COMPLETED', manager.id, 'Order completed');
await notifyOrderStakeholders(context, updated, 'COMPLETED', 'Order completed');
if (referralBonus?.isNew) {
await dispatchToUserConnections(
context.prisma,
referralBonus.transaction.userId,
`Начислен бонус: ${toFloat(referralBonus.transaction.amount)} за заказ ${updated.code}.`,
);
}
return context.prisma.order.findUnique({
where: { id: updated.id },
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
});
},
createReferral: (_, { input }, context) => {
createReferral: async (_, { input }, context) => {
const manager = requireManagerAccess(context);
const referrerUserId = normalizeText(input.referrerUserId);
const refereeUserId = normalizeText(input.refereeUserId);
const bonusPercent = roundMoney(input.bonusPercent);
if (!referrerUserId || !refereeUserId) {
throw new Error('Both linked clients are required.');
}
if (referrerUserId === refereeUserId) {
throw new Error('A client cannot be linked to themselves.');
}
if (!Number.isFinite(bonusPercent) || bonusPercent <= 0 || bonusPercent > 100) {
throw new Error('Bonus percent must be greater than 0 and no more than 100.');
}
await Promise.all([
assertManagerCanAccessUser(context.prisma, manager, referrerUserId),
assertManagerCanAccessUser(context.prisma, manager, refereeUserId),
]);
const users = await context.prisma.user.findMany({
where: {
id: { in: [referrerUserId, refereeUserId] },
},
select: {
id: true,
role: true,
},
});
if (users.length !== 2) {
throw new Error('Both clients must exist.');
}
if (users.some((user) => user.role !== 'CLIENT')) {
throw new Error('Referral links can only be created between client accounts.');
}
const existingRefereeLink = await context.prisma.referralLink.findFirst({
where: {
refereeId: refereeUserId,
},
});
if (existingRefereeLink) {
if (existingRefereeLink.referrerId === referrerUserId) {
throw new Error('These clients are already linked.');
}
throw new Error('The selected client is already linked to another bonus program participant.');
}
return context.prisma.referralLink.create({
data: {
referrerId: manager.id,
refereeId: input.refereeUserId,
referrerId: referrerUserId,
refereeId: refereeUserId,
createdById: manager.id,
bonusPercent,
},
});
},
@@ -1744,6 +1933,10 @@ export const resolvers = {
amount: (tx) => toFloat(tx.amount),
},
ReferralLink: {
bonusPercent: (link) => toFloat(link.bonusPercent),
},
RewardWithdrawalRequest: {
amount: (tx) => toFloat(tx.amount),
},