chitfund/Technical_Implementation_Gu...

33 KiB

LuckyChit - Technical Implementation Guide (Revised)

Unified Flutter Web Implementation

Executive Summary

This technical implementation guide provides detailed specifications for building the LuckyChit platform using a unified Flutter Web approach with dual interfaces - Manager Dashboard and Member Dashboard, with a Node.js backend and PostgreSQL database.


1. Flutter Web Implementation Strategy

1.1 Project Structure

luckychit/
├── lib/
│   ├── main.dart
│   ├── app.dart
│   ├── core/
│   │   ├── constants/
│   │   ├── utils/
│   │   ├── services/
│   │   └── models/
│   ├── features/
│   │   ├── auth/
│   │   ├── dashboard/
│   │   ├── groups/
│   │   ├── payments/
│   │   ├── lottery/
│   │   └── profile/
│   ├── shared/
│   │   ├── widgets/
│   │   ├── themes/
│   │   └── navigation/
│   └── interfaces/
│       ├── manager/
│       └── member/
├── web/
├── pubspec.yaml
└── README.md

1.2 Interface-Specific Adaptations

Manager Interface (Desktop-Optimized)

// lib/interfaces/manager/manager_app.dart
class ManagerApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LuckyChit Manager',
      theme: ThemeData(
        primarySwatch: Colors.green,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ManagerDashboard(),
      builder: (context, child) {
        return ResponsiveWrapper.builder(
          child,
          maxWidth: 1400,
          minWidth: 800,
          defaultScale: true,
          breakpoints: [
            ResponsiveBreakpoint.resize(800, name: TABLET),
            ResponsiveBreakpoint.resize(1200, name: DESKTOP),
          ],
        );
      },
    );
  }
}

Member Interface (Mobile-Responsive)

// lib/interfaces/member/member_app.dart
class MemberApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LuckyChit',
      theme: ThemeData(
        primarySwatch: Colors.green,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MemberDashboard(),
      builder: (context, child) {
        return ResponsiveWrapper.builder(
          child,
          maxWidth: 600,
          minWidth: 320,
          defaultScale: true,
          breakpoints: [
            ResponsiveBreakpoint.resize(320, name: MOBILE),
            ResponsiveBreakpoint.resize(600, name: TABLET),
          ],
        );
      },
    );
  }
}

1.3 Responsive Design Implementation

Manager Dashboard (Desktop-Optimized)

// lib/features/dashboard/manager_dashboard.dart
class ManagerDashboard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Manager Dashboard')),
      body: ResponsiveBuilder(
        builder: (context, sizingInformation) {
          if (sizingInformation.isDesktop) {
            return DesktopDashboard();
          } else {
            return TabletDashboard();
          }
        },
      ),
    );
  }
}

class DesktopDashboard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        // Sidebar Navigation
        Container(
          width: 280,
          child: ManagerSidebar(),
        ),
        // Main Content Area
        Expanded(
          child: Column(
            children: [
              // Statistics Cards
              Row(
                children: [
                  Expanded(child: StatsCard(title: 'Active Groups', value: '5')),
                  Expanded(child: StatsCard(title: 'Total Members', value: '120')),
                  Expanded(child: StatsCard(title: 'This Month Collection', value: '₹2.5L')),
                  Expanded(child: StatsCard(title: 'Pending Actions', value: '3')),
                ],
              ),
              // Recent Activities and Charts
              Expanded(
                child: Row(
                  children: [
                    Expanded(child: RecentActivities()),
                    Expanded(child: CollectionChart()),
                  ],
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

Member Dashboard (Mobile-Responsive)

// lib/features/dashboard/member_dashboard.dart
class MemberDashboard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My Chit Groups')),
      body: RefreshIndicator(
        onRefresh: () async {
          // Refresh data
        },
        child: ListView(
          padding: EdgeInsets.all(16),
          children: [
            // Payment Due Card
            PaymentDueCard(),
            SizedBox(height: 16),
            // My Groups
            Text('My Groups', style: Theme.of(context).textTheme.headline6),
            SizedBox(height: 8),
            GroupList(),
            SizedBox(height: 16),
            // Recent Activity
            Text('Recent Activity', style: Theme.of(context).textTheme.headline6),
            SizedBox(height: 8),
            ActivityList(),
          ],
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.payment), label: 'Payments'),
          BottomNavigationBarItem(icon: Icon(Icons.casino), label: 'Lottery'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }
}

2. Backend API Implementation

2.1 Express.js Server Structure

// server/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const authRoutes = require('./routes/auth');
const groupRoutes = require('./routes/groups');
const paymentRoutes = require('./routes/payments');
const lotteryRoutes = require('./routes/lottery');

const app = express();

// Security middleware
app.use(helmet());
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true
}));

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);

// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/groups', groupRoutes);
app.use('/api/payments', paymentRoutes);
app.use('/api/lottery', lotteryRoutes);

// Error handling
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong!' });
});

module.exports = app;

2.2 Authentication Service

// server/services/authService.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { User } = require('../models');

class AuthService {
  async createMemberAccount(mobileNumber, fullName, managerId) {
    // Generate temporary password
    const tempPassword = this.generateTempPassword();
    const hashedPassword = await bcrypt.hash(tempPassword, 12);
    
    const user = await User.create({
      mobile_number: mobileNumber,
      full_name: fullName,
      password_hash: hashedPassword,
      role: 'member',
      created_by: managerId,
      is_active: true
    });
    
    return {
      user: {
        id: user.id,
        mobile_number: user.mobile_number,
        full_name: user.full_name,
        role: user.role
      },
      tempPassword
    };
  }
  
  async login(mobileNumber, password) {
    const user = await User.findOne({ 
      where: { mobile_number: mobileNumber, is_active: true }
    });
    
    if (!user) {
      throw new Error('User not found');
    }
    
    const isValidPassword = await bcrypt.compare(password, user.password_hash);
    if (!isValidPassword) {
      throw new Error('Invalid password');
    }
    
    const token = jwt.sign(
      { 
        userId: user.id, 
        role: user.role,
        mobileNumber: user.mobile_number 
      },
      process.env.JWT_SECRET,
      { expiresIn: '24h' }
    );
    
    return {
      token,
      user: {
        id: user.id,
        mobile_number: user.mobile_number,
        full_name: user.full_name,
        role: user.role
      }
    };
  }
  
  async changePassword(userId, currentPassword, newPassword) {
    const user = await User.findByPk(userId);
    if (!user) {
      throw new Error('User not found');
    }
    
    const isValidPassword = await bcrypt.compare(currentPassword, user.password_hash);
    if (!isValidPassword) {
      throw new Error('Current password is incorrect');
    }
    
    const hashedNewPassword = await bcrypt.hash(newPassword, 12);
    await user.update({ password_hash: hashedNewPassword });
    
    return { message: 'Password changed successfully' };
  }
  
  generateTempPassword() {
    return Math.random().toString(36).slice(-8);
  }
}

module.exports = new AuthService();

2.3 Group Management Service

// server/services/groupService.js
const { ChitGroup, GroupMember, User } = require('../models');
const { Op } = require('sequelize');

class GroupService {
  async createGroup(groupData, managerId) {
    const group = await ChitGroup.create({
      ...groupData,
      manager_id: managerId,
      status: 'forming'
    });
    
    return group;
  }
  
  async addMemberToGroup(groupId, memberId, managerId) {
    // Verify manager owns the group
    const group = await ChitGroup.findOne({
      where: { id: groupId, manager_id: managerId }
    });
    
    if (!group) {
      throw new Error('Group not found or access denied');
    }
    
    // Check if member is already in group
    const existingMember = await GroupMember.findOne({
      where: { group_id: groupId, user_id: memberId }
    });
    
    if (existingMember) {
      throw new Error('Member already in group');
    }
    
    // Check group capacity
    const memberCount = await GroupMember.count({
      where: { group_id: groupId, status: 'active' }
    });
    
    if (memberCount >= group.max_members) {
      throw new Error('Group is full');
    }
    
    const groupMember = await GroupMember.create({
      group_id: groupId,
      user_id: memberId,
      status: 'active'
    });
    
    return groupMember;
  }
  
