chitfund/backend/src/controllers/chitGroupController.js

699 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
};