feat: Phase 1 critical gaps — gamification API, Razorpay checkout, Google OAuth, notifications

- Fix gamification endpoints to use Node.js server (app.eventifyplus.com)
- Replace 6 mock gamification methods with real API calls (dashboard, leaderboard, shop, redeem, submit)
- Add booking models, service, payment service (Razorpay), checkout provider
- Add 3-step CheckoutScreen with Razorpay native modal integration
- Add Google OAuth login (Flutter + Django backend)
- Add full notifications system (Django model + 3 endpoints + Flutter UI)
- Register CheckoutProvider, NotificationProvider in main.dart MultiProvider
- Wire notification bell in HomeScreen app bar
- Add razorpay_flutter ^1.3.7 and google_sign_in ^6.2.2 packages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 15:46:53 +05:30
parent 847577c09d
commit 29e326b8fc
24 changed files with 1663 additions and 164 deletions

View File

@@ -1,146 +1,139 @@
// lib/features/gamification/services/gamification_service.dart
//
// Stub service using the real API contract from TechDocs v2.
// All methods currently return mock data.
// TODO: replace each mock block with a real ApiClient call once
// the backend endpoints are live on uat.eventifyplus.com.
// Real API service for the Contributor / Gamification module.
// Calls the Node.js gamification server at app.eventifyplus.com.
import 'dart:math';
import '../../../core/api/api_client.dart';
import '../../../core/api/api_endpoints.dart';
import '../../../core/storage/token_storage.dart';
import '../models/gamification_models.dart';
class GamificationService {
final ApiClient _api = ApiClient();
/// Helper: get current user's email for API calls.
Future<String> _getUserEmail() async {
final email = await TokenStorage.getUsername();
return email ?? '';
}
// ---------------------------------------------------------------------------
// User Gamification Profile
// TODO: replace with ApiClient.get(ApiEndpoints.gamificationProfile)
// Dashboard (profile + submissions)
// GET /v1/gamification/dashboard?user_id={email}
// ---------------------------------------------------------------------------
Future<UserGamificationProfile> getProfile() async {
await Future.delayed(const Duration(milliseconds: 400));
return const UserGamificationProfile(
userId: 'mock-user-001',
lifetimeEp: 320,
currentEp: 70,
currentRp: 45,
tier: ContributorTier.SILVER,
Future<DashboardResponse> getDashboard() async {
final email = await _getUserEmail();
final url = '${ApiEndpoints.gamificationDashboard}?user_id=$email';
final res = await _api.post(url, requiresAuth: false);
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
final rawSubs = res['submissions'] as List? ?? [];
final submissions = rawSubs
.map((s) => SubmissionModel.fromJson(Map<String, dynamic>.from(s as Map)))
.toList();
return DashboardResponse(
profile: UserGamificationProfile.fromJson(profileJson),
submissions: submissions,
);
}
/// Convenience — returns just the profile (backward-compatible with provider).
Future<UserGamificationProfile> getProfile() async {
final dashboard = await getDashboard();
return dashboard.profile;
}
// ---------------------------------------------------------------------------
// Leaderboard
// district: 'Overall Kerala' | 'Thiruvananthapuram' | 'Kollam' | ...
// timePeriod: 'all_time' | 'this_month'
// TODO: replace with ApiClient.get(ApiEndpoints.leaderboard, params: {'district': district, 'period': timePeriod})
// GET /v1/gamification/leaderboard?period=all|month&district=X&user_id=Y&limit=50
// ---------------------------------------------------------------------------
Future<List<LeaderboardEntry>> getLeaderboard({
Future<LeaderboardResponse> getLeaderboard({
required String district,
required String timePeriod,
}) async {
await Future.delayed(const Duration(milliseconds: 500));
final email = await _getUserEmail();
// Realistic mock names per district
final names = [
'Annette Black', 'Jerome Bell', 'Theresa Webb', 'Courtney Henry',
'Cameron Williamson', 'Dianne Russell', 'Wade Warren', 'Albert Flores',
'Kristin Watson', 'Guy Hawkins',
];
// Map Flutter filter values to API params
final period = timePeriod == 'this_month' ? 'month' : 'all';
final rng = Random(district.hashCode ^ timePeriod.hashCode);
final baseEp = timePeriod == 'this_month' ? 800 : 4500;
final params = <String, String>{
'period': period,
'user_id': email,
'limit': '50',
};
if (district != 'Overall Kerala') {
params['district'] = district;
}
final entries = List.generate(10, (i) {
final ep = baseEp - (i * (timePeriod == 'this_month' ? 55 : 280)) + rng.nextInt(30);
return LeaderboardEntry(
rank: i + 1,
username: names[i],
lifetimeEp: ep,
tier: tierFromEp(ep),
eventsCount: 149 - i * 12,
isCurrentUser: i == 7, // mock: current user is rank 8
final query = Uri(queryParameters: params).query;
final url = '${ApiEndpoints.leaderboard}?$query';
final res = await _api.post(url, requiresAuth: false);
final rawList = res['leaderboard'] as List? ?? [];
final entries = rawList
.map((e) => LeaderboardEntry.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
CurrentUserStats? currentUser;
if (res['currentUser'] != null && res['currentUser'] is Map) {
currentUser = CurrentUserStats.fromJson(
Map<String, dynamic>.from(res['currentUser'] as Map),
);
});
}
return entries;
}
// ---------------------------------------------------------------------------
// Redeem Shop Items
// TODO: replace with ApiClient.get(ApiEndpoints.shopItems)
// ---------------------------------------------------------------------------
Future<List<ShopItem>> getShopItems() async {
await Future.delayed(const Duration(milliseconds: 400));
return const [
ShopItem(
id: 'item-001',
name: 'Amazon ₹500 Voucher',
description: 'Redeem for any purchase on Amazon India.',
rpCost: 50,
stockQuantity: 20,
),
ShopItem(
id: 'item-002',
name: 'Swiggy ₹200 Voucher',
description: 'Free food delivery credit on Swiggy.',
rpCost: 20,
stockQuantity: 35,
),
ShopItem(
id: 'item-003',
name: 'Eventify Pro — 1 Month',
description: 'Premium access to Eventify.Plus features.',
rpCost: 30,
stockQuantity: 100,
),
ShopItem(
id: 'item-004',
name: 'Zomato ₹150 Voucher',
description: 'Discount on your next Zomato order.',
rpCost: 15,
stockQuantity: 50,
),
ShopItem(
id: 'item-005',
name: 'BookMyShow ₹300 Voucher',
description: 'Movie & event ticket credit on BookMyShow.',
rpCost: 30,
stockQuantity: 15,
),
ShopItem(
id: 'item-006',
name: 'Exclusive Badge',
description: 'Rare "Pioneer" badge for your profile.',
rpCost: 5,
stockQuantity: 0, // out of stock
),
];
}
// ---------------------------------------------------------------------------
// Redeem an item
// TODO: replace with ApiClient.post(ApiEndpoints.shopRedeem, body: {'item_id': itemId})
// ---------------------------------------------------------------------------
Future<RedemptionRecord> redeemItem(String itemId) async {
await Future.delayed(const Duration(milliseconds: 600));
// Generate a fake voucher code
final code = 'EVT-${itemId.toUpperCase().replaceAll('-', '').substring(0, 4)}-${Random().nextInt(9000) + 1000}';
return RedemptionRecord(
id: 'redemption-${DateTime.now().millisecondsSinceEpoch}',
itemId: itemId,
rpSpent: 0, // provider will look up cost
voucherCode: code,
timestamp: DateTime.now(),
return LeaderboardResponse(
entries: entries,
currentUser: currentUser,
totalParticipants: (res['totalParticipants'] as num?)?.toInt() ?? entries.length,
);
}
// ---------------------------------------------------------------------------
// Shop Items
// GET /v1/shop/items
// ---------------------------------------------------------------------------
Future<List<ShopItem>> getShopItems() async {
final res = await _api.post(ApiEndpoints.shopItems, requiresAuth: false);
final rawItems = res['items'] as List? ?? [];
return rawItems
.map((e) => ShopItem.fromJson(Map<String, dynamic>.from(e as Map)))
.toList();
}
// ---------------------------------------------------------------------------
// Redeem Item
// POST /v1/shop/redeem body: { user_id, item_id }
// ---------------------------------------------------------------------------
Future<RedemptionRecord> redeemItem(String itemId) async {
final email = await _getUserEmail();
final res = await _api.post(
ApiEndpoints.shopRedeem,
body: {'user_id': email, 'item_id': itemId},
requiresAuth: false,
);
final voucher = res['voucher'] as Map<String, dynamic>? ?? res;
return RedemptionRecord.fromJson(Map<String, dynamic>.from(voucher));
}
// ---------------------------------------------------------------------------
// Submit Contribution
// TODO: replace with ApiClient.post(ApiEndpoints.contributeSubmit, body: data)
// POST /v1/gamification/submit-event body: event data
// ---------------------------------------------------------------------------
Future<void> submitContribution(Map<String, dynamic> data) async {
await Future.delayed(const Duration(milliseconds: 800));
// Mock always succeeds
final email = await _getUserEmail();
final body = <String, dynamic>{'user_id': email, ...data};
await _api.post(
ApiEndpoints.contributeSubmit,
body: body,
requiresAuth: false,
);
}
// ---------------------------------------------------------------------------
// Achievements
// TODO: wire to achievements API when available on Node.js server
// ---------------------------------------------------------------------------
Future<List<AchievementBadge>> getAchievements() async {
await Future.delayed(const Duration(milliseconds: 300));