  async getManagerGroups(managerId) {
    const groups = await ChitGroup.findAll({
      where: { manager_id: managerId },
      include: [
        {
          model: GroupMember,
          include: [{ model: User, attributes: ['id', 'full_name', 'mobile_number'] }]
        }
      ],
      order: [['created_at', 'DESC']]
    });
    
    return groups;
  }
  
  async getMemberGroups(memberId) {
    const groupMemberships = await GroupMember.findAll({
      where: { user_id: memberId, status: 'active' },
      include: [
        {
          model: ChitGroup,
          include: [{ model: User, as: 'manager', attributes: ['id', 'full_name', 'mobile_number'] }]
        }
      ]
    });
    
    return groupMemberships.map(gm => gm.ChitGroup);
  }
}

module.exports = new GroupService();

2.4 Lottery Service Implementation

// server/services/lotteryService.js
const crypto = require('crypto');
const { MonthlyDraw, GroupMember, Payment, User } = require('../models');
const { Op } = require('sequelize');

class LotteryService {
  constructor() {
    this.serverSeed = this.generateServerSeed();
    this.serverSeedHash = this.hashSeed(this.serverSeed);
  }
  
  generateServerSeed() {
    return crypto.randomBytes(32).toString('hex');
  }
  
  hashSeed(seed) {
    return crypto.createHash('sha256').update(seed).digest('hex');
  }
  
  async getEligibleMembers(groupId, month, year) {
    // Get all active members
    const groupMembers = await GroupMember.findAll({
      where: { group_id: groupId, status: 'active' },
      include: [{ model: User, attributes: ['id', 'full_name', 'mobile_number'] }]
    });
    
    // Get members who have paid for this month
    const paidMembers = await Payment.findAll({
      where: {
        group_id: groupId,
        month: month,
        year: year,
        status: 'success'
      },
      attributes: ['user_id']
    });
    
    const paidMemberIds = paidMembers.map(pm => pm.user_id);
    
    // Get members who haven't won before
    const previousWinners = await MonthlyDraw.findAll({
      where: { group_id: groupId },
      attributes: ['winner_id']
    });
    
    const previousWinnerIds = previousWinners.map(pw => pw.winner_id);
    
    // Filter eligible members
    const eligibleMembers = groupMembers.filter(member => 
      paidMemberIds.includes(member.user_id) && 
      !previousWinnerIds.includes(member.user_id)
    );
    
    return eligibleMembers;
  }
  
  async conductDraw(groupId, month, year, managerId) {
    // Verify manager owns the group
    const group = await ChitGroup.findOne({
      where: { id: groupId, manager_id: managerId }
    });
    
    if (!group) {
      throw new Error('Group not found or access denied');
    }
    
    // Check if draw already exists
    const existingDraw = await MonthlyDraw.findOne({
      where: { group_id: groupId, month: month, year: year }
    });
    
    if (existingDraw) {
      throw new Error('Draw already conducted for this month');
    }
    
    // Get eligible members
    const eligibleMembers = await this.getEligibleMembers(groupId, month, year);
    
    if (eligibleMembers.length === 0) {
      throw new Error('No eligible members for draw');
    }
    
    // Generate client seed and nonce
    const clientSeed = crypto.randomBytes(16).toString('hex');
    const nonce = Date.now();
    
    // Generate result
    const result = this.generateResult(clientSeed, nonce, eligibleMembers);
    
    // Calculate prize amount
    const paidCount = eligibleMembers.length;
    const prizeAmount = (paidCount * group.monthly_installment) * 
                       (1 - group.foreman_commission_percentage / 100);
    
    // Create draw record
    const draw = await MonthlyDraw.create({
      group_id: groupId,
      month: month,
      year: year,
      draw_date: new Date(),
      eligible_members: eligibleMembers.map(em => ({
        id: em.user_id,
        name: em.User.full_name,
        mobile: em.User.mobile_number
      })),
      winner_id: result.winnerId,
      prize_amount: prizeAmount,
      server_seed: this.serverSeed,
      server_seed_hash: this.serverSeedHash,
      client_seed: clientSeed,
      nonce: nonce,
      result_hash: result.resultHash,
      status: 'completed'
    });
    
    // Generate new server seed for next draw
    this.serverSeed = this.generateServerSeed();
    this.serverSeedHash = this.hashSeed(this.serverSeed);
    
    return {
      draw,
      result,
      eligibleMembers: eligibleMembers.length
    };
  }
  
