1367 lines
33 KiB
Markdown
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.
|