appointment_system/backend/src/services/commissionService.js
2025-12-24 01:01:17 +08:00

338 lines
9.6 KiB
JavaScript

const User = require('../models/User');
const Commission = require('../models/Commission');
const PaymentOrder = require('../models/PaymentOrder');
const { sequelize } = require('../config/database');
const commissionConfigService = require('./commissionConfigService');
/**
* Commission Service
* Handles commission calculation and management
*/
/**
* Calculate and record commission for a payment order
* @param {string} paymentOrderId - Payment order ID
* @returns {Object|null} Commission record or null if no commission generated
*/
const calculateCommission = async (paymentOrderId) => {
// Get payment order with user info
const paymentOrder = await PaymentOrder.findByPk(paymentOrderId, {
include: [{ model: User, as: 'user' }],
});
if (!paymentOrder) {
throw new Error('Payment order not found');
}
if (paymentOrder.status !== 'active') {
return null; // Don't calculate commission for cancelled orders
}
// Check if user was invited
const inviteeUser = paymentOrder.user;
if (!inviteeUser || !inviteeUser.invitedBy) {
return null; // User was not invited, no commission
}
// Get current commission rate
const commissionRate = await commissionConfigService.getCommissionRate();
// Calculate commission amount - prioritize RMB, then Peso, then legacy amount
// Commission is calculated based on whichever currency amount is available
let paymentAmount = 0;
let currency = 'RMB';
if (paymentOrder.amountRmb && parseFloat(paymentOrder.amountRmb) > 0) {
paymentAmount = parseFloat(paymentOrder.amountRmb);
currency = 'RMB';
} else if (paymentOrder.amountPeso && parseFloat(paymentOrder.amountPeso) > 0) {
paymentAmount = parseFloat(paymentOrder.amountPeso);
currency = 'PHP';
} else if (paymentOrder.amount && parseFloat(paymentOrder.amount) > 0) {
paymentAmount = parseFloat(paymentOrder.amount);
currency = 'RMB';
}
if (paymentAmount <= 0) {
return null; // No valid payment amount
}
const commissionAmount = paymentAmount * commissionRate;
// Start transaction
const transaction = await sequelize.transaction();
try {
// Create commission record with currency
const commission = await Commission.create({
inviterId: inviteeUser.invitedBy,
inviteeId: inviteeUser.id,
paymentOrderId: paymentOrder.id,
paymentAmount: paymentAmount,
commissionRate: commissionRate,
commissionAmount: commissionAmount,
currency: currency,
status: 'credited',
}, { transaction });
// Update inviter's balance based on currency
const inviter = await User.findByPk(inviteeUser.invitedBy, { transaction });
if (inviter) {
if (currency === 'PHP') {
inviter.balancePeso = parseFloat(inviter.balancePeso || 0) + commissionAmount;
} else {
inviter.balance = parseFloat(inviter.balance) + commissionAmount;
}
await inviter.save({ transaction });
}
await transaction.commit();
return commission;
} catch (error) {
await transaction.rollback();
throw error;
}
};
/**
* Get commission records for an inviter
* @param {string} inviterId - Inviter user ID
* @param {Object} options - Query options (page, limit)
* @returns {Object} Commission records with pagination
*/
const getCommissionsByInviter = async (inviterId, options = {}) => {
const { page = 1, limit = 20 } = options;
const offset = (page - 1) * limit;
const { count, rows } = await Commission.findAndCountAll({
where: { inviterId, status: 'credited' },
include: [
{
model: User,
as: 'invitee',
attributes: ['id', 'uid', 'nickname', 'avatar'],
},
{
model: PaymentOrder,
as: 'paymentOrder',
attributes: ['id', 'orderNo', 'serviceContent', 'paymentTime'],
},
],
order: [['createdAt', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset),
});
const records = rows.map(commission => ({
id: commission.id,
invitee: {
id: commission.invitee.id,
uid: commission.invitee.uid,
nickname: commission.invitee.nickname,
avatar: commission.invitee.avatar,
},
paymentOrder: {
id: commission.paymentOrder.id,
orderNo: commission.paymentOrder.orderNo,
serviceContent: commission.paymentOrder.serviceContent,
paymentTime: commission.paymentOrder.paymentTime,
},
paymentAmount: parseFloat(commission.paymentAmount).toFixed(2),
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
createdAt: commission.createdAt,
}));
return {
records,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: count,
totalPages: Math.ceil(count / limit),
},
};
};
/**
* Generate invitation code
* @returns {string} Invitation code
*/
const generateInvitationCode = () => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let code = '';
for (let i = 0; i < 6; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
};
/**
* Get commission statistics for an inviter
* @param {string} inviterId - Inviter user ID
* @returns {Object} Commission statistics
*/
const getCommissionStats = async (inviterId) => {
const user = await User.findByPk(inviterId);
if (!user) {
throw new Error('User not found');
}
// If user doesn't have invitation code, generate one
let invitationCode = user.invitationCode;
if (!invitationCode) {
invitationCode = generateInvitationCode();
await user.update({ invitationCode });
}
// Get total invites count
const totalInvites = await User.count({
where: { invitedBy: inviterId },
});
// Get commission statistics by currency
const commissions = await Commission.findAll({
where: { inviterId, status: 'credited' },
attributes: ['commissionAmount', 'currency'],
});
// Calculate total commission by currency
let totalCommissionRmb = 0;
let totalCommissionPeso = 0;
commissions.forEach(c => {
const amount = parseFloat(c.commissionAmount);
if (c.currency === 'PHP') {
totalCommissionPeso += amount;
} else {
totalCommissionRmb += amount;
}
});
// Get paid invites count (invitees who have made payments)
const paidInvitesCount = await Commission.count({
where: { inviterId, status: 'credited' },
distinct: true,
col: 'invitee_id',
});
return {
totalInvites,
paidInvites: paidInvitesCount,
totalCommission: totalCommissionRmb.toFixed(2),
totalCommissionRmb: totalCommissionRmb.toFixed(2),
totalCommissionPeso: totalCommissionPeso.toFixed(2),
commissionCount: commissions.length,
availableBalance: parseFloat(user.balance).toFixed(2),
availableBalanceRmb: parseFloat(user.balance).toFixed(2),
availableBalancePeso: parseFloat(user.balancePeso || 0).toFixed(2),
invitationCode: invitationCode,
};
};
/**
* Get all commission records (for admin)
* @param {Object} options - Query options (page, limit, inviterId, inviteeId)
* @returns {Object} Commission records with pagination
*/
const getAllCommissions = async (options = {}) => {
const { page = 1, limit = 20, inviterId, inviteeId, status } = options;
const offset = (page - 1) * limit;
const where = {};
if (inviterId) where.inviterId = inviterId;
if (inviteeId) where.inviteeId = inviteeId;
if (status) where.status = status;
const { count, rows } = await Commission.findAndCountAll({
where,
include: [
{
model: User,
as: 'inviter',
attributes: ['id', 'uid', 'nickname'],
},
{
model: User,
as: 'invitee',
attributes: ['id', 'uid', 'nickname'],
},
{
model: PaymentOrder,
as: 'paymentOrder',
attributes: ['id', 'orderNo', 'amount'],
},
],
order: [['createdAt', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset),
});
const records = rows.map(commission => ({
id: commission.id,
inviter: commission.inviter,
invitee: commission.invitee,
paymentOrder: commission.paymentOrder,
paymentAmount: parseFloat(commission.paymentAmount).toFixed(2),
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
status: commission.status,
createdAt: commission.createdAt,
}));
return {
records,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: count,
totalPages: Math.ceil(count / limit),
},
};
};
/**
* Get platform commission statistics (for admin)
* @returns {Object} Platform commission statistics
*/
const getPlatformCommissionStats = async () => {
// Total commission paid
const totalPaid = await Commission.sum('commissionAmount', {
where: { status: 'credited' },
}) || 0;
// Total commission count
const totalCount = await Commission.count({
where: { status: 'credited' },
});
// Get user balance statistics (pending withdrawal)
const users = await User.findAll({
where: sequelize.where(
sequelize.cast(sequelize.col('balance'), 'DECIMAL(10,2)'),
{ [require('sequelize').Op.gt]: 0 }
),
attributes: ['balance'],
});
const pendingWithdrawal = users.reduce(
(sum, u) => sum + parseFloat(u.balance),
0
);
return {
totalCommissionPaid: parseFloat(totalPaid).toFixed(2),
totalCommissionCount: totalCount,
pendingWithdrawal: pendingWithdrawal.toFixed(2),
};
};
module.exports = {
calculateCommission,
getCommissionsByInviter,
getCommissionStats,
getAllCommissions,
getPlatformCommissionStats,
};