const { MonthlyDraw, ChitGroup, GroupMember, User, Payment } = require('../models'); const { Op } = require('sequelize'); const crypto = require('crypto'); // Generate provably fair lottery result const generateProvablyFairResult = (serverSeed, clientSeed, nonce, eligibleMembers) => { // Create combined seed const combinedSeed = `${serverSeed}-${clientSeed}-${nonce}`; const hash = crypto.createHash('sha256').update(combinedSeed).digest('hex'); // Use first 8 characters of hash as random number const randomHex = hash.substring(0, 8); const randomNumber = parseInt(randomHex, 16); // Select winner from eligible members const winnerIndex = randomNumber % eligibleMembers.length; const winner = eligibleMembers[winnerIndex]; return { winner, hash, randomNumber, serverSeed, clientSeed, nonce }; }; // Create a new monthly draw const createMonthlyDraw = async (req, res) => { try { const { group_id, month, year, client_seed, winner_id, prize_amount, is_past_draw } = req.body; // Convert month and year to integers const monthInt = parseInt(month, 10); const yearInt = parseInt(year, 10); const managerId = req.user.id; // Validate input if (!group_id || !month || !year) { return res.status(400).json({ success: false, message: 'Group ID, month, and year are required' }); } // Validate month and year are valid numbers if (isNaN(monthInt) || isNaN(yearInt)) { return res.status(400).json({ success: false, message: 'Month and year must be valid numbers' }); } // Validate month range if (monthInt < 1 || monthInt > 12) { return res.status(400).json({ success: false, message: 'Month must be between 1 and 12' }); } // Check if user is the manager of this group const chitGroup = await ChitGroup.findOne({ where: { id: group_id, manager_id: managerId } }); if (!chitGroup) { return res.status(404).json({ success: false, message: 'Chit group not found or you are not the manager' }); } // Check if group is active if (chitGroup.status !== 'active') { return res.status(400).json({ success: false, message: 'Can only conduct draws for active chit groups' }); } // Check if draw already exists for this month/year const existingDraw = await MonthlyDraw.findOne({ where: { group_id, month: monthInt, year: yearInt } }); if (existingDraw) { return res.status(400).json({ success: false, message: 'Draw already exists for this month and year' }); } // Get eligible members (active members who haven't won yet) const eligibleMembers = await GroupMember.findAll({ where: { group_id, status: 'active' }, include: [ { model: User, attributes: ['id', 'full_name', 'mobile_number'] } ] }); // Filter out members who have already won const wonMembers = await MonthlyDraw.findAll({ where: { group_id }, attributes: ['winner_id'] }); const wonMemberIds = wonMembers.map(draw => draw.winner_id); const availableMembers = eligibleMembers.filter(member => !wonMemberIds.includes(member.user_id) ); if (availableMembers.length === 0 && !is_past_draw) { return res.status(400).json({ success: false, message: 'No eligible members available for draw' }); } // Determine winner let selectedWinnerId; let selectedWinner; let serverSeed; let resultHash; let nonce; if (is_past_draw && winner_id) { // Past draw - manual winner selection selectedWinnerId = winner_id; serverSeed = `PAST_DRAW_${Date.now()}`; nonce = Date.now(); resultHash = crypto.createHash('sha256').update(`${serverSeed}_${winner_id}_${nonce}`).digest('hex'); // Get winner details selectedWinner = eligibleMembers.find(m => m.user_id === winner_id); if (!selectedWinner) { return res.status(400).json({ success: false, message: 'Selected winner is not a member of this group' }); } // Check if this member has already won if (wonMemberIds.includes(winner_id)) { return res.status(400).json({ success: false, message: `${selectedWinner.User.full_name} has already won in a previous draw. Each member can only win once.`, alreadyWon: true, winnerName: selectedWinner.User.full_name }); } } else { // Regular draw - provably fair random selection serverSeed = crypto.randomBytes(32).toString('hex'); nonce = Date.now(); const result = generateProvablyFairResult( serverSeed, client_seed || 'default', nonce, availableMembers ); selectedWinnerId = result.winner.user_id; selectedWinner = result.winner; resultHash = result.hash; } // Calculate prize amount const totalMembers = eligibleMembers.length; const calculatedPrizeAmount = prize_amount || chitGroup.total_value; // Use provided or default // For past draws, use all eligible members; for regular draws, use only available members const membersForDraw = is_past_draw ? eligibleMembers : availableMembers; // Create monthly draw const monthlyDraw = await MonthlyDraw.create({ group_id, month: monthInt, year: yearInt, draw_date: new Date(), eligible_members: membersForDraw.map(member => ({ id: member.user_id, name: member.User.full_name, mobile: member.User.mobile_number })), winner_id: selectedWinnerId, prize_amount: calculatedPrizeAmount, server_seed: serverSeed, server_seed_hash: crypto.createHash('sha256').update(serverSeed).digest('hex'), client_seed: client_seed || (is_past_draw ? 'PAST_DRAW' : 'default'), nonce, result_hash: resultHash, status: 'completed', notes: is_past_draw ? `Past draw result (imported) - Winner: ${selectedWinner.User.full_name}` : `Winner selected: ${selectedWinner.User.full_name}` }); res.status(201).json({ success: true, message: 'Monthly draw completed successfully', data: { ...monthlyDraw.toJSON(), winner: selectedWinner.User, eligible_count: membersForDraw.length } }); } catch (error) { console.error('Create monthly draw error:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }; // Get all monthly draws for a group const getGroupMonthlyDraws = async (req, res) => { try { const { group_id } = req.params; const { status, page = 1, limit = 20 } = req.query; // Check if user has access to this group const chitGroup = await ChitGroup.findOne({ where: { id: group_id, [Op.or]: [ { manager_id: req.user.id }, { '$members.user_id$': req.user.id } ] }, include: [ { model: GroupMember, as: 'members', include: [ { model: User, attributes: ['id', 'full_name', 'mobile_number'] } ] } ] }); if (!chitGroup) { return res.status(404).json({ success: false, message: 'Chit group not found or access denied' }); } const whereClause = { group_id }; if (status) { whereClause.status = status; } const offset = (page - 1) * limit; const monthlyDraws = await MonthlyDraw.findAndCountAll({ where: whereClause, include: [ { model: User, as: 'winner', attributes: ['id', 'full_name', 'mobile_number'] } ], order: [['created_at', 'DESC']], limit: parseInt(limit), offset: parseInt(offset) }); res.json({ success: true, message: 'Monthly draws retrieved successfully', data: { monthlyDraws: monthlyDraws.rows, total: monthlyDraws.count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(monthlyDraws.count / limit) } }); } catch (error) { console.error('Get group monthly draws error:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }; // Get monthly draw details const getMonthlyDrawDetails = async (req, res) => { try { const { draw_id } = req.params; const monthlyDraw = await MonthlyDraw.findOne({ where: { id: draw_id }, include: [ { model: User, as: 'winner', attributes: ['id', 'full_name', 'mobile_number'] }, { model: ChitGroup, attributes: ['id', 'name', 'monthly_installment', 'foreman_commission_amount'] } ] }); if (!monthlyDraw) { return res.status(404).json({ success: false, message: 'Monthly draw not found' }); } // Check if user has access to this draw const chitGroup = await ChitGroup.findOne({ where: { id: monthlyDraw.group_id, [Op.or]: [ { manager_id: req.user.id }, { '$members.user_id$': req.user.id } ] } }); if (!chitGroup) { return res.status(403).json({ success: false, message: 'Access denied to this monthly draw' }); } res.json({ success: true, message: 'Monthly draw details retrieved successfully', data: monthlyDraw }); } catch (error) { console.error('Get monthly draw details error:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }; // Verify provably fair result const verifyDrawResult = async (req, res) => { try { const { draw_id } = req.params; const { client_seed } = req.body; const monthlyDraw = await MonthlyDraw.findOne({ where: { id: draw_id }, include: [ { model: User, as: 'winner', attributes: ['id', 'full_name', 'mobile_number'] } ] }); if (!monthlyDraw) { return res.status(404).json({ success: false, message: 'Monthly draw not found' }); } // Check if user has access to this draw const chitGroup = await ChitGroup.findOne({ where: { id: monthlyDraw.group_id, [Op.or]: [ { manager_id: req.user.id }, { '$members.user_id$': req.user.id } ] } }); if (!chitGroup) { return res.status(403).json({ success: false, message: 'Access denied to this monthly draw' }); } // Verify the result const verificationResult = generateProvablyFairResult( monthlyDraw.server_seed, client_seed || monthlyDraw.client_seed, monthlyDraw.nonce, monthlyDraw.eligible_members ); const isVerified = verificationResult.hash === monthlyDraw.result_hash; res.json({ success: true, message: 'Draw verification completed', data: { isVerified, originalHash: monthlyDraw.result_hash, verificationHash: verificationResult.hash, serverSeed: monthlyDraw.server_seed, clientSeed: client_seed || monthlyDraw.client_seed, nonce: monthlyDraw.nonce, winner: monthlyDraw.winner } }); } catch (error) { console.error('Verify draw result error:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }; // Get draw statistics for a group const getDrawStatistics = async (req, res) => { try { const { group_id } = req.params; // Check if user has access to this group const chitGroup = await ChitGroup.findOne({ where: { id: group_id, [Op.or]: [ { manager_id: req.user.id }, { '$members.user_id$': req.user.id } ] } }); if (!chitGroup) { return res.status(404).json({ success: false, message: 'Chit group not found or access denied' }); } // Get all draws for this group const draws = await MonthlyDraw.findAll({ where: { group_id }, include: [ { model: User, as: 'winner', attributes: ['id', 'full_name', 'mobile_number'] } ], order: [['created_at', 'ASC']] }); // Get total members const totalMembers = await GroupMember.count({ where: { group_id, status: 'active' } }); // Calculate statistics const completedDraws = draws.filter(draw => draw.status === 'completed'); const totalPrizeAmount = completedDraws.reduce((sum, draw) => sum + parseFloat(draw.prize_amount), 0); const averagePrizeAmount = completedDraws.length > 0 ? totalPrizeAmount / completedDraws.length : 0; // Get winners by month const winnersByMonth = completedDraws.map(draw => ({ month: draw.month, year: draw.year, winner: draw.winner, prize_amount: draw.prize_amount, draw_date: draw.draw_date })); res.json({ success: true, message: 'Draw statistics retrieved successfully', data: { totalDraws: draws.length, completedDraws: completedDraws.length, pendingDraws: draws.filter(draw => draw.status === 'pending').length, totalMembers, totalPrizeAmount, averagePrizeAmount, winnersByMonth, remainingDraws: chitGroup.duration_months - completedDraws.length } }); } catch (error) { console.error('Get draw statistics error:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }; // Delete monthly draw (manager only, for mistakes) const deleteMonthlyDraw = async (req, res) => { try { const { draw_id } = req.params; const managerId = req.user.id; // Find the draw const monthlyDraw = await MonthlyDraw.findOne({ where: { id: draw_id }, include: [ { model: ChitGroup, attributes: ['id', 'manager_id', 'name'] } ] }); if (!monthlyDraw) { return res.status(404).json({ success: false, message: 'Monthly draw not found' }); } // Check if user is the manager if (monthlyDraw.ChitGroup.manager_id !== managerId) { return res.status(403).json({ success: false, message: 'Only the group manager can delete draws' }); } // Store draw info for response const drawInfo = { month: monthlyDraw.month, year: monthlyDraw.year, groupName: monthlyDraw.ChitGroup.name }; // Delete the draw await monthlyDraw.destroy(); res.json({ success: true, message: `Draw for ${drawInfo.month}/${drawInfo.year} deleted successfully`, data: drawInfo }); } catch (error) { console.error('Delete monthly draw error:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }; // Update monthly draw (manager only, for corrections) const updateMonthlyDraw = async (req, res) => { try { const { draw_id } = req.params; const { winner_id, prize_amount, notes } = req.body; const managerId = req.user.id; // Find the draw const monthlyDraw = await MonthlyDraw.findOne({ where: { id: draw_id }, include: [ { model: ChitGroup, attributes: ['id', 'manager_id', 'name'] } ] }); if (!monthlyDraw) { return res.status(404).json({ success: false, message: 'Monthly draw not found' }); } // Check if user is the manager if (monthlyDraw.ChitGroup.manager_id !== managerId) { return res.status(403).json({ success: false, message: 'Only the group manager can update draws' }); } const updates = {}; // Update winner if provided if (winner_id) { // Verify the winner is a member of the group const member = await GroupMember.findOne({ where: { group_id: monthlyDraw.ChitGroup.id, user_id: winner_id, status: 'active' }, include: [{ model: User, attributes: ['id', 'full_name'] }] }); if (!member) { return res.status(400).json({ success: false, message: 'Winner must be an active member of this group' }); } updates.winner_id = winner_id; if (!notes) { updates.notes = `Winner updated to: ${member.User.full_name}`; } } // Update prize amount if provided if (prize_amount) { updates.prize_amount = prize_amount; } // Update notes if provided if (notes) { updates.notes = notes; } // Perform update await monthlyDraw.update(updates); // Fetch updated draw with winner details const updatedDraw = await MonthlyDraw.findOne({ where: { id: draw_id }, include: [ { model: User, as: 'winner', attributes: ['id', 'full_name', 'mobile_number'] } ] }); res.json({ success: true, message: 'Monthly draw updated successfully', data: updatedDraw }); } catch (error) { console.error('Update monthly draw error:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }; module.exports = { createMonthlyDraw, getGroupMonthlyDraws, getMonthlyDrawDetails, verifyDrawResult, getDrawStatistics, deleteMonthlyDraw, updateMonthlyDraw };