  generateResult(clientSeed, nonce, eligibleMembers) {
    const combined = this.serverSeed + clientSeed + nonce.toString();
    const hash = crypto.createHash('sha256').update(combined).digest('hex');
    
    const randomNumber = parseInt(hash.substring(0, 8), 16);
    const winnerIndex = randomNumber % eligibleMembers.length;
    
    return {
      winnerId: eligibleMembers[winnerIndex].user_id,
      serverSeed: this.serverSeed,
      serverSeedHash: this.serverSeedHash,
      clientSeed,
      nonce,
      resultHash: hash,
      proof: {
        combined,
        randomNumber,
        winnerIndex
      }
    };
  }
  
  verifyResult(serverSeed, clientSeed, nonce, resultHash, eligibleMembers, winnerId) {
    const combined = serverSeed + clientSeed + nonce.toString();
    const calculatedHash = crypto.createHash('sha256').update(combined).digest('hex');
    
    if (calculatedHash !== resultHash) {
      return false;
    }
    
    const randomNumber = parseInt(calculatedHash.substring(0, 8), 16);
    const winnerIndex = randomNumber % eligibleMembers.length;
    
    return eligibleMembers[winnerIndex].user_id === winnerId;
  }
}

module.exports = new LotteryService();

3. Database Implementation

3.1 Sequelize Models

// server/models/User.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');

const User = sequelize.define('User', {
  id: {
    type: DataTypes.UUID,
    defaultValue: DataTypes.UUIDV4,
    primaryKey: true
  },
  mobile_number: {
    type: DataTypes.STRING(15),
    allowNull: false,
    unique: true,
    validate: {
      is: /^[0-9]{10}$/
    }
  },
  full_name: {
    type: DataTypes.STRING(255),
    allowNull: false
  },
  password_hash: {
    type: DataTypes.STRING(255),
    allowNull: false
  },
  role: {
    type: DataTypes.ENUM('manager', 'member'),
    allowNull: false
  },
  created_by: {
    type: DataTypes.UUID,
    allowNull: true,
    references: {
      model: 'Users',
      key: 'id'
    }
  },
  is_active: {
    type: DataTypes.BOOLEAN,
    defaultValue: true
  }
}, {
  tableName: 'users',
  timestamps: true,
  createdAt: 'created_at',
  updatedAt: 'updated_at'
});

module.exports = User;
// server/models/ChitGroup.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');

const ChitGroup = sequelize.define('ChitGroup', {
  id: {
    type: DataTypes.UUID,
    defaultValue: DataTypes.UUIDV4,
    primaryKey: true
  },
  name: {
    type: DataTypes.STRING(255),
    allowNull: false
  },
  total_value: {
    type: DataTypes.DECIMAL(15, 2),
    allowNull: false,
    validate: {
      min: 0
    }
  },
  monthly_installment: {
    type: DataTypes.DECIMAL(15, 2),
    allowNull: false,
    validate: {
      min: 0
    }
  },
  duration_months: {
    type: DataTypes.INTEGER,
    allowNull: false,
    validate: {
      min: 1
    }
  },
  max_members: {
    type: DataTypes.INTEGER,
    allowNull: false,
    validate: {
      min: 2
    }
  },
  foreman_commission_percentage: {
    type: DataTypes.DECIMAL(5, 2),
    allowNull: false,
    validate: {
      min: 0,
      max: 100
    }
  },
  draw_date: {
    type: DataTypes.INTEGER,
    allowNull: false,
    validate: {
      min: 1,
      max: 31
    }
  },
  status: {
    type: DataTypes.ENUM('forming', 'active', 'completed'),
    defaultValue: 'forming'
  },
  manager_id: {
    type: DataTypes.UUID,
    allowNull: false,
    references: {
      model: 'Users',
      key: 'id'
    }
  }
}, {
  tableName: 'chit_groups',
  timestamps: true,
  createdAt: 'created_at',
  updatedAt: false
});

module.exports = ChitGroup;

