This commit is contained in:
18631081161 2025-12-24 15:09:56 +08:00
parent 784e373c7e
commit c79e164f87
6 changed files with 2135 additions and 128 deletions

View File

@ -2,15 +2,23 @@
<div class="commissions-container">
<!-- Statistics Cards -->
<el-row :gutter="20" class="stats-row">
<el-col :span="8">
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-value">¥{{ stats.totalCommissionPaid }}</div>
<div class="stat-label">累计佣金支出</div>
<div class="stat-value rmb">¥{{ stats.totalCommissionPaidRmb || stats.totalCommissionPaid }}</div>
<div class="stat-label">累计佣金支出(人民币)</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-value peso">{{ stats.totalCommissionPaidPeso || '0.00' }}</div>
<div class="stat-label">累计佣金支出(比索)</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-value">{{ stats.totalCommissionCount }}</div>
@ -18,10 +26,11 @@
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-value">¥{{ stats.pendingWithdrawal }}</div>
<div class="stat-value rmb">¥{{ stats.pendingWithdrawalRmb || stats.pendingWithdrawal }}</div>
<div class="stat-value peso">{{ stats.pendingWithdrawalPeso || '0.00' }}</div>
<div class="stat-label">待提现金额</div>
</div>
</el-card>
@ -93,13 +102,20 @@
</el-table-column>
<el-table-column prop="paymentAmount" label="支付金额" width="120">
<template #default="{ row }">
<span class="payment-amount">¥{{ row.paymentAmount }}</span>
<span class="payment-amount">{{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.paymentAmount }}</span>
</template>
</el-table-column>
<el-table-column prop="commissionRate" label="佣金比例" width="100" />
<el-table-column prop="commissionAmount" label="佣金金额" width="120">
<template #default="{ row }">
<span class="commission-amount">¥{{ row.commissionAmount }}</span>
<span class="commission-amount">{{ row.currency === 'PHP' ? '₱' : '¥' }}{{ row.commissionAmount }}</span>
</template>
</el-table-column>
<el-table-column prop="currency" label="货币" width="80">
<template #default="{ row }">
<el-tag :type="row.currency === 'PHP' ? 'success' : 'warning'" size="small">
{{ row.currency === 'PHP' ? '比索' : '人民币' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
@ -271,6 +287,15 @@ onMounted(() => {
font-size: 28px;
font-weight: bold;
color: #409eff;
&.rmb {
color: #e6a23c;
}
&.peso {
color: #67c23a;
font-size: 20px;
}
}
.stat-label {
@ -304,6 +329,10 @@ onMounted(() => {
.payment-amount {
color: #e6a23c;
&.peso {
color: #67c23a;
}
}
.commission-amount {

View File

@ -78,9 +78,18 @@
{{ formatDate(row.paymentTime) }}
</template>
</el-table-column>
<el-table-column label="佣金" width="120">
<el-table-column label="佣金" width="150">
<template #default="{ row }">
<span v-if="row.commission" class="commission">¥{{ row.commission.amount }}</span>
<div v-if="row.commissions && row.commissions.length > 0" class="commissions-info">
<span
v-for="(comm, index) in row.commissions"
:key="index"
class="commission-item"
:class="{ 'peso': comm.currency === 'PHP', 'rmb': comm.currency !== 'PHP' }"
>
{{ comm.currency === 'PHP' ? '₱' : '¥' }}{{ comm.amount }}
</span>
</div>
<span v-else class="no-commission"></span>
</template>
</el-table-column>
@ -189,17 +198,35 @@
</el-descriptions-item>
</el-descriptions>
<el-divider v-if="currentOrder?.commission">佣金信息</el-divider>
<el-descriptions v-if="currentOrder?.commission" :column="2" border>
<el-descriptions-item label="邀请人">{{ currentOrder.commission.inviter?.nickname }} (UID: {{ currentOrder.commission.inviter?.uid }})</el-descriptions-item>
<el-descriptions-item label="佣金比例">{{ currentOrder.commission.commissionRate }}</el-descriptions-item>
<el-descriptions-item label="佣金金额">¥{{ currentOrder.commission.commissionAmount }}</el-descriptions-item>
<el-descriptions-item label="佣金状态">
<el-tag :type="currentOrder.commission.status === 'credited' ? 'success' : 'danger'">
{{ currentOrder.commission.status === 'credited' ? '已入账' : '已取消' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<el-divider v-if="currentOrder?.commissions && currentOrder.commissions.length > 0">佣金信息</el-divider>
<div v-if="currentOrder?.commissions && currentOrder.commissions.length > 0">
<el-descriptions
v-for="(comm, index) in currentOrder.commissions"
:key="comm.id || index"
:column="2"
border
:class="{ 'commission-section': index > 0 }"
>
<el-descriptions-item label="货币类型">
<el-tag :type="comm.currency === 'PHP' ? 'primary' : 'warning'" size="small">
{{ comm.currency === 'PHP' ? '比索 (PHP)' : '人民币 (RMB)' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="佣金状态">
<el-tag :type="comm.status === 'credited' ? 'success' : 'danger'">
{{ comm.status === 'credited' ? '已入账' : '已取消' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="邀请人">{{ comm.inviter?.nickname || '-' }} (UID: {{ comm.inviter?.uid || '-' }})</el-descriptions-item>
<el-descriptions-item label="佣金比例">{{ comm.commissionRate }}</el-descriptions-item>
<el-descriptions-item label="佣金金额">
<span :class="{ 'peso-amount': comm.currency === 'PHP', 'rmb-amount': comm.currency !== 'PHP' }">
{{ comm.currency === 'PHP' ? '₱' : '¥' }}{{ comm.commissionAmount }}
</span>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(comm.createdAt) }}</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
</div>
</template>
@ -370,8 +397,14 @@ async function handleCreate() {
createDialogVisible.value = false
fetchOrders()
if (response.data.data.commission) {
ElMessage.info(`已生成佣金 ¥${response.data.data.commission.commissionAmount}`)
// Show commission info for all created commissions
const commissions = response.data.data.commissions
if (commissions && commissions.length > 0) {
const commissionMessages = commissions.map(c => {
const currencySymbol = c.currency === 'PHP' ? '₱' : '¥'
return `${currencySymbol}${c.commissionAmount}`
})
ElMessage.info(`已生成佣金: ${commissionMessages.join(', ')}`)
}
} else {
ElMessage.error(response.data.error?.message || '创建失败')
@ -510,6 +543,24 @@ onMounted(() => {
font-weight: bold;
}
.commissions-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.commission-item {
font-weight: bold;
&.peso {
color: #409eff;
}
&.rmb {
color: #e6a23c;
}
}
.no-commission {
color: #909399;
}
@ -551,5 +602,21 @@ onMounted(() => {
.cancel-reason {
color: #f56c6c;
}
.commission-section {
margin-top: 16px;
}
.peso-amount {
color: #409eff;
font-weight: bold;
font-size: 16px;
}
.rmb-amount {
color: #e6a23c;
font-weight: bold;
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,175 @@
/**
* Script to fix dual-currency commissions for existing payment orders
*
* This script finds payment orders that have both amountRmb and amountPeso,
* but only have one commission record, and creates the missing commission.
*
* Usage: node src/scripts/fixDualCurrencyCommissions.js [--dry-run]
*/
const { sequelize } = require('../config/database');
const PaymentOrder = require('../models/PaymentOrder');
const Commission = require('../models/Commission');
const User = require('../models/User');
const commissionConfigService = require('../services/commissionConfigService');
const isDryRun = process.argv.includes('--dry-run');
async function fixDualCurrencyCommissions() {
console.log('=== Fix Dual Currency Commissions ===');
console.log(`Mode: ${isDryRun ? 'DRY RUN (no changes will be made)' : 'LIVE'}`);
console.log('');
try {
// Find all active payment orders with both currencies
const orders = await PaymentOrder.findAll({
where: {
status: 'active',
},
include: [
{
model: User,
as: 'user',
attributes: ['id', 'invitedBy'],
},
],
});
console.log(`Found ${orders.length} active payment orders`);
let fixedCount = 0;
let skippedCount = 0;
for (const order of orders) {
const amountRmb = parseFloat(order.amountRmb || 0);
const amountPeso = parseFloat(order.amountPeso || 0);
const legacyAmount = parseFloat(order.amount || 0);
// Check if order has both currencies
const hasRmb = amountRmb > 0 || legacyAmount > 0;
const hasPeso = amountPeso > 0;
if (!hasRmb && !hasPeso) {
continue; // No amounts, skip
}
// Check if user was invited
if (!order.user || !order.user.invitedBy) {
continue; // No inviter, skip
}
// Get existing commissions for this order
const existingCommissions = await Commission.findAll({
where: { paymentOrderId: order.id },
});
const existingCurrencies = existingCommissions.map(c => c.currency || 'RMB');
const hasRmbCommission = existingCurrencies.includes('RMB');
const hasPhpCommission = existingCurrencies.includes('PHP');
// Check if we need to create missing commissions
const needsRmbCommission = hasRmb && !hasRmbCommission;
const needsPhpCommission = hasPeso && !hasPhpCommission;
if (!needsRmbCommission && !needsPhpCommission) {
continue; // All commissions exist, skip
}
console.log(`\nOrder ${order.orderNo}:`);
console.log(` - amountRmb: ${amountRmb}, amountPeso: ${amountPeso}, amount: ${legacyAmount}`);
console.log(` - Existing commissions: ${existingCurrencies.join(', ') || 'none'}`);
console.log(` - Needs RMB commission: ${needsRmbCommission}`);
console.log(` - Needs PHP commission: ${needsPhpCommission}`);
if (isDryRun) {
console.log(` - [DRY RUN] Would create missing commissions`);
fixedCount++;
continue;
}
// Get commission rate (use existing rate if available, otherwise get current rate)
let commissionRate;
if (existingCommissions.length > 0) {
commissionRate = parseFloat(existingCommissions[0].commissionRate);
} else {
commissionRate = await commissionConfigService.getCommissionRate();
}
const transaction = await sequelize.transaction();
try {
const inviter = await User.findByPk(order.user.invitedBy, { transaction });
if (needsRmbCommission) {
const rmbPaymentAmount = amountRmb > 0 ? amountRmb : legacyAmount;
const rmbCommissionAmount = rmbPaymentAmount * commissionRate;
await Commission.create({
inviterId: order.user.invitedBy,
inviteeId: order.user.id,
paymentOrderId: order.id,
paymentAmount: rmbPaymentAmount,
commissionRate: commissionRate,
commissionAmount: rmbCommissionAmount,
currency: 'RMB',
status: 'credited',
}, { transaction });
if (inviter) {
inviter.balance = parseFloat(inviter.balance || 0) + rmbCommissionAmount;
}
console.log(` - Created RMB commission: ¥${rmbCommissionAmount.toFixed(2)}`);
}
if (needsPhpCommission) {
const phpCommissionAmount = amountPeso * commissionRate;
await Commission.create({
inviterId: order.user.invitedBy,
inviteeId: order.user.id,
paymentOrderId: order.id,
paymentAmount: amountPeso,
commissionRate: commissionRate,
commissionAmount: phpCommissionAmount,
currency: 'PHP',
status: 'credited',
}, { transaction });
if (inviter) {
inviter.balancePeso = parseFloat(inviter.balancePeso || 0) + phpCommissionAmount;
}
console.log(` - Created PHP commission: ₱${phpCommissionAmount.toFixed(2)}`);
}
if (inviter) {
await inviter.save({ transaction });
}
await transaction.commit();
fixedCount++;
console.log(` - ✓ Fixed successfully`);
} catch (error) {
await transaction.rollback();
console.error(` - ✗ Error: ${error.message}`);
skippedCount++;
}
}
console.log('\n=== Summary ===');
console.log(`Fixed: ${fixedCount}`);
console.log(`Skipped/Errors: ${skippedCount}`);
if (isDryRun) {
console.log('\nThis was a dry run. Run without --dry-run to apply changes.');
}
} catch (error) {
console.error('Error:', error);
} finally {
await sequelize.close();
}
}
fixDualCurrencyCommissions();

View File

@ -10,9 +10,10 @@ const commissionConfigService = require('./commissionConfigService');
*/
/**
* Calculate and record commission for a payment order
* Calculate and record commissions for a payment order
* Creates separate commission records for each currency present (RMB and/or PHP)
* @param {string} paymentOrderId - Payment order ID
* @returns {Object|null} Commission record or null if no commission generated
* @returns {Array<Object>|null} Array of commission records or null if no commission generated
*/
const calculateCommission = async (paymentOrderId) => {
// Get payment order with user info
@ -37,57 +38,70 @@ const calculateCommission = async (paymentOrderId) => {
// 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';
// Collect all currency amounts to process
const currencyAmounts = [];
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';
// Check RMB amount (including legacy 'amount' field)
const rmbAmount = parseFloat(paymentOrder.amountRmb || 0) || parseFloat(paymentOrder.amount || 0);
if (rmbAmount > 0) {
currencyAmounts.push({ currency: 'RMB', paymentAmount: rmbAmount });
}
if (paymentAmount <= 0) {
return null; // No valid payment amount
// Check Peso amount
const pesoAmount = parseFloat(paymentOrder.amountPeso || 0);
if (pesoAmount > 0) {
currencyAmounts.push({ currency: 'PHP', paymentAmount: pesoAmount });
}
if (currencyAmounts.length === 0) {
return null; // No valid payment amounts
}
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 commissions = [];
// Get inviter for balance updates
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;
// Create commission record for each currency
for (const { currency, paymentAmount } of currencyAmounts) {
const commissionAmount = paymentAmount * commissionRate;
// Create commission record
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 });
commissions.push(commission);
// Update inviter's balance based on currency
if (inviter) {
if (currency === 'PHP') {
inviter.balancePeso = parseFloat(inviter.balancePeso || 0) + commissionAmount;
} else {
inviter.balance = parseFloat(inviter.balance || 0) + commissionAmount;
}
}
}
// Save inviter balance updates
if (inviter) {
await inviter.save({ transaction });
}
await transaction.commit();
return commission;
return commissions;
} catch (error) {
await transaction.rollback();
throw error;
@ -376,6 +390,7 @@ const getAllCommissions = async (options = {}) => {
paymentAmount: parseFloat(commission.paymentAmount).toFixed(2),
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
currency: commission.currency || 'RMB',
status: commission.status,
createdAt: commission.createdAt,
}));
@ -396,34 +411,52 @@ const getAllCommissions = async (options = {}) => {
* @returns {Object} Platform commission statistics
*/
const getPlatformCommissionStats = async () => {
// Total commission paid
const totalPaid = await Commission.sum('commissionAmount', {
const Op = require('sequelize').Op;
// Total commission paid by currency
const commissions = await Commission.findAll({
where: { status: 'credited' },
}) || 0;
attributes: ['commissionAmount', 'currency'],
raw: true,
});
let totalPaidRmb = 0;
let totalPaidPeso = 0;
commissions.forEach(c => {
const amount = parseFloat(c.commissionAmount) || 0;
if (c.currency === 'PHP') {
totalPaidPeso += amount;
} else {
totalPaidRmb += amount;
}
});
// Total commission count
const totalCount = await Commission.count({
where: { status: 'credited' },
});
// Get user balance statistics (pending withdrawal)
// Get user balance statistics (pending withdrawal) - dual currency
const users = await User.findAll({
where: sequelize.where(
sequelize.cast(sequelize.col('balance'), 'DECIMAL(10,2)'),
{ [require('sequelize').Op.gt]: 0 }
),
attributes: ['balance'],
attributes: ['balance', 'balancePeso'],
raw: true,
});
const pendingWithdrawal = users.reduce(
(sum, u) => sum + parseFloat(u.balance),
0
);
let pendingWithdrawalRmb = 0;
let pendingWithdrawalPeso = 0;
users.forEach(u => {
pendingWithdrawalRmb += parseFloat(u.balance) || 0;
pendingWithdrawalPeso += parseFloat(u.balancePeso) || 0;
});
return {
totalCommissionPaid: parseFloat(totalPaid).toFixed(2),
totalCommissionPaid: parseFloat(totalPaidRmb).toFixed(2),
totalCommissionPaidRmb: parseFloat(totalPaidRmb).toFixed(2),
totalCommissionPaidPeso: parseFloat(totalPaidPeso).toFixed(2),
totalCommissionCount: totalCount,
pendingWithdrawal: pendingWithdrawal.toFixed(2),
pendingWithdrawal: pendingWithdrawalRmb.toFixed(2),
pendingWithdrawalRmb: pendingWithdrawalRmb.toFixed(2),
pendingWithdrawalPeso: pendingWithdrawalPeso.toFixed(2),
};
};

View File

@ -117,16 +117,28 @@ const createPaymentOrder = async (data, adminId) => {
});
// Calculate commission (if applicable, for any currency)
let commission = null;
// commissionService.calculateCommission now returns an array of commissions
let commissions = null;
if (totalAmountForCommission > 0) {
try {
commission = await commissionService.calculateCommission(paymentOrder.id);
commissions = await commissionService.calculateCommission(paymentOrder.id);
} catch (error) {
// Log error but don't fail the order creation
console.error('Commission calculation error:', error.message);
}
}
// Format commissions array for response
const formattedCommissions = commissions && Array.isArray(commissions) && commissions.length > 0
? commissions.map(c => ({
id: c.id,
inviterId: c.inviterId,
commissionAmount: parseFloat(c.commissionAmount).toFixed(2),
commissionRate: `${(parseFloat(c.commissionRate) * 100).toFixed(2)}%`,
currency: c.currency || 'RMB',
}))
: null;
return {
paymentOrder: {
id: paymentOrder.id,
@ -142,12 +154,9 @@ const createPaymentOrder = async (data, adminId) => {
status: paymentOrder.status,
createdAt: paymentOrder.createdAt,
},
commission: commission ? {
id: commission.id,
inviterId: commission.inviterId,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
} : null,
commissions: formattedCommissions,
// Keep backward compatibility with single commission field
commission: formattedCommissions && formattedCommissions.length > 0 ? formattedCommissions[0] : null,
};
};
@ -232,37 +241,50 @@ const getPaymentOrders = async (options = {}) => {
totalPeso: parseFloat(statisticsResult?.totalPeso || 0).toFixed(2),
};
// Get commission status for each order
// Get all commissions for each order (dual-currency support)
const Commission = require('../models/Commission');
const orderIds = rows.map(o => o.id);
const commissions = await Commission.findAll({
where: { paymentOrderId: orderIds },
attributes: ['paymentOrderId', 'commissionAmount', 'status'],
attributes: ['paymentOrderId', 'commissionAmount', 'status', 'currency'],
});
const commissionMap = {};
// Build a map of orderId -> array of commissions
const commissionsMap = {};
commissions.forEach(c => {
commissionMap[c.paymentOrderId] = {
const orderId = c.paymentOrderId;
if (!commissionsMap[orderId]) {
commissionsMap[orderId] = [];
}
commissionsMap[orderId].push({
amount: parseFloat(c.commissionAmount).toFixed(2),
status: c.status,
};
currency: c.currency || 'RMB',
});
});
const records = rows.map(order => ({
id: order.id,
orderNo: order.orderNo,
user: order.user,
appointment: order.appointment,
amount: order.amount ? parseFloat(order.amount).toFixed(2) : null,
amountPeso: order.amountPeso ? parseFloat(order.amountPeso).toFixed(2) : null,
amountRmb: order.amountRmb ? parseFloat(order.amountRmb).toFixed(2) : null,
serviceContent: order.serviceContent,
paymentTime: order.paymentTime,
notes: order.notes,
status: order.status,
creator: order.creator,
commission: commissionMap[order.id] || null,
createdAt: order.createdAt,
}));
const records = rows.map(order => {
const orderCommissions = commissionsMap[order.id] || [];
return {
id: order.id,
orderNo: order.orderNo,
user: order.user,
appointment: order.appointment,
amount: order.amount ? parseFloat(order.amount).toFixed(2) : null,
amountPeso: order.amountPeso ? parseFloat(order.amountPeso).toFixed(2) : null,
amountRmb: order.amountRmb ? parseFloat(order.amountRmb).toFixed(2) : null,
serviceContent: order.serviceContent,
paymentTime: order.paymentTime,
notes: order.notes,
status: order.status,
creator: order.creator,
// Return array of all commissions for dual-currency support
commissions: orderCommissions.length > 0 ? orderCommissions : null,
// Keep backward compatibility with single commission field (first commission)
commission: orderCommissions.length > 0 ? orderCommissions[0] : null,
createdAt: order.createdAt,
};
});
return {
records,
@ -306,9 +328,9 @@ const getPaymentOrderById = async (orderId) => {
throw new Error('Payment order not found');
}
// Get commission info
// Get all commission info (dual-currency support)
const Commission = require('../models/Commission');
const commission = await Commission.findOne({
const commissions = await Commission.findAll({
where: { paymentOrderId: orderId },
include: [
{
@ -319,6 +341,19 @@ const getPaymentOrderById = async (orderId) => {
],
});
// Format commissions array
const formattedCommissions = commissions.length > 0
? commissions.map(c => ({
id: c.id,
inviter: c.inviter,
commissionAmount: parseFloat(c.commissionAmount).toFixed(2),
commissionRate: `${(parseFloat(c.commissionRate) * 100).toFixed(2)}%`,
currency: c.currency || 'RMB',
status: c.status,
createdAt: c.createdAt,
}))
: null;
return {
id: order.id,
orderNo: order.orderNo,
@ -333,14 +368,10 @@ const getPaymentOrderById = async (orderId) => {
status: order.status,
cancelReason: order.cancelReason,
creator: order.creator,
commission: commission ? {
id: commission.id,
inviter: commission.inviter,
commissionAmount: parseFloat(commission.commissionAmount).toFixed(2),
commissionRate: `${(parseFloat(commission.commissionRate) * 100).toFixed(2)}%`,
status: commission.status,
createdAt: commission.createdAt,
} : null,
// Return array of all commissions for dual-currency support
commissions: formattedCommissions,
// Keep backward compatibility with single commission field (first commission)
commission: formattedCommissions && formattedCommissions.length > 0 ? formattedCommissions[0] : null,
createdAt: order.createdAt,
updatedAt: order.updatedAt,
};
@ -375,25 +406,59 @@ const cancelPaymentOrder = async (orderId, cancelReason) => {
order.cancelReason = cancelReason.trim();
await order.save({ transaction });
// Cancel related commission and revert balance
// Find ALL related commissions (dual-currency support)
const Commission = require('../models/Commission');
const commission = await Commission.findOne({
const commissions = await Commission.findAll({
where: { paymentOrderId: orderId, status: 'credited' },
transaction,
});
if (commission) {
// Revert inviter's balance
const inviter = await User.findByPk(commission.inviterId, { transaction });
if (inviter) {
inviter.balance = parseFloat(inviter.balance) - parseFloat(commission.commissionAmount);
if (inviter.balance < 0) inviter.balance = 0;
await inviter.save({ transaction });
}
let commissionsCancelled = 0;
// Cancel commission
commission.status = 'cancelled';
await commission.save({ transaction });
if (commissions.length > 0) {
// Group commissions by inviter to batch balance updates
const inviterBalanceUpdates = {};
for (const commission of commissions) {
const inviterId = commission.inviterId;
const currency = commission.currency || 'RMB';
const amount = parseFloat(commission.commissionAmount);
if (!inviterBalanceUpdates[inviterId]) {
inviterBalanceUpdates[inviterId] = { rmbDeduction: 0, pesoDeduction: 0 };
}
if (currency === 'PHP') {
inviterBalanceUpdates[inviterId].pesoDeduction += amount;
} else {
inviterBalanceUpdates[inviterId].rmbDeduction += amount;
}
// Cancel commission
commission.status = 'cancelled';
await commission.save({ transaction });
commissionsCancelled++;
}
// Update inviter balances
for (const [inviterId, deductions] of Object.entries(inviterBalanceUpdates)) {
const inviter = await User.findByPk(inviterId, { transaction });
if (inviter) {
// Deduct RMB balance (floor at zero)
if (deductions.rmbDeduction > 0) {
inviter.balance = parseFloat(inviter.balance || 0) - deductions.rmbDeduction;
if (inviter.balance < 0) inviter.balance = 0;
}
// Deduct Peso balance (floor at zero)
if (deductions.pesoDeduction > 0) {
inviter.balancePeso = parseFloat(inviter.balancePeso || 0) - deductions.pesoDeduction;
if (inviter.balancePeso < 0) inviter.balancePeso = 0;
}
await inviter.save({ transaction });
}
}
}
await transaction.commit();
@ -403,7 +468,8 @@ const cancelPaymentOrder = async (orderId, cancelReason) => {
orderNo: order.orderNo,
status: order.status,
cancelReason: order.cancelReason,
commissionCancelled: !!commission,
commissionCancelled: commissionsCancelled > 0,
commissionsCancelledCount: commissionsCancelled,
};
} catch (error) {
await transaction.rollback();

File diff suppressed because it is too large Load Diff