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:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user