699 lines
20 KiB
JavaScript
699 lines
20 KiB
JavaScript
const { ChitGroup, GroupMember, User, Payment, MonthlyDraw } = require('../models');
|
||
const { Op } = require('sequelize');
|
||
|
||
// Create a new chit group
|
||
const createChitGroup = async (req, res) => {
|
||
try {
|
||
const {
|
||
name,
|
||
total_value,
|
||
monthly_installment,
|
||
duration_months,
|
||
max_members,
|
||
foreman_commission_amount,
|
||
foreman_commission_type = 'fixed',
|
||
foreman_commission_rate,
|
||
draw_date,
|
||
start_date, // Optional: for importing existing groups
|
||
current_month, // Optional: current month number for existing groups
|
||
is_import, // Flag to indicate this is an existing group
|
||
status // Optional: can be 'active' for imported groups
|
||
} = req.body;
|
||
|
||
const managerId = req.user.id;
|
||
|
||
// Validate input
|
||
if (!name || !total_value || !monthly_installment || !duration_months || !max_members || !foreman_commission_amount || !draw_date) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'All fields are required'
|
||
});
|
||
}
|
||
|
||
// Validate business rules based on commission type
|
||
let expectedTotalValue;
|
||
let actualCommission;
|
||
|
||
if (foreman_commission_type === 'percentage') {
|
||
// For percentage commission: commission = (total_value / duration_months) * (rate / 100)
|
||
// So: monthly_installment = (total_value / duration_months) + (total_value / duration_months) * (rate / 100)
|
||
// monthly_installment = (total_value / duration_months) * (1 + rate/100)
|
||
// total_value = monthly_installment * duration_months / (1 + rate/100)
|
||
const rate = foreman_commission_rate || 5;
|
||
expectedTotalValue = (monthly_installment * duration_months) / (1 + rate/100);
|
||
actualCommission = (expectedTotalValue / duration_months) * (rate / 100);
|
||
} else {
|
||
// For fixed commission: total_value = (monthly_installment - foreman_commission_amount) × duration_months
|
||
expectedTotalValue = (monthly_installment - foreman_commission_amount) * duration_months;
|
||
actualCommission = foreman_commission_amount;
|
||
}
|
||
|
||
if (Math.abs(total_value - expectedTotalValue) > 1) { // Allow small rounding differences
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: `Total value calculation mismatch. Expected: ₹${expectedTotalValue.toFixed(0)}, Provided: ₹${total_value}. Commission type: ${foreman_commission_type}`
|
||
});
|
||
}
|
||
|
||
// Validate commission based on type
|
||
if (foreman_commission_type === 'percentage') {
|
||
const rate = foreman_commission_rate || 5;
|
||
if (rate < 0 || rate > 100) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Commission rate must be between 0 and 100%'
|
||
});
|
||
}
|
||
} else {
|
||
if (foreman_commission_amount < 0 || foreman_commission_amount > total_value * 0.1) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Foreman commission must be between 0 and 10% of total value'
|
||
});
|
||
}
|
||
}
|
||
|
||
if (draw_date < 1 || draw_date > 31) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Draw date must be between 1 and 31'
|
||
});
|
||
}
|
||
|
||
// Determine start date and status
|
||
const groupStartDate = is_import && start_date ? new Date(start_date) : new Date();
|
||
const groupStatus = is_import && status === 'active' ? 'active' : 'forming';
|
||
|
||
// Calculate end date from start date
|
||
const endDate = new Date(groupStartDate);
|
||
endDate.setMonth(endDate.getMonth() + duration_months);
|
||
|
||
// Create chit group
|
||
const chitGroup = await ChitGroup.create({
|
||
name,
|
||
total_value,
|
||
monthly_installment,
|
||
duration_months,
|
||
max_members,
|
||
foreman_commission_amount: actualCommission, // Use calculated commission
|
||
foreman_commission_type,
|
||
foreman_commission_rate,
|
||
draw_date,
|
||
manager_id: managerId,
|
||
status: groupStatus,
|
||
start_date: groupStartDate,
|
||
end_date: endDate
|
||
});
|
||
|
||
const message = is_import
|
||
? `Existing group imported successfully. Started: ${groupStartDate.toLocaleDateString()}. Current month: ${current_month || 1}`
|
||
: 'Chit group created successfully';
|
||
|
||
res.status(201).json({
|
||
success: true,
|
||
message,
|
||
data: {
|
||
...chitGroup.toJSON(),
|
||
current_month: current_month || 1,
|
||
is_imported: is_import || false
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Create chit group error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Internal server error',
|
||
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||
});
|
||
}
|
||
};
|
||
|
||
// Get all chit groups for the manager
|
||
const getManagerChitGroups = async (req, res) => {
|
||
try {
|
||
const managerId = req.user.id;
|
||
const { status, page = 1, limit = 10 } = req.query;
|
||
|
||
const whereClause = { manager_id: managerId };
|
||
if (status) {
|
||
whereClause.status = status;
|
||
}
|
||
|
||
const offset = (page - 1) * limit;
|
||
|
||
const chitGroups = await ChitGroup.findAndCountAll({
|
||
where: whereClause,
|
||
include: [
|
||
{
|
||
model: GroupMember,
|
||
as: 'members',
|
||
include: [
|
||
{
|
||
model: User,
|
||
attributes: ['id', 'full_name', 'mobile_number']
|
||
}
|
||
]
|
||
}
|
||
],
|
||
limit: parseInt(limit),
|
||
offset: parseInt(offset),
|
||
order: [['created_at', 'DESC']]
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
data: {
|
||
chitGroups: chitGroups.rows,
|
||
pagination: {
|
||
currentPage: parseInt(page),
|
||
totalPages: Math.ceil(chitGroups.count / limit),
|
||
totalItems: chitGroups.count,
|
||
itemsPerPage: parseInt(limit)
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Get manager chit groups error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Internal server error'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Get chit groups for a member
|
||
const getMemberChitGroups = async (req, res) => {
|
||
try {
|
||
const memberId = req.user.id;
|
||
const { status, page = 1, limit = 10 } = req.query;
|
||
|
||
const whereClause = { user_id: memberId };
|
||
if (status) {
|
||
whereClause.status = status;
|
||
}
|
||
|
||
const offset = (page - 1) * limit;
|
||
|
||
const groupMemberships = await GroupMember.findAndCountAll({
|
||
where: whereClause,
|
||
include: [
|
||
{
|
||
model: ChitGroup,
|
||
include: [
|
||
{
|
||
model: User,
|
||
as: 'manager',
|
||
attributes: ['id', 'full_name', 'mobile_number']
|
||
}
|
||
]
|
||
}
|
||
],
|
||
limit: parseInt(limit),
|
||
offset: parseInt(offset),
|
||
order: [['created_at', 'DESC']]
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
data: {
|
||
chitGroups: groupMemberships.rows.map(membership => ({
|
||
...membership.ChitGroup.toJSON(),
|
||
membership_status: membership.status,
|
||
total_paid: membership.total_paid,
|
||
total_won: membership.total_won
|
||
})),
|
||
pagination: {
|
||
currentPage: parseInt(page),
|
||
totalPages: Math.ceil(groupMemberships.count / limit),
|
||
totalItems: groupMemberships.count,
|
||
itemsPerPage: parseInt(limit)
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Get member chit groups error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Internal server error'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Get single chit group details
|
||
const getChitGroupDetails = async (req, res) => {
|
||
try {
|
||
const { groupId } = req.params;
|
||
const userId = req.user.id;
|
||
|
||
const chitGroup = await ChitGroup.findByPk(groupId, {
|
||
include: [
|
||
{
|
||
model: User,
|
||
as: 'manager',
|
||
attributes: ['id', 'full_name', 'mobile_number']
|
||
},
|
||
{
|
||
model: GroupMember,
|
||
as: 'members',
|
||
include: [
|
||
{
|
||
model: User,
|
||
attributes: ['id', 'full_name', 'mobile_number']
|
||
}
|
||
]
|
||
},
|
||
{
|
||
model: MonthlyDraw,
|
||
order: [['created_at', 'DESC']],
|
||
limit: 5
|
||
}
|
||
]
|
||
});
|
||
|
||
if (!chitGroup) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Chit group not found'
|
||
});
|
||
}
|
||
|
||
// Check if user has access to this group
|
||
const isManager = chitGroup.manager_id === userId;
|
||
const isMember = chitGroup.members.some(member => member.user_id === userId);
|
||
|
||
if (!isManager && !isMember) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: 'Access denied'
|
||
});
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
data: chitGroup
|
||
});
|
||
} catch (error) {
|
||
console.error('Get chit group details error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Internal server error'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Update chit group
|
||
const updateChitGroup = async (req, res) => {
|
||
try {
|
||
const { groupId } = req.params;
|
||
const managerId = req.user.id;
|
||
const updateData = req.body;
|
||
|
||
const chitGroup = await ChitGroup.findOne({
|
||
where: { id: groupId, manager_id: managerId }
|
||
});
|
||
|
||
if (!chitGroup) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Chit group not found or access denied'
|
||
});
|
||
}
|
||
|
||
// Only allow updates if group is still forming
|
||
if (chitGroup.status !== 'forming') {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Cannot update chit group after it has started'
|
||
});
|
||
}
|
||
|
||
// Validate business rules if updating financial fields
|
||
if (updateData.monthly_installment && updateData.duration_months) {
|
||
const commissionType = updateData.foreman_commission_type || chitGroup.foreman_commission_type || 'fixed';
|
||
let expectedTotalValue;
|
||
let actualCommission;
|
||
|
||
if (commissionType === 'percentage') {
|
||
const rate = updateData.foreman_commission_rate || chitGroup.foreman_commission_rate || 5;
|
||
expectedTotalValue = (updateData.monthly_installment * updateData.duration_months) / (1 + rate/100);
|
||
actualCommission = (expectedTotalValue / updateData.duration_months) * (rate / 100);
|
||
} else {
|
||
const commission = updateData.foreman_commission_amount || chitGroup.foreman_commission_amount;
|
||
expectedTotalValue = (updateData.monthly_installment - commission) * updateData.duration_months;
|
||
actualCommission = commission;
|
||
}
|
||
|
||
if (updateData.total_value && Math.abs(updateData.total_value - expectedTotalValue) > 1) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: `Total value calculation mismatch. Expected: ₹${expectedTotalValue.toFixed(0)}, Provided: ₹${updateData.total_value}. Commission type: ${commissionType}`
|
||
});
|
||
}
|
||
updateData.total_value = expectedTotalValue;
|
||
updateData.foreman_commission_amount = actualCommission;
|
||
}
|
||
|
||
await chitGroup.update(updateData);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Chit group updated successfully',
|
||
data: chitGroup
|
||
});
|
||
} catch (error) {
|
||
console.error('Update chit group error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Internal server error'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Delete chit group (only if forming and no active members)
|
||
const deleteChitGroup = async (req, res) => {
|
||
try {
|
||
const { groupId } = req.params;
|
||
const managerId = req.user.id;
|
||
|
||
const chitGroup = await ChitGroup.findOne({
|
||
where: { id: groupId, manager_id: managerId }
|
||
});
|
||
|
||
if (!chitGroup) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Chit group not found or access denied'
|
||
});
|
||
}
|
||
|
||
if (chitGroup.status !== 'forming') {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Cannot delete chit group after it has started'
|
||
});
|
||
}
|
||
|
||
// Count only ACTIVE members (not removed or inactive)
|
||
const activeMemberCount = await GroupMember.count({
|
||
where: {
|
||
group_id: groupId,
|
||
status: 'active'
|
||
}
|
||
});
|
||
|
||
if (activeMemberCount > 0) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: `Cannot delete chit group with ${activeMemberCount} active member(s). Please remove all active members first.`
|
||
});
|
||
}
|
||
|
||
await chitGroup.destroy();
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Chit group deleted successfully'
|
||
});
|
||
} catch (error) {
|
||
console.error('Delete chit group error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Internal server error'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Start chit group (activate it)
|
||
const startChitGroup = async (req, res) => {
|
||
try {
|
||
const { groupId } = req.params;
|
||
const managerId = req.user.id;
|
||
|
||
const chitGroup = await ChitGroup.findOne({
|
||
where: { id: groupId, manager_id: managerId },
|
||
include: [
|
||
{
|
||
model: GroupMember,
|
||
as: 'members'
|
||
}
|
||
]
|
||
});
|
||
|
||
if (!chitGroup) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Chit group not found or access denied'
|
||
});
|
||
}
|
||
|
||
if (chitGroup.status !== 'forming') {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Chit group is already active or completed'
|
||
});
|
||
}
|
||
|
||
if (chitGroup.members.length < 2) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: 'Need at least 2 members to start the chit group'
|
||
});
|
||
}
|
||
|
||
await chitGroup.update({
|
||
status: 'active',
|
||
start_date: new Date()
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Chit group activated successfully',
|
||
data: chitGroup
|
||
});
|
||
} catch (error) {
|
||
console.error('Start chit group error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Internal server error'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Get chit group statistics
|
||
const getChitGroupStats = async (req, res) => {
|
||
try {
|
||
const { groupId } = req.params;
|
||
const userId = req.user.id;
|
||
|
||
console.log('Getting stats for group:', groupId, 'user:', userId);
|
||
|
||
// First, try to find the chit group without includes
|
||
const chitGroup = await ChitGroup.findByPk(groupId);
|
||
|
||
if (!chitGroup) {
|
||
console.log('Chit group not found:', groupId);
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Chit group not found'
|
||
});
|
||
}
|
||
|
||
console.log('Chit group found:', chitGroup.name, 'Manager:', chitGroup.manager_id);
|
||
|
||
// Check access - user must be the manager
|
||
const isManager = chitGroup.manager_id === userId;
|
||
|
||
if (!isManager) {
|
||
console.log('Access denied - user is not manager');
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: 'Access denied - only group manager can view stats'
|
||
});
|
||
}
|
||
|
||
// Get related data separately to avoid complex query issues
|
||
const members = await GroupMember.findAll({
|
||
where: { group_id: groupId },
|
||
include: [{ model: User, attributes: ['id', 'full_name', 'mobile_number'] }]
|
||
});
|
||
|
||
const payments = await Payment.findAll({
|
||
where: { group_id: groupId, status: 'success' }
|
||
});
|
||
|
||
const draws = await MonthlyDraw.findAll({
|
||
where: { group_id: groupId, status: 'completed' }
|
||
});
|
||
|
||
// Calculate statistics
|
||
const totalMembers = members.length;
|
||
const totalPayments = payments.length;
|
||
const totalDraws = draws.length;
|
||
const totalCollection = totalPayments * parseFloat(chitGroup.monthly_installment);
|
||
const foremanCommission = parseFloat(chitGroup.foreman_commission_amount);
|
||
const totalPrizePool = totalCollection - foremanCommission;
|
||
|
||
const stats = {
|
||
totalMembers,
|
||
totalPayments,
|
||
totalDraws,
|
||
totalCollection,
|
||
foremanCommission,
|
||
totalPrizePool,
|
||
averagePrizePerDraw: totalDraws > 0 ? totalPrizePool / totalDraws : 0,
|
||
completionPercentage: (totalDraws / chitGroup.duration_months) * 100
|
||
};
|
||
|
||
console.log('Stats calculated:', stats);
|
||
|
||
res.json({
|
||
success: true,
|
||
data: stats
|
||
});
|
||
} catch (error) {
|
||
console.error('Get chit group stats error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Internal server error'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Get chit group financial data
|
||
const getChitGroupFinancialData = async (req, res) => {
|
||
try {
|
||
const { groupId } = req.params;
|
||
const userId = req.user.id;
|
||
|
||
console.log('Getting financial data for group:', groupId, 'user:', userId);
|
||
|
||
const chitGroup = await ChitGroup.findByPk(groupId);
|
||
|
||
if (!chitGroup) {
|
||
console.log('Chit group not found:', groupId);
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: 'Chit group not found'
|
||
});
|
||
}
|
||
|
||
// Check access
|
||
const isManager = chitGroup.manager_id === userId;
|
||
const isMember = await GroupMember.findOne({
|
||
where: { group_id: groupId, user_id: userId }
|
||
});
|
||
|
||
if (!isManager && !isMember) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: 'Access denied'
|
||
});
|
||
}
|
||
|
||
// Generate financial data based on group parameters
|
||
const financialData = generateFinancialData(chitGroup);
|
||
|
||
res.json({
|
||
success: true,
|
||
data: {
|
||
financial_data: financialData
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Get chit group financial data error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Internal server error'
|
||
});
|
||
}
|
||
};
|
||
|
||
// Helper function to generate financial data
|
||
const generateFinancialData = (chitGroup) => {
|
||
const financialData = [];
|
||
const totalMonths = chitGroup.duration_months;
|
||
const chitValue = chitGroup.total_value;
|
||
const monthlyInstallment = chitGroup.monthly_installment;
|
||
const commission = chitGroup.foreman_commission_amount;
|
||
|
||
// Use actual start_date if valid; invalid/legacy DB values → today (avoids NaN → "undefined-N" labels).
|
||
let startDate = chitGroup.start_date ? new Date(chitGroup.start_date) : new Date();
|
||
if (Number.isNaN(startDate.getTime())) {
|
||
startDate = new Date();
|
||
}
|
||
const startMonth = startDate.getMonth(); // 0-11 (0 = January)
|
||
const startYear = startDate.getFullYear();
|
||
|
||
// Calculate starting bid amount (typically 85-90% of chit value)
|
||
let currentBidAmount = chitGroup.total_value * 0.8765; // Starting at 87.65%
|
||
|
||
for (let i = 0; i < totalMonths; i++) {
|
||
// Calculate the actual calendar month and year for this cycle month
|
||
const totalMonthsFromStart = startMonth + i;
|
||
const currentMonth = totalMonthsFromStart % 12; // 0-11
|
||
const currentYear = startYear + Math.floor(totalMonthsFromStart / 12);
|
||
|
||
const monthName = getMonthName(currentMonth);
|
||
const monthYear = `${monthName}-${currentYear.toString().substring(2)}`;
|
||
|
||
// Calculate dividend (difference between chit value and bid amount)
|
||
const dividend = chitValue - currentBidAmount;
|
||
|
||
financialData.push({
|
||
month_year: monthYear,
|
||
chit_value: chitValue,
|
||
bid_amount: Math.round(currentBidAmount),
|
||
subscription_amount: monthlyInstallment,
|
||
commission_installment: commission,
|
||
total_payable_installment: parseFloat(monthlyInstallment) + parseFloat(commission),
|
||
dividend_amount: Math.round(dividend)
|
||
});
|
||
|
||
// Increase bid amount for next month
|
||
currentBidAmount += 2600; // Increment by ₹2600 per month
|
||
}
|
||
|
||
// Add total row
|
||
const totalChitValue = chitValue * totalMonths;
|
||
const totalBidAmount = financialData.reduce((sum, entry) => sum + entry.bid_amount, 0);
|
||
const totalSubscription = monthlyInstallment * totalMonths;
|
||
const totalCommission = commission * totalMonths;
|
||
const totalPayable = (monthlyInstallment + commission) * totalMonths;
|
||
const totalDividend = financialData.reduce((sum, entry) => sum + entry.dividend_amount, 0);
|
||
|
||
financialData.push({
|
||
month_year: 'Total',
|
||
chit_value: totalChitValue,
|
||
bid_amount: totalBidAmount,
|
||
subscription_amount: totalSubscription,
|
||
commission_installment: totalCommission,
|
||
total_payable_installment: totalPayable,
|
||
dividend_amount: totalDividend
|
||
});
|
||
|
||
return financialData;
|
||
};
|
||
|
||
const getMonthName = (monthIndex) => {
|
||
const months = [
|
||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||
];
|
||
if (typeof monthIndex !== 'number' || Number.isNaN(monthIndex) || monthIndex < 0 || monthIndex > 11) {
|
||
return '?';
|
||
}
|
||
return months[monthIndex];
|
||
};
|
||
|
||
module.exports = {
|
||
createChitGroup,
|
||
getManagerChitGroups,
|
||
getMemberChitGroups,
|
||
getChitGroupDetails,
|
||
updateChitGroup,
|
||
deleteChitGroup,
|
||
startChitGroup,
|
||
getChitGroupStats,
|
||
getChitGroupFinancialData
|
||
};
|