3.2 Model Associations

// server/models/index.js
const User = require('./User');
const ChitGroup = require('./ChitGroup');
const GroupMember = require('./GroupMember');
const MonthlyDraw = require('./MonthlyDraw');
const Payment = require('./Payment');

// User associations
User.hasMany(ChitGroup, { as: 'managedGroups', foreignKey: 'manager_id' });
User.hasMany(GroupMember, { foreignKey: 'user_id' });
User.hasMany(MonthlyDraw, { as: 'wins', foreignKey: 'winner_id' });
User.hasMany(Payment, { foreignKey: 'user_id' });
User.belongsTo(User, { as: 'creator', foreignKey: 'created_by' });

// ChitGroup associations
ChitGroup.belongsTo(User, { as: 'manager', foreignKey: 'manager_id' });
ChitGroup.hasMany(GroupMember, { foreignKey: 'group_id' });
ChitGroup.hasMany(MonthlyDraw, { foreignKey: 'group_id' });
ChitGroup.hasMany(Payment, { foreignKey: 'group_id' });

// GroupMember associations
GroupMember.belongsTo(User, { foreignKey: 'user_id' });
GroupMember.belongsTo(ChitGroup, { foreignKey: 'group_id' });

// MonthlyDraw associations
MonthlyDraw.belongsTo(ChitGroup, { foreignKey: 'group_id' });
MonthlyDraw.belongsTo(User, { as: 'winner', foreignKey: 'winner_id' });

// Payment associations
Payment.belongsTo(User, { foreignKey: 'user_id' });
Payment.belongsTo(ChitGroup, { foreignKey: 'group_id' });

module.exports = {
  User,
  ChitGroup,
  GroupMember,
  MonthlyDraw,
  Payment
};

4. Payment Integration

4.1 Razorpay Integration

// server/services/paymentService.js
const Razorpay = require('razorpay');
const crypto = require('crypto');

class PaymentService {
  constructor() {
    this.razorpay = new Razorpay({
      key_id: process.env.RAZORPAY_KEY_ID,
      key_secret: process.env.RAZORPAY_KEY_SECRET
    });
  }
  
  async createPaymentOrder(amount, currency = 'INR', receipt = null) {
    const options = {
      amount: amount * 100, // Razorpay expects amount in paise
      currency: currency,
      receipt: receipt || `receipt_${Date.now()}`,
      payment_capture: 1
    };
    
    try {
      const order = await this.razorpay.orders.create(options);
      return order;
    } catch (error) {
      throw new Error(`Payment order creation failed: ${error.message}`);
    }
  }
  
  async verifyPayment(paymentId, orderId, signature) {
    const text = orderId + '|' + paymentId;
    const signatureGenerated = crypto
      .createHmac('sha256', process.env.RAZORPAY_KEY_SECRET)
      .update(text)
      .digest('hex');
    
    if (signatureGenerated === signature) {
      return true;
    } else {
      return false;
    }
  }
  
  async processPayment(paymentData) {
    const { paymentId, orderId, signature, amount, groupId, userId, month, year } = paymentData;
    
    // Verify payment
    const isValid = await this.verifyPayment(paymentId, orderId, signature);
    
    if (!isValid) {
      throw new Error('Payment verification failed');
    }
    
    // Create payment record
    const payment = await Payment.create({
      group_id: groupId,
      user_id: userId,
      month: month,
      year: year,
      amount: amount,
      payment_method: 'upi',
      transaction_id: paymentId,
      status: 'success',
      paid_at: new Date()
    });
    
    return payment;
  }
}

module.exports = new PaymentService();

4.2 Flutter Payment Integration

// lib/services/payment_service.dart
import 'package:razorpay_flutter/razorpay_flutter.dart';

class PaymentService {
  late Razorpay _razorpay;
  
  PaymentService() {
    _razorpay = Razorpay();
    _razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
    _razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
    _razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
  }
  
