appointment_system/backend/src/services/invitationService.js
2025-12-23 23:47:41 +08:00

210 lines
5.8 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
const records = rows.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,
};