chitfund/backend/src/controllers/monthlyDrawController.js

677 lines
18 KiB
JavaScript

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