212 lines
5.9 KiB
JavaScript
212 lines
5.9 KiB
JavaScript
const User = require('../models/User');
|
|
const Invitation = require('../models/Invitation');
|
|
const Appointment = require('../models/Appointment');
|
|
const { sequelize } = require('../config/database');
|
|
const { generateInvitationCode } = require('./authService');
|
|
|
|
/**
|
|
* Invitation Service
|
|
* Handles invitation code generation, tracking, and rewards
|
|
*/
|
|
|
|
/**
|
|
* Generate invitation code for user
|
|
* @param {string} userId - User ID
|
|
* @returns {Object} User with invitation code
|
|
*/
|
|
const generateInvitationCodeForUser = async (userId) => {
|
|
const user = await User.findByPk(userId);
|
|
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
// Check if user already has an invitation code
|
|
if (user.invitationCode) {
|
|
return user;
|
|
}
|
|
|
|
// Generate unique invitation code
|
|
let code;
|
|
let exists = true;
|
|
|
|
while (exists) {
|
|
code = generateInvitationCode();
|
|
const existingUser = await User.findOne({ where: { invitationCode: code } });
|
|
exists = !!existingUser;
|
|
}
|
|
|
|
// Update user with invitation code
|
|
user.invitationCode = code;
|
|
await user.save();
|
|
|
|
return user;
|
|
};
|
|
|
|
/**
|
|
* Get invitation statistics for user
|
|
* @param {string} userId - User ID
|
|
* @returns {Object} Invitation statistics
|
|
*/
|
|
const getInvitationStats = async (userId) => {
|
|
const user = await User.findByPk(userId);
|
|
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
// Get all invitations where user is the inviter
|
|
const invitations = await Invitation.findAll({
|
|
where: { inviterId: userId },
|
|
});
|
|
|
|
// Calculate statistics
|
|
const totalInvites = invitations.length;
|
|
const paidInvites = invitations.filter(inv => inv.firstPaymentAt != null).length;
|
|
const totalRewards = invitations
|
|
.filter(inv => inv.rewardStatus === 'credited')
|
|
.reduce((sum, inv) => sum + parseFloat(inv.rewardAmount || 0), 0);
|
|
|
|
return {
|
|
invitationCode: user.invitationCode,
|
|
totalInvites,
|
|
paidInvites,
|
|
totalRewards: totalRewards.toFixed(2),
|
|
availableBalance: parseFloat(user.balance).toFixed(2),
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Get invitation records for user
|
|
* @param {string} userId - User ID
|
|
* @param {Object} options - Query options (page, limit)
|
|
* @returns {Object} Invitation records with pagination
|
|
*/
|
|
const getInvitationRecords = async (userId, options = {}) => {
|
|
const { page = 1, limit = 20 } = options;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const user = await User.findByPk(userId);
|
|
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
// Get invitations with invitee information
|
|
const { count, rows } = await Invitation.findAndCountAll({
|
|
where: { inviterId: userId },
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'invitee',
|
|
attributes: ['id', 'uid', 'nickname', 'avatar', 'createdAt'],
|
|
},
|
|
],
|
|
order: [['createdAt', 'DESC']],
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset),
|
|
});
|
|
|
|
// Format records - filter out records where invitee was deleted
|
|
const records = rows
|
|
.filter(invitation => invitation.invitee != null)
|
|
.map(invitation => ({
|
|
id: invitation.id,
|
|
invitee: {
|
|
id: invitation.invitee.id,
|
|
uid: invitation.invitee.uid,
|
|
nickname: invitation.invitee.nickname,
|
|
avatar: invitation.invitee.avatar,
|
|
registeredAt: invitation.registeredAt,
|
|
},
|
|
firstPaymentAt: invitation.firstPaymentAt,
|
|
firstPaymentAmount: invitation.firstPaymentAmount ? parseFloat(invitation.firstPaymentAmount).toFixed(2) : null,
|
|
rewardAmount: invitation.rewardAmount ? parseFloat(invitation.rewardAmount).toFixed(2) : null,
|
|
rewardStatus: invitation.rewardStatus,
|
|
createdAt: invitation.createdAt,
|
|
}));
|
|
|
|
return {
|
|
records,
|
|
pagination: {
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
total: count,
|
|
totalPages: Math.ceil(count / limit),
|
|
},
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Record invitation relationship when user registers
|
|
* @param {string} inviteeId - New user ID
|
|
* @param {string} invitationCode - Invitation code used
|
|
* @returns {Object} Invitation record
|
|
*/
|
|
const recordInvitationRelationship = async (inviteeId, invitationCode) => {
|
|
// Find inviter by invitation code
|
|
const inviter = await User.findOne({ where: { invitationCode } });
|
|
|
|
if (!inviter) {
|
|
throw new Error('Invalid invitation code');
|
|
}
|
|
|
|
// Check if invitation relationship already exists
|
|
const existingInvitation = await Invitation.findOne({
|
|
where: { inviteeId },
|
|
});
|
|
|
|
if (existingInvitation) {
|
|
return existingInvitation;
|
|
}
|
|
|
|
// Create invitation record
|
|
const invitation = await Invitation.create({
|
|
inviterId: inviter.id,
|
|
inviteeId,
|
|
invitationCode,
|
|
registeredAt: new Date(),
|
|
rewardStatus: 'pending',
|
|
});
|
|
|
|
return invitation;
|
|
};
|
|
|
|
/**
|
|
* Process first payment and calculate reward
|
|
* @deprecated This function is deprecated. Commission calculation is now handled by
|
|
* commissionService.calculateCommission() when payment orders are created in admin panel.
|
|
* The new system calculates commission for EVERY payment (not just first), and uses
|
|
* configurable commission rate (default 2%) instead of fixed 10%.
|
|
*
|
|
* @param {string} userId - User ID who made payment
|
|
* @param {string} appointmentId - Appointment ID
|
|
* @returns {Object} Updated invitation record or null
|
|
*/
|
|
const processFirstPayment = async (userId, appointmentId) => {
|
|
// DEPRECATED: Commission calculation is now handled by commissionService
|
|
// when admin creates payment orders. This function is kept for backward
|
|
// compatibility but no longer calculates or credits commissions.
|
|
|
|
// Check if user was invited
|
|
const invitation = await Invitation.findOne({
|
|
where: { inviteeId: userId },
|
|
});
|
|
|
|
if (!invitation) {
|
|
return null; // User was not invited
|
|
}
|
|
|
|
// Just return the invitation record without processing commission
|
|
// Commission is now calculated via PaymentOrder creation in admin panel
|
|
return invitation;
|
|
};
|
|
|
|
module.exports = {
|
|
generateInvitationCodeForUser,
|
|
getInvitationStats,
|
|
getInvitationRecords,
|
|
recordInvitationRelationship,
|
|
processFirstPayment,
|
|
};
|