marco.pms.mobileapp/lib/controller/payment/payment_controller.dart

285 lines
8.6 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:razorpay_flutter/razorpay_flutter.dart';
import 'package:marco/helpers/services/payment_service.dart';
import 'package:marco/helpers/services/tenant_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:get/get.dart';
class PaymentController with ChangeNotifier {
Razorpay? _razorpay;
final PaymentService _paymentService = PaymentService();
final TenantService _tenantService = TenantService();
bool isProcessing = false;
// Pending values to use after payment verification
String? _pendingTenantEnquireId;
String? _pendingPlanId;
/// ==============================
/// START PAYMENT (Safe init)
/// ==============================
Future<void> startPayment({
required double amount,
required String description,
required BuildContext context,
String? tenantEnquireId,
String? planId,
}) async {
try {
isProcessing = true;
notifyListeners();
// Save pending ids for post-payment subscription call
_pendingTenantEnquireId = tenantEnquireId;
_pendingPlanId = planId;
// Step 1: Create payment order
final result = await _paymentService.createOrder(amount);
logSafe("🧩 Raw response in PaymentController: $result");
// Step 2: Validate result before accessing keys
if (result == null) {
_showError("Failed to create order. Server returned null response.");
isProcessing = false;
notifyListeners();
return;
}
// Step 3: Handle both wrapped and unwrapped formats
final data = result['data'] ?? result;
final orderId = data?['orderId'];
final key = data?['key'];
if (orderId == null || key == null) {
_showError("Invalid response from server. Missing orderId or key.");
logSafe("💥 Invalid response structure: $result");
isProcessing = false;
notifyListeners();
return;
}
// Step 4: Initialize Razorpay if needed
_razorpay ??= Razorpay();
_razorpay!.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
_razorpay!.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
_razorpay!.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
// Step 5: Open Razorpay checkout
final options = {
'key': key,
'amount': (amount * 100).toInt(), // Razorpay expects amount in paise
'name': 'Marco',
'description': description,
'order_id': orderId,
'prefill': {'contact': '9999999999', 'email': 'test@marco.com'},
};
logSafe("🟠 Opening Razorpay checkout with options: $options");
_razorpay!.open(options);
} catch (e, s) {
_showError("Payment initialization failed. Please try again.");
logSafe("💥 Exception in startPayment: $e\n$s");
} finally {
isProcessing = false;
notifyListeners();
}
}
/// ==============================
/// EVENT HANDLERS
/// ==============================
void _handlePaymentSuccess(PaymentSuccessResponse response) async {
logSafe("✅ Payment Success: ${response.paymentId}");
isProcessing = true;
notifyListeners();
Map<String, dynamic>? verificationResult;
try {
logSafe("🟢 Verifying payment via backend...");
verificationResult = await _paymentService
.verifyPayment(
paymentId: response.paymentId!,
orderId: response.orderId!,
signature: response.signature!,
)
.timeout(const Duration(seconds: 15));
logSafe("🧩 Verification result: $verificationResult");
} catch (e) {
logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error);
}
isProcessing = false;
notifyListeners();
// Handle backend verification response properly
if (verificationResult != null) {
final isVerified = verificationResult['verified'] == true;
final msg = verificationResult['message'] ?? '';
if (isVerified) {
// If we have pending tenant and plan IDs, call subscription API
await _maybeSubscribeTenantAfterPayment(
verificationResult: verificationResult,
razorpayResponse: response,
);
_showDialog(
title: "Payment Successful 🎉",
message: msg.isNotEmpty ? msg : "Your payment was verified successfully.",
success: true,
);
} else {
_showDialog(
title: "Verification Failed ❌",
message: msg.isNotEmpty ? msg : "Payment completed but verification failed.",
success: false,
);
}
} else {
_showDialog(
title: "Verification Failed ❌",
message: "Payment completed but backend verification returned null.",
success: false,
);
}
_cleanup();
}
Future<void> _maybeSubscribeTenantAfterPayment({
required Map<String, dynamic> verificationResult,
required PaymentSuccessResponse razorpayResponse,
}) async {
if (_pendingTenantEnquireId == null || _pendingPlanId == null) {
logSafe(" No pending tenant/plan id to subscribe.");
return;
}
// Determine paymentDetailId — prefer backend value if provided
final paymentDetailId = verificationResult['paymentDetailId'] ?? verificationResult['paymentId'] ?? razorpayResponse.paymentId;
final subscribePayload = {
"tenantEnquireId": _pendingTenantEnquireId,
"paymentDetailId": paymentDetailId,
"planId": _pendingPlanId,
};
logSafe("🟢 Subscribing tenant automatically: $subscribePayload");
final subResp = await _tenantService.subscribeTenant(subscribePayload);
// Clear pending values immediately to avoid double-invoke
_pendingTenantEnquireId = null;
_pendingPlanId = null;
if (subResp == null) {
logSafe("❌ subscribeTenant returned null");
_showDialog(
title: "Subscription Failed",
message: "Failed to call subscription API. Please contact support.",
success: false,
);
return;
}
final data = subResp['data'] ?? subResp;
// backend success flag might be in different places; check robustly
final bool success = (data is Map && (data['success'] == true || subResp['success'] == true)) || (subResp['statusCode'] == 200);
if (success) {
_showDialog(
title: "Subscription Active ✅",
message: data['message'] ?? "Tenant subscribed successfully.",
success: true,
);
} else {
_showDialog(
title: "Subscription Failed",
message: data['message'] ?? "Subscription API did not confirm success.",
success: false,
);
}
}
void _handlePaymentError(PaymentFailureResponse response) {
logSafe("❌ Payment Failed: ${response.message}");
isProcessing = false;
notifyListeners();
_showDialog(
title: "Payment Failed ❌",
message: "Reason: ${response.message ?? 'Unknown error'}",
success: false,
);
_cleanup();
}
void _handleExternalWallet(ExternalWalletResponse response) {
logSafe(" External Wallet Used: ${response.walletName}");
}
/// ==============================
/// CLEANUP / DISPOSE
/// ==============================
void _cleanup() {
try {
_razorpay?.clear();
} catch (_) {}
_razorpay = null;
logSafe("🧹 Razorpay instance cleaned up.");
}
@override
void dispose() {
_cleanup();
super.dispose();
}
void disposeController() => _cleanup();
/// ==============================
/// HELPER UI FUNCTIONS
/// ==============================
void _showDialog({
required String title,
required String message,
required bool success,
}) {
if (Get.isDialogOpen == true) Get.back(); // Close any existing dialogs
Get.defaultDialog(
title: title,
middleText: message,
confirm: ElevatedButton(
onPressed: () {
Get.back(); // close dialog
Get.snackbar(
success ? "Payment Successful 🎉" : "Payment Failed ❌",
success
? "Payment verified successfully!"
: "Payment failed or could not be verified.",
backgroundColor: success ? Colors.green : Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
},
child: const Text("OK"),
),
);
}
void _showError(String message) {
if (Get.isDialogOpen == true) Get.back(); // Close any open dialog
Get.snackbar(
"Payment Error",
message,
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
}