chitfund/Technical_Implementation_Gu...

1367 lines
33 KiB
Markdown

# 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<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
```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<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
# 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.