佣金2
This commit is contained in:
parent
784e373c7e
commit
c79e164f87
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
175
backend/src/scripts/fixDualCurrencyCommissions.js
Normal file
175
backend/src/scripts/fixDualCurrencyCommissions.js
Normal 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();
|
||||
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
1637
backend/src/tests/dualCurrencyCommission.property.test.js
Normal file
1637
backend/src/tests/dualCurrencyCommission.property.test.js
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user