Add client referral bonus links
This commit is contained in:
211
src/resolvers.js
211
src/resolvers.js
@@ -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),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user