  Future<void> makePayment({
    required double amount,
    required String groupId,
    required int month,
    required int year,
  }) async {
    try {
      // Create order on backend
      final orderResponse = await ApiService.createPaymentOrder(
        amount: amount,
        groupId: groupId,
        month: month,
        year: year,
      );
      
      final options = {
        'key': 'rzp_test_YOUR_KEY',
        'amount': (amount * 100).toInt(), // Convert to paise
        'name': 'LuckyChit',
        'description': 'Monthly Chit Payment',
        'order_id': orderResponse['orderId'],
        'prefill': {
          'contact': '9876543210',
          'email': 'user@example.com'
        },
        'external': {
          'wallets': ['paytm', 'phonepe']
        }
      };
      
      _razorpay.open(options);
    } catch (e) {
      throw Exception('Payment initiation failed: $e');
    }
  }
  
  void _handlePaymentSuccess(PaymentSuccessResponse response) async {
    try {
      // Verify payment on backend
      await ApiService.verifyPayment(
        paymentId: response.paymentId!,
        orderId: response.orderId!,
        signature: response.signature!,
      );
      
      // Update UI
      Get.snackbar('Success', 'Payment completed successfully!');
    } catch (e) {
      Get.snackbar('Error', 'Payment verification failed');
    }
  }
  
  void _handlePaymentError(PaymentFailureResponse response) {
    Get.snackbar('Error', 'Payment failed: ${response.message}');
  }
  
  void _handleExternalWallet(ExternalWalletResponse response) {
    Get.snackbar('External Wallet', 'External wallet selected: ${response.walletName}');
  }
  
  void dispose() {
    _razorpay.clear();
  }
}

5. Real-time Notifications

5.1 WebSocket Implementation

// server/services/notificationService.js
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');

class NotificationService {
  constructor(server) {
    this.wss = new WebSocket.Server({ server });
    this.clients = new Map(); // userId -> WebSocket
    
    this.wss.on('connection', (ws, req) => {
      this.handleConnection(ws, req);
    });
  }
  
  handleConnection(ws, req) {
    // Extract token from query string
    const url = new URL(req.url, 'http://localhost');
    const token = url.searchParams.get('token');
    
    if (!token) {
      ws.close(1008, 'Token required');
      return;
    }
    
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      const userId = decoded.userId;
      
      this.clients.set(userId, ws);
      
      ws.on('close', () => {
        this.clients.delete(userId);
      });
      
      ws.on('error', (error) => {
        console.error('WebSocket error:', error);
        this.clients.delete(userId);
      });
      
    } catch (error) {
      ws.close(1008, 'Invalid token');
    }
  }
  
  sendToUser(userId, message) {
    const ws = this.clients.get(userId);
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(message));
    }
  }
  
  sendToGroup(groupMemberIds, message) {
    groupMemberIds.forEach(userId => {
      this.sendToUser(userId, message);
    });
  }
  
  notifyPaymentReceived(userId, groupName, amount) {
    this.sendToUser(userId, {
      type: 'payment_received',
      data: {
        groupName,
        amount,
        timestamp: new Date().toISOString()
      }
    });
  }
  
  notifyDrawResult(groupMemberIds, winnerName, prizeAmount) {
    this.sendToGroup(groupMemberIds, {
      type: 'draw_result',
      data: {
        winnerName,
        prizeAmount,
        timestamp: new Date().toISOString()
      }
    });
  }
}

module.exports = NotificationService;

5.2 Flutter WebSocket Client

// lib/services/websocket_service.dart
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/io.dart';

class WebSocketService {
  WebSocketChannel? _channel;
  String? _token;
  
  void connect(String token) {
    _token = token;
    final uri = Uri.parse('ws://localhost:3000?token=$token');
    _channel = IOWebSocketChannel.connect(uri);
    
    _channel!.stream.listen(
      (message) {
        _handleMessage(message);
      },
      onError: (error) {
        print('WebSocket error: $error');
        _reconnect();
      },
      onDone: () {
        print('WebSocket connection closed');
        _reconnect();
      },
    );
  }
  
  void _handleMessage(dynamic message) {
    try {
      final data = jsonDecode(message);
      
      switch (data['type']) {
        case 'payment_received':
          _handlePaymentReceived(data['data']);
          break;
        case 'draw_result':
          _handleDrawResult(data['data']);
          break;
        default:
          print('Unknown message type: ${data['type']}');
      }
    } catch (e) {
      print('Error parsing WebSocket message: $e');
    }
  }
  
