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
This commit is contained in:
87
lib/features/booking/models/booking_models.dart
Normal file
87
lib/features/booking/models/booking_models.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
// lib/features/booking/models/booking_models.dart
|
||||
|
||||
class TicketMetaModel {
|
||||
final int id;
|
||||
final int eventId;
|
||||
final String ticketType;
|
||||
final double price;
|
||||
final int availableQuantity;
|
||||
final String? description;
|
||||
|
||||
const TicketMetaModel({
|
||||
required this.id,
|
||||
required this.eventId,
|
||||
required this.ticketType,
|
||||
required this.price,
|
||||
this.availableQuantity = 0,
|
||||
this.description,
|
||||
});
|
||||
|
||||
factory TicketMetaModel.fromJson(Map<String, dynamic> json) {
|
||||
return TicketMetaModel(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
eventId: (json['event_id'] as num?)?.toInt() ?? (json['event'] as num?)?.toInt() ?? 0,
|
||||
ticketType: json['ticket_type'] as String? ?? json['name'] as String? ?? '',
|
||||
price: (json['price'] as num?)?.toDouble() ?? 0.0,
|
||||
availableQuantity: (json['available_quantity'] as num?)?.toInt() ?? 0,
|
||||
description: json['description'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CartItemModel {
|
||||
final TicketMetaModel ticket;
|
||||
int quantity;
|
||||
|
||||
CartItemModel({required this.ticket, this.quantity = 1});
|
||||
|
||||
double get subtotal => ticket.price * quantity;
|
||||
}
|
||||
|
||||
class ShippingDetails {
|
||||
final String name;
|
||||
final String email;
|
||||
final String phone;
|
||||
final String? address;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? zipCode;
|
||||
|
||||
const ShippingDetails({
|
||||
required this.name,
|
||||
required this.email,
|
||||
required this.phone,
|
||||
this.address,
|
||||
this.city,
|
||||
this.state,
|
||||
this.zipCode,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
if (address != null) 'address': address,
|
||||
if (city != null) 'city': city,
|
||||
if (state != null) 'state': state,
|
||||
if (zipCode != null) 'zip_code': zipCode,
|
||||
};
|
||||
}
|
||||
|
||||
class OrderSummary {
|
||||
final List<CartItemModel> items;
|
||||
final double subtotal;
|
||||
final double discount;
|
||||
final double tax;
|
||||
final double total;
|
||||
final String? couponCode;
|
||||
|
||||
const OrderSummary({
|
||||
required this.items,
|
||||
required this.subtotal,
|
||||
this.discount = 0,
|
||||
this.tax = 0,
|
||||
required this.total,
|
||||
this.couponCode,
|
||||
});
|
||||
}
|
||||
147
lib/features/booking/providers/checkout_provider.dart
Normal file
147
lib/features/booking/providers/checkout_provider.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
// lib/features/booking/providers/checkout_provider.dart
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../../core/utils/error_utils.dart';
|
||||
import '../models/booking_models.dart';
|
||||
import '../services/booking_service.dart';
|
||||
|
||||
enum CheckoutStep { tickets, details, payment, confirmation }
|
||||
|
||||
class CheckoutProvider extends ChangeNotifier {
|
||||
final BookingService _service = BookingService();
|
||||
|
||||
// Event being booked
|
||||
int? eventId;
|
||||
String eventName = '';
|
||||
|
||||
// Step tracking
|
||||
CheckoutStep currentStep = CheckoutStep.tickets;
|
||||
|
||||
// Ticket selection
|
||||
List<TicketMetaModel> availableTickets = [];
|
||||
List<CartItemModel> cart = [];
|
||||
|
||||
// Shipping
|
||||
ShippingDetails? shippingDetails;
|
||||
|
||||
// Coupon
|
||||
String? couponCode;
|
||||
|
||||
// Status
|
||||
bool loading = false;
|
||||
String? error;
|
||||
String? paymentId;
|
||||
|
||||
/// Initialize checkout for an event.
|
||||
Future<void> initForEvent(int eventId, String eventName) async {
|
||||
this.eventId = eventId;
|
||||
this.eventName = eventName;
|
||||
currentStep = CheckoutStep.tickets;
|
||||
cart = [];
|
||||
shippingDetails = null;
|
||||
couponCode = null;
|
||||
paymentId = null;
|
||||
error = null;
|
||||
loading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
availableTickets = await _service.getTicketMeta(eventId);
|
||||
} catch (e) {
|
||||
error = userFriendlyError(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Add or update cart item.
|
||||
void setTicketQuantity(TicketMetaModel ticket, int qty) {
|
||||
cart.removeWhere((c) => c.ticket.id == ticket.id);
|
||||
if (qty > 0) {
|
||||
cart.add(CartItemModel(ticket: ticket, quantity: qty));
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
double get subtotal => cart.fold(0, (sum, item) => sum + item.subtotal);
|
||||
double get total => subtotal; // expand with discount/tax later
|
||||
|
||||
bool get hasItems => cart.isNotEmpty;
|
||||
|
||||
/// Move to next step.
|
||||
void nextStep() {
|
||||
if (currentStep == CheckoutStep.tickets && hasItems) {
|
||||
currentStep = CheckoutStep.details;
|
||||
} else if (currentStep == CheckoutStep.details && shippingDetails != null) {
|
||||
currentStep = CheckoutStep.payment;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Move to previous step.
|
||||
void previousStep() {
|
||||
if (currentStep == CheckoutStep.payment) {
|
||||
currentStep = CheckoutStep.details;
|
||||
} else if (currentStep == CheckoutStep.details) {
|
||||
currentStep = CheckoutStep.tickets;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set shipping details from form.
|
||||
void setShipping(ShippingDetails details) {
|
||||
shippingDetails = details;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Process checkout on backend.
|
||||
Future<Map<String, dynamic>> processCheckout() async {
|
||||
loading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final tickets = cart.map((c) => {
|
||||
'ticket_meta_id': c.ticket.id,
|
||||
'quantity': c.quantity,
|
||||
}).toList();
|
||||
|
||||
final res = await _service.processCheckout(
|
||||
eventId: eventId!,
|
||||
tickets: tickets,
|
||||
shippingDetails: shippingDetails?.toJson() ?? {},
|
||||
couponCode: couponCode,
|
||||
);
|
||||
return res;
|
||||
} catch (e) {
|
||||
error = userFriendlyError(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark payment as complete.
|
||||
void markPaymentSuccess(String id) {
|
||||
paymentId = id;
|
||||
currentStep = CheckoutStep.confirmation;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Reset checkout state.
|
||||
void reset() {
|
||||
eventId = null;
|
||||
eventName = '';
|
||||
currentStep = CheckoutStep.tickets;
|
||||
availableTickets = [];
|
||||
cart = [];
|
||||
shippingDetails = null;
|
||||
couponCode = null;
|
||||
paymentId = null;
|
||||
error = null;
|
||||
loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
53
lib/features/booking/services/booking_service.dart
Normal file
53
lib/features/booking/services/booking_service.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
// lib/features/booking/services/booking_service.dart
|
||||
|
||||
import '../../../core/api/api_client.dart';
|
||||
import '../../../core/api/api_endpoints.dart';
|
||||
import '../models/booking_models.dart';
|
||||
|
||||
class BookingService {
|
||||
final ApiClient _api = ApiClient();
|
||||
|
||||
/// Fetch available ticket types for an event.
|
||||
Future<List<TicketMetaModel>> getTicketMeta(int eventId) async {
|
||||
final res = await _api.post(
|
||||
ApiEndpoints.ticketMetaList,
|
||||
body: {'event_id': eventId},
|
||||
);
|
||||
final rawList = res['ticket_metas'] ?? res['tickets'] ?? res['data'] ?? [];
|
||||
if (rawList is List) {
|
||||
return rawList
|
||||
.map((e) => TicketMetaModel.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Add item to cart.
|
||||
Future<Map<String, dynamic>> addToCart({
|
||||
required int ticketMetaId,
|
||||
required int quantity,
|
||||
}) async {
|
||||
return await _api.post(
|
||||
ApiEndpoints.cartAdd,
|
||||
body: {'ticket_meta_id': ticketMetaId, 'quantity': quantity},
|
||||
);
|
||||
}
|
||||
|
||||
/// Process checkout — creates booking + returns order ID for payment.
|
||||
Future<Map<String, dynamic>> processCheckout({
|
||||
required int eventId,
|
||||
required List<Map<String, dynamic>> tickets,
|
||||
required Map<String, dynamic> shippingDetails,
|
||||
String? couponCode,
|
||||
}) async {
|
||||
return await _api.post(
|
||||
ApiEndpoints.checkout,
|
||||
body: {
|
||||
'event_id': eventId,
|
||||
'tickets': tickets,
|
||||
'shipping': shippingDetails,
|
||||
if (couponCode != null) 'coupon_code': couponCode,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/features/booking/services/payment_service.dart
Normal file
67
lib/features/booking/services/payment_service.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
// lib/features/booking/services/payment_service.dart
|
||||
|
||||
import 'package:razorpay_flutter/razorpay_flutter.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
typedef PaymentSuccessCallback = void Function(PaymentSuccessResponse response);
|
||||
typedef PaymentErrorCallback = void Function(PaymentFailureResponse response);
|
||||
typedef ExternalWalletCallback = void Function(ExternalWalletResponse response);
|
||||
|
||||
class PaymentService {
|
||||
late Razorpay _razorpay;
|
||||
|
||||
// Razorpay test key — matches web app
|
||||
static const String _testKey = 'rzp_test_S49PVZmqAVoWSH';
|
||||
|
||||
PaymentSuccessCallback? onSuccess;
|
||||
PaymentErrorCallback? onError;
|
||||
ExternalWalletCallback? onExternalWallet;
|
||||
|
||||
void initialize({
|
||||
required PaymentSuccessCallback onSuccess,
|
||||
required PaymentErrorCallback onError,
|
||||
ExternalWalletCallback? onExternalWallet,
|
||||
}) {
|
||||
_razorpay = Razorpay();
|
||||
this.onSuccess = onSuccess;
|
||||
this.onError = onError;
|
||||
this.onExternalWallet = onExternalWallet;
|
||||
|
||||
_razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handleSuccess);
|
||||
_razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handleError);
|
||||
_razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
|
||||
}
|
||||
|
||||
void openPayment({
|
||||
required double amount,
|
||||
required String email,
|
||||
required String phone,
|
||||
required String eventName,
|
||||
String? orderId,
|
||||
}) {
|
||||
final options = <String, dynamic>{
|
||||
'key': _testKey,
|
||||
'amount': (amount * 100).toInt(), // paise
|
||||
'currency': 'INR',
|
||||
'name': 'Eventify',
|
||||
'description': 'Ticket: $eventName',
|
||||
'prefill': {
|
||||
'email': email,
|
||||
'contact': phone,
|
||||
},
|
||||
'theme': {'color': '#0B63D6'},
|
||||
};
|
||||
if (orderId != null) options['order_id'] = orderId;
|
||||
|
||||
if (kDebugMode) debugPrint('PaymentService: opening Razorpay with amount=${amount * 100} paise');
|
||||
_razorpay.open(options);
|
||||
}
|
||||
|
||||
void _handleSuccess(PaymentSuccessResponse res) => onSuccess?.call(res);
|
||||
void _handleError(PaymentFailureResponse res) => onError?.call(res);
|
||||
void _handleExternalWallet(ExternalWalletResponse res) => onExternalWallet?.call(res);
|
||||
|
||||
void dispose() {
|
||||
_razorpay.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user