311 lines
8.7 KiB
JavaScript
311 lines
8.7 KiB
JavaScript
const User = require('../models/User');
|
|
const Withdrawal = require('../models/Withdrawal');
|
|
const { sequelize } = require('../config/database');
|
|
const { Op } = require('sequelize');
|
|
const logger = require('../config/logger');
|
|
|
|
/**
|
|
* Admin Withdrawal Service
|
|
* Handles withdrawal management operations for admin panel
|
|
*/
|
|
|
|
/**
|
|
* Get paginated withdrawal list with search and filters
|
|
* @param {Object} options - Query options
|
|
* @returns {Promise<Object>} Withdrawal list with pagination
|
|
*/
|
|
const getWithdrawalList = async (options = {}) => {
|
|
const {
|
|
page = 1,
|
|
limit = 20,
|
|
status = '',
|
|
search = '',
|
|
sortBy = 'createdAt',
|
|
sortOrder = 'DESC',
|
|
} = options;
|
|
|
|
const offset = (page - 1) * limit;
|
|
|
|
// Build where clause
|
|
const where = {};
|
|
|
|
// Filter by status
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
|
|
// Search by withdrawal number or user info
|
|
let userIds = [];
|
|
if (search) {
|
|
// Search for users matching the search term
|
|
const users = await User.findAll({
|
|
where: {
|
|
[Op.or]: [
|
|
{ nickname: { [Op.like]: `%${search}%` } },
|
|
{ realName: { [Op.like]: `%${search}%` } },
|
|
{ phone: { [Op.like]: `%${search}%` } },
|
|
],
|
|
},
|
|
attributes: ['id'],
|
|
});
|
|
userIds = users.map(u => u.id);
|
|
|
|
// Also search by withdrawal number
|
|
where[Op.or] = [
|
|
{ withdrawalNo: { [Op.like]: `%${search}%` } },
|
|
...(userIds.length > 0 ? [{ userId: { [Op.in]: userIds } }] : []),
|
|
];
|
|
}
|
|
|
|
// Query withdrawals with user information
|
|
const { count, rows: withdrawals } = await Withdrawal.findAndCountAll({
|
|
where,
|
|
limit: parseInt(limit),
|
|
offset,
|
|
order: [[sortBy, sortOrder]],
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'nickname', 'realName', 'phone', 'whatsapp', 'wechatId'],
|
|
},
|
|
],
|
|
});
|
|
|
|
// Format response
|
|
const formattedWithdrawals = withdrawals.map(withdrawal => ({
|
|
id: withdrawal.id,
|
|
withdrawalNo: withdrawal.withdrawalNo,
|
|
amount: parseFloat(withdrawal.amount).toFixed(2),
|
|
paymentMethod: withdrawal.paymentMethod,
|
|
paymentDetails: withdrawal.paymentDetails,
|
|
status: withdrawal.status,
|
|
reviewedBy: withdrawal.reviewedBy,
|
|
reviewedAt: withdrawal.reviewedAt,
|
|
rejectionReason: withdrawal.rejectionReason,
|
|
completedAt: withdrawal.completedAt,
|
|
createdAt: withdrawal.createdAt,
|
|
updatedAt: withdrawal.updatedAt,
|
|
user: withdrawal.user ? {
|
|
id: withdrawal.user.id,
|
|
nickname: withdrawal.user.nickname,
|
|
realName: withdrawal.user.realName,
|
|
phone: withdrawal.user.phone,
|
|
whatsapp: withdrawal.user.whatsapp,
|
|
wechatId: withdrawal.user.wechatId,
|
|
} : null,
|
|
}));
|
|
|
|
return {
|
|
withdrawals: formattedWithdrawals,
|
|
pagination: {
|
|
total: count,
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
totalPages: Math.ceil(count / limit),
|
|
},
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Approve withdrawal request
|
|
* @param {String} withdrawalId - Withdrawal ID
|
|
* @param {String} adminId - Admin ID performing the action
|
|
* @param {String} notes - Optional notes
|
|
* @returns {Promise<Object>} Updated withdrawal
|
|
*/
|
|
const approveWithdrawal = async (withdrawalId, adminId, notes = '') => {
|
|
const transaction = await sequelize.transaction();
|
|
|
|
try {
|
|
// Get withdrawal with lock
|
|
const withdrawal = await Withdrawal.findByPk(withdrawalId, {
|
|
transaction,
|
|
lock: transaction.LOCK.UPDATE,
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'balance', 'balancePeso'],
|
|
},
|
|
],
|
|
});
|
|
|
|
if (!withdrawal) {
|
|
throw new Error('Withdrawal not found');
|
|
}
|
|
|
|
// Check if withdrawal is in waiting status
|
|
if (withdrawal.status !== 'waiting') {
|
|
throw new Error(`Cannot approve withdrawal with status: ${withdrawal.status}`);
|
|
}
|
|
|
|
// Update withdrawal status (balance was already deducted when user submitted the request)
|
|
await withdrawal.update({
|
|
status: 'completed',
|
|
reviewedBy: adminId,
|
|
reviewedAt: new Date(),
|
|
completedAt: new Date(),
|
|
}, { transaction });
|
|
|
|
await transaction.commit();
|
|
|
|
logger.info(`Withdrawal ${withdrawal.withdrawalNo} approved by admin ${adminId}`);
|
|
|
|
// Get current user balance for response
|
|
const user = withdrawal.user;
|
|
const currency = withdrawal.currency || 'CNY';
|
|
const currentBalance = currency === 'PHP'
|
|
? parseFloat(user?.balancePeso || 0).toFixed(2)
|
|
: parseFloat(user?.balance || 0).toFixed(2);
|
|
|
|
return {
|
|
id: withdrawal.id,
|
|
withdrawalNo: withdrawal.withdrawalNo,
|
|
amount: parseFloat(withdrawal.amount).toFixed(2),
|
|
currency: currency,
|
|
status: withdrawal.status,
|
|
reviewedBy: withdrawal.reviewedBy,
|
|
reviewedAt: withdrawal.reviewedAt,
|
|
completedAt: withdrawal.completedAt,
|
|
userBalance: currentBalance,
|
|
};
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
logger.error('Approve withdrawal error:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Reject withdrawal request
|
|
* @param {String} withdrawalId - Withdrawal ID
|
|
* @param {String} adminId - Admin ID performing the action
|
|
* @param {String} reason - Rejection reason (required)
|
|
* @returns {Promise<Object>} Updated withdrawal
|
|
*/
|
|
const rejectWithdrawal = async (withdrawalId, adminId, reason) => {
|
|
if (!reason || reason.trim() === '') {
|
|
throw new Error('Rejection reason is required');
|
|
}
|
|
|
|
const transaction = await sequelize.transaction();
|
|
|
|
try {
|
|
// Get withdrawal with lock
|
|
const withdrawal = await Withdrawal.findByPk(withdrawalId, {
|
|
transaction,
|
|
lock: transaction.LOCK.UPDATE,
|
|
});
|
|
|
|
if (!withdrawal) {
|
|
throw new Error('Withdrawal not found');
|
|
}
|
|
|
|
// Check if withdrawal is in waiting status
|
|
if (withdrawal.status !== 'waiting') {
|
|
throw new Error(`Cannot reject withdrawal with status: ${withdrawal.status}`);
|
|
}
|
|
|
|
// Get user with lock to refund the balance
|
|
const user = await User.findByPk(withdrawal.userId, {
|
|
transaction,
|
|
lock: transaction.LOCK.UPDATE,
|
|
});
|
|
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
// Refund the withdrawal amount to user balance based on currency
|
|
const withdrawalAmount = parseFloat(withdrawal.amount);
|
|
const currency = withdrawal.currency || 'CNY';
|
|
|
|
if (currency === 'PHP') {
|
|
user.balancePeso = parseFloat(user.balancePeso || 0) + withdrawalAmount;
|
|
} else {
|
|
user.balance = parseFloat(user.balance || 0) + withdrawalAmount;
|
|
}
|
|
await user.save({ transaction });
|
|
|
|
// Update withdrawal status
|
|
await withdrawal.update({
|
|
status: 'rejected',
|
|
reviewedBy: adminId,
|
|
reviewedAt: new Date(),
|
|
rejectionReason: reason,
|
|
}, { transaction });
|
|
|
|
await transaction.commit();
|
|
|
|
logger.info(`Withdrawal ${withdrawal.withdrawalNo} rejected by admin ${adminId}, amount refunded to user`);
|
|
|
|
return {
|
|
id: withdrawal.id,
|
|
withdrawalNo: withdrawal.withdrawalNo,
|
|
amount: parseFloat(withdrawal.amount).toFixed(2),
|
|
currency: currency,
|
|
status: withdrawal.status,
|
|
reviewedBy: withdrawal.reviewedBy,
|
|
reviewedAt: withdrawal.reviewedAt,
|
|
rejectionReason: withdrawal.rejectionReason,
|
|
};
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
logger.error('Reject withdrawal error:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get withdrawal details
|
|
* @param {String} withdrawalId - Withdrawal ID
|
|
* @returns {Promise<Object>} Withdrawal details with user info
|
|
*/
|
|
const getWithdrawalDetails = async (withdrawalId) => {
|
|
const withdrawal = await Withdrawal.findByPk(withdrawalId, {
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'nickname', 'realName', 'phone', 'whatsapp', 'wechatId', 'balance'],
|
|
},
|
|
],
|
|
});
|
|
|
|
if (!withdrawal) {
|
|
throw new Error('Withdrawal not found');
|
|
}
|
|
|
|
return {
|
|
id: withdrawal.id,
|
|
withdrawalNo: withdrawal.withdrawalNo,
|
|
amount: parseFloat(withdrawal.amount).toFixed(2),
|
|
paymentMethod: withdrawal.paymentMethod,
|
|
paymentDetails: withdrawal.paymentDetails,
|
|
status: withdrawal.status,
|
|
reviewedBy: withdrawal.reviewedBy,
|
|
reviewedAt: withdrawal.reviewedAt,
|
|
rejectionReason: withdrawal.rejectionReason,
|
|
completedAt: withdrawal.completedAt,
|
|
createdAt: withdrawal.createdAt,
|
|
updatedAt: withdrawal.updatedAt,
|
|
user: withdrawal.user ? {
|
|
id: withdrawal.user.id,
|
|
nickname: withdrawal.user.nickname,
|
|
realName: withdrawal.user.realName,
|
|
phone: withdrawal.user.phone,
|
|
whatsapp: withdrawal.user.whatsapp,
|
|
wechatId: withdrawal.user.wechatId,
|
|
balance: parseFloat(withdrawal.user.balance).toFixed(2),
|
|
} : null,
|
|
};
|
|
};
|
|
|
|
module.exports = {
|
|
getWithdrawalList,
|
|
approveWithdrawal,
|
|
rejectWithdrawal,
|
|
getWithdrawalDetails,
|
|
};
|