  void _handlePaymentReceived(Map<String, dynamic> data) {
    Get.snackbar(
      'Payment Received',
      '₹${data['amount']} received for ${data['groupName']}',
      duration: Duration(seconds: 5),
    );
  }
  
  void _handleDrawResult(Map<String, dynamic> data) {
    Get.dialog(
      AlertDialog(
        title: Text('Draw Result'),
        content: Text('${data['winnerName']} won ₹${data['prizeAmount']}!'),
        actions: [
          TextButton(
            onPressed: () => Get.back(),
            child: Text('OK'),
          ),
        ],
      ),
    );
  }
  
  void _reconnect() {
    if (_token != null) {
      Future.delayed(Duration(seconds: 5), () {
        connect(_token!);
      });
    }
  }
  
  void disconnect() {
    _channel?.sink.close();
  }
}

6. Deployment Configuration

6.1 Docker Configuration

# Dockerfile
FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Expose port
EXPOSE 3000

# Start application
CMD ["npm", "start"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:password@db:5432/luckychit
      - JWT_SECRET=your-secret-key
      - RAZORPAY_KEY_ID=your-razorpay-key
      - RAZORPAY_KEY_SECRET=your-razorpay-secret
    depends_on:
      - db
      - redis
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=luckychit
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    restart: unless-stopped

volumes:
  postgres_data:

6.2 Environment Configuration

# .env.example
NODE_ENV=development
PORT=3000

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/luckychit

# JWT
JWT_SECRET=your-super-secret-jwt-key

# Razorpay
RAZORPAY_KEY_ID=rzp_test_your_key_id
RAZORPAY_KEY_SECRET=your_razorpay_secret

# Firebase (for push notifications)
FIREBASE_PROJECT_ID=your-project-id
FIREBASE_PRIVATE_KEY=your-private-key
FIREBASE_CLIENT_EMAIL=your-client-email

# CORS
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080

# Redis
REDIS_URL=redis://localhost:6379

7. Testing Strategy

7.1 Unit Tests

// tests/services/lotteryService.test.js
const LotteryService = require('../../server/services/lotteryService');
const { User, ChitGroup, GroupMember, Payment } = require('../../server/models');

describe('LotteryService', () => {
  beforeEach(async () => {
    // Setup test database
    await sequelize.sync({ force: true });
  });
  
  describe('getEligibleMembers', () => {
    it('should return only paid members who haven\'t won before', async () => {
      // Test implementation
    });
  });
  
  describe('conductDraw', () => {
    it('should generate provably fair result', async () => {
      // Test implementation
    });
  });
  
  describe('verifyResult', () => {
    it('should correctly verify draw fairness', async () => {
      // Test implementation
    });
  });
});

7.2 Integration Tests

// tests/integration/payment.test.js
const request = require('supertest');
const app = require('../../server/app');

describe('Payment API', () => {
  describe('POST /api/payments/create-order', () => {
    it('should create payment order successfully', async () => {
      const response = await request(app)
        .post('/api/payments/create-order')
        .set('Authorization', `Bearer ${validToken}`)
        .send({
          amount: 5000,
          groupId: 'group-id',
          month: 3,
          year: 2024
        });
      
      expect(response.status).toBe(200);
      expect(response.body).toHaveProperty('orderId');
    });
  });
});

8. Monitoring and Logging

8.1 Application Monitoring

// server/middleware/monitoring.js
const winston = require('winston');
const morgan = require('morgan');

// Configure logging
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'luckychit-api' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

// Request logging middleware
const requestLogger = morgan('combined', {
  stream: {
    write: (message) => logger.info(message.trim())
  }
});

module.exports = { logger, requestLogger };

8.2 Performance Monitoring

// server/middleware/performance.js
const { logger } = require('./monitoring');

const performanceMiddleware = (req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info('Request completed', {
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration: `${duration}ms`
    });
  });
  
  next();
};

module.exports = performanceMiddleware;

This technical implementation guide provides a comprehensive foundation for building the LuckyChit platform with the revised requirements. The unified Flutter approach ensures code reuse between web and mobile platforms while maintaining platform-specific optimizations.