# 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) ```dart // 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) ```dart // 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) ```dart // 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) ```dart // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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; ``` ```javascript // 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 ```javascript // 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 ```javascript // 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 ```dart // 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 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 ```javascript // 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 ```dart // 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 data) { Get.snackbar( 'Payment Received', '₹${data['amount']} received for ${data['groupName']}', duration: Duration(seconds: 5), ); } void _handleDrawResult(Map 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 # 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"] ``` ```yaml # 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 ```bash # .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 ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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.