From 7e427237c3395c18e38493d84a299df564c4aec7 Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 31 Oct 2025 16:57:38 +0530 Subject: [PATCH] implement create tender screen --- .../payment/payment_controller.dart | 93 ++++++- .../subscription_controller.dart | 2 +- lib/helpers/services/api_endpoints.dart | 5 + lib/helpers/services/api_service.dart | 87 +++++- lib/helpers/services/tenant_service.dart | 43 ++- lib/routes.dart | 7 + lib/view/payment/payment_screen.dart | 15 +- .../subscriptions/subscriptions_screen.dart | 10 +- lib/view/tenant/tenant_create_screen.dart | 263 ++++++++++++++++++ 9 files changed, 491 insertions(+), 34 deletions(-) create mode 100644 lib/view/tenant/tenant_create_screen.dart diff --git a/lib/controller/payment/payment_controller.dart b/lib/controller/payment/payment_controller.dart index 3dc2698..5c646e1 100644 --- a/lib/controller/payment/payment_controller.dart +++ b/lib/controller/payment/payment_controller.dart @@ -1,15 +1,20 @@ 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; - //BuildContext? _context; + + // Pending values to use after payment verification + String? _pendingTenantEnquireId; + String? _pendingPlanId; /// ============================== /// START PAYMENT (Safe init) @@ -18,11 +23,17 @@ class PaymentController with ChangeNotifier { 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"); @@ -35,7 +46,7 @@ class PaymentController with ChangeNotifier { return; } - // Step 2: Handle both wrapped and unwrapped formats + // Step 3: Handle both wrapped and unwrapped formats final data = result['data'] ?? result; final orderId = data?['orderId']; final key = data?['key']; @@ -48,18 +59,18 @@ class PaymentController with ChangeNotifier { return; } - // Step 3: Initialize Razorpay if needed + // 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 4: Open Razorpay checkout + // Step 5: Open Razorpay checkout final options = { 'key': key, 'amount': (amount * 100).toInt(), // Razorpay expects amount in paise 'name': 'Marco', - 'description': 'Subscription Payment', + 'description': description, 'order_id': orderId, 'prefill': {'contact': '9999999999', 'email': 'test@marco.com'}, }; @@ -94,6 +105,7 @@ class PaymentController with ChangeNotifier { signature: response.signature!, ) .timeout(const Duration(seconds: 15)); + logSafe("🧩 Verification result: $verificationResult"); } catch (e) { logSafe("âąī¸ Verification timeout/error: $e", level: LogLevel.error); } @@ -101,25 +113,27 @@ class PaymentController with ChangeNotifier { isProcessing = false; notifyListeners(); - // ✅ Handle backend verification response properly + // Handle backend verification response properly if (verificationResult != null) { - // Example backend response: { "verified": true, "message": "Payment verified" } 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.", + 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.", + message: msg.isNotEmpty ? msg : "Payment completed but verification failed.", success: false, ); } @@ -134,6 +148,61 @@ class PaymentController with ChangeNotifier { _cleanup(); } + Future _maybeSubscribeTenantAfterPayment({ + required Map 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; diff --git a/lib/controller/subscriptions/subscription_controller.dart b/lib/controller/subscriptions/subscription_controller.dart index 9a65f61..bb7f28a 100644 --- a/lib/controller/subscriptions/subscription_controller.dart +++ b/lib/controller/subscriptions/subscription_controller.dart @@ -6,7 +6,7 @@ class SubscriptionController extends GetxController { var isLoading = true.obs; // Frequency tabs - final frequencies = ['monthly', 'quarterly', 'halfyearly', 'yearly']; + final frequencies = ['monthly', 'quarterly', 'half-yearly', 'yearly']; var selectedFrequency = 'monthly'.obs; @override diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index d9e5372..4b5eb54 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -104,4 +104,9 @@ class ApiEndpoints { // Payment Module API Endpoints static const String createOrder = "/payment/create-order"; static const String verifyPayment = "/payment/verify-payment"; + + // Tenant endpoints + static const String createTenantSelf = '/Tenant/self/create'; + static const String tenantSubscribe = '/Tenant/self/subscription'; + static const String tenantRenewSubscription = '/Tenant/renew/subscription'; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 9109d9f..91feee9 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -283,16 +283,20 @@ class ApiService { Map? additionalHeaders, Duration customTimeout = extendedTimeout, bool hasRetried = false, + bool requireAuth = true, // ✅ added }) async { - String? token = await _getToken(); - if (token == null) return null; + String? token; + + if (requireAuth) { + token = await _getToken(); + if (token == null) return null; + } final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); - logSafe( - "PUT $uri\nHeaders: ${_headers(token)}\nBody: $body", - ); + final headers = { - ..._headers(token), + 'Content-Type': 'application/json', + if (requireAuth && token != null) ..._headers(token), if (additionalHeaders != null) ...additionalHeaders, }; @@ -305,19 +309,23 @@ class ApiService { .put(uri, headers: headers, body: jsonEncode(body)) .timeout(customTimeout); - if (response.statusCode == 401 && !hasRetried) { - logSafe("Unauthorized PUT. Attempting token refresh..."); + if (response.statusCode == 401 && requireAuth && !hasRetried) { + logSafe("âš ī¸ Unauthorized PUT. Attempting token refresh..."); if (await AuthService.refreshToken()) { - return await _putRequest(endpoint, body, - additionalHeaders: additionalHeaders, - customTimeout: customTimeout, - hasRetried: true); + return await _putRequest( + endpoint, + body, + additionalHeaders: additionalHeaders, + customTimeout: customTimeout, + hasRetried: true, + requireAuth: requireAuth, + ); } } return response; } catch (e) { - logSafe("HTTP PUT Exception: $e", level: LogLevel.error); + logSafe("❌ HTTP PUT Exception: $e", level: LogLevel.error); return null; } } @@ -2247,6 +2255,59 @@ class ApiService { } } + /// Create tenant (self) + static Future?> createTenantSelf( + Map payload) async { + try { + final response = await _postRequest( + ApiEndpoints.createTenantSelf, + payload, + requireAuth: false, // likely public + ); + if (response == null) return null; + return _parseResponse(response, label: "Create Tenant Self"); + } catch (e) { + logSafe("❌ Exception in createTenantSelf: $e", level: LogLevel.error); + return null; + } + } + + /// Subscribe tenant (after successful payment) + static Future?> subscribeTenant( + Map payload) async { + try { + final response = await _postRequest( + ApiEndpoints.tenantSubscribe, + payload, + requireAuth: + true, // likely needs auth if user logged in; set according to backend + ); + if (response == null) return null; + return _parseResponse(response, label: "Tenant Subscribe"); + } catch (e) { + logSafe("❌ Exception in subscribeTenant: $e", level: LogLevel.error); + return null; + } + } + + /// Renew tenant subscription (PUT) + static Future?> renewTenantSubscription( + Map payload) async { + try { + final response = await _putRequest( + ApiEndpoints.tenantRenewSubscription, + payload, + requireAuth: true, + ); + if (response == null) return null; + return _parseResponse(response, label: "Renew Tenant Subscription"); + } catch (e) { + logSafe("❌ Exception in renewTenantSubscription: $e", + level: LogLevel.error); + return null; + } + } + // === Employee APIs === /// Search employees by first name and last name only (not middle name) /// Returns a list of up to 10 employee records matching the search string. diff --git a/lib/helpers/services/tenant_service.dart b/lib/helpers/services/tenant_service.dart index 3d9be30..5e2adfa 100644 --- a/lib/helpers/services/tenant_service.dart +++ b/lib/helpers/services/tenant_service.dart @@ -7,6 +7,7 @@ import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/auth_service.dart'; +import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/tenant/tenant_list_model.dart'; /// Abstract interface for tenant service functionality @@ -130,7 +131,7 @@ class TenantService implements ITenantService { } // 🔹 Register FCM token after tenant selection - final fcmToken = LocalStorage.getFcmToken(); + final fcmToken = LocalStorage.getFcmToken(); if (fcmToken?.isNotEmpty ?? false) { final success = await AuthService.registerDeviceToken(fcmToken!); logSafe( @@ -160,4 +161,44 @@ class TenantService implements ITenantService { return false; } } + + Future?> createTenant( + Map payload) async { + try { + logSafe("đŸŸĸ Creating tenant: $payload"); + final resp = await ApiService.createTenantSelf(payload); + logSafe("🧩 createTenant response: $resp"); + return resp; + } catch (e, s) { + logSafe("❌ Exception in createTenant: $e\n$s", level: LogLevel.error); + return null; + } + } + + Future?> subscribeTenant( + Map payload) async { + try { + logSafe("đŸŸĸ Subscribing tenant: $payload"); + final resp = await ApiService.subscribeTenant(payload); + logSafe("🧩 subscribeTenant response: $resp"); + return resp; + } catch (e, s) { + logSafe("❌ Exception in subscribeTenant: $e\n$s", level: LogLevel.error); + return null; + } + } + + Future?> renewSubscription( + Map payload) async { + try { + logSafe("đŸŸĸ Renewing subscription: $payload"); + final resp = await ApiService.renewTenantSubscription(payload); + logSafe("🧩 renewSubscription response: $resp"); + return resp; + } catch (e, s) { + logSafe("❌ Exception in renewSubscription: $e\n$s", + level: LogLevel.error); + return null; + } + } } diff --git a/lib/routes.dart b/lib/routes.dart index 3b23d46..bc1c3b8 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -23,6 +23,7 @@ import 'package:marco/view/document/user_document_screen.dart'; import 'package:marco/view/tenant/tenant_selection_screen.dart'; import 'package:marco/view/payment/payment_screen.dart'; import 'package:marco/view/subscriptions/subscriptions_screen.dart'; +import 'package:marco/view/tenant/tenant_create_screen.dart'; class AuthMiddleware extends GetMiddleware { @override @@ -34,6 +35,7 @@ class AuthMiddleware extends GetMiddleware { '/subscription', '/payment', '/select-tenant', + '/create-tenant', ]; // ✅ Allow any route that starts with these public routes @@ -110,6 +112,11 @@ getPageRoute() { middlewares: [AuthMiddleware()]), // Payment GetPage(name: '/payment', page: () => PaymentScreen()), + GetPage( + name: '/create-tenant', + page: () => const TenantCreateScreen(), + ), + GetPage( name: '/subscription', page: () => SubscriptionScreen(), diff --git a/lib/view/payment/payment_screen.dart b/lib/view/payment/payment_screen.dart index f2253e9..0d19a70 100644 --- a/lib/view/payment/payment_screen.dart +++ b/lib/view/payment/payment_screen.dart @@ -10,7 +10,7 @@ class PaymentScreen extends StatefulWidget { const PaymentScreen({ super.key, this.amount = 0.0, - this.description = "No description", + this.description = "No description", }); @override @@ -66,7 +66,10 @@ class _PaymentScreenState extends State { final controller = _controller!; return Scaffold( appBar: AppBar( - title: const Text("Payment", style: TextStyle(color: Colors.black),), + title: const Text( + "Payment", + style: TextStyle(color: Colors.black), + ), backgroundColor: Colors.white, ), body: Padding( @@ -90,10 +93,18 @@ class _PaymentScreenState extends State { Center( child: ElevatedButton( onPressed: () async { + // Extract optional IDs passed via Get.arguments + final String tenantEnquireId = + (args['tenantEnquireId'] ?? '') as String; + final String planId = (args['planId'] ?? '') as String; + await controller.startPayment( amount: finalAmount, description: finalDescription, context: context, + tenantEnquireId: + tenantEnquireId.isNotEmpty ? tenantEnquireId : null, + planId: planId.isNotEmpty ? planId : null, ); }, style: ElevatedButton.styleFrom( diff --git a/lib/view/subscriptions/subscriptions_screen.dart b/lib/view/subscriptions/subscriptions_screen.dart index c11a8fc..bb71a7f 100644 --- a/lib/view/subscriptions/subscriptions_screen.dart +++ b/lib/view/subscriptions/subscriptions_screen.dart @@ -16,7 +16,7 @@ class SubscriptionScreen extends StatelessWidget { title: const Text( 'Subscription Plans', style: TextStyle( - color: Colors.black87, + color: Colors.black87, ), ), leading: IconButton( @@ -106,11 +106,11 @@ class SubscriptionScreen extends StatelessWidget { width: double.infinity, child: ElevatedButton( onPressed: () { - Get.toNamed('/payment', arguments: { - 'amount': plan['price'] ?? 0, - 'description': - plan['planName'] ?? 'Subscription', + Get.toNamed('/create-tenant', arguments: { 'planId': plan['id'] ?? '', + 'amount': plan['price'] ?? 0, + 'planName': + plan['planName'] ?? 'Subscription', }); }, child: MyText.bodyMedium( diff --git a/lib/view/tenant/tenant_create_screen.dart b/lib/view/tenant/tenant_create_screen.dart new file mode 100644 index 0000000..001a189 --- /dev/null +++ b/lib/view/tenant/tenant_create_screen.dart @@ -0,0 +1,263 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/services/tenant_service.dart'; +import 'package:marco/helpers/services/app_logger.dart'; + +class TenantCreateScreen extends StatefulWidget { + const TenantCreateScreen({super.key}); + + @override + State createState() => _TenantCreateScreenState(); +} + +class _TenantCreateScreenState extends State { + final _formKey = GlobalKey(); + final _firstNameCtrl = TextEditingController(); + final _lastNameCtrl = TextEditingController(); + final _orgNameCtrl = TextEditingController(); + final _emailCtrl = TextEditingController(); + final _contactCtrl = TextEditingController(); + final _billingCtrl = TextEditingController(); + final _orgSizeCtrl = TextEditingController(); + final _industryIdCtrl = TextEditingController(); + final _referenceCtrl = TextEditingController(); + + final TenantService _tenantService = TenantService(); + bool _loading = false; + + // Values from subscription screen + late final String planId; + late final double amount; + late final String planName; + + @override + void initState() { + super.initState(); + final args = (Get.arguments ?? {}) as Map; + planId = args['planId'] ?? ''; + amount = (args['amount'] ?? 0).toDouble(); + planName = args['planName'] ?? 'Subscription'; + } + + @override + void dispose() { + _firstNameCtrl.dispose(); + _lastNameCtrl.dispose(); + _orgNameCtrl.dispose(); + _emailCtrl.dispose(); + _contactCtrl.dispose(); + _billingCtrl.dispose(); + _orgSizeCtrl.dispose(); + _industryIdCtrl.dispose(); + _referenceCtrl.dispose(); + super.dispose(); + } + + Future _submit() async { + if (!_formKey.currentState!.validate()) return; + + final payload = { + "firstName": _firstNameCtrl.text.trim(), + "lastName": _lastNameCtrl.text.trim(), + "organizationName": _orgNameCtrl.text.trim(), + "email": _emailCtrl.text.trim(), + "contactNumber": _contactCtrl.text.trim(), + "billingAddress": _billingCtrl.text.trim(), + "organizationSize": _orgSizeCtrl.text.trim(), + "industryId": _industryIdCtrl.text.trim(), + "reference": _referenceCtrl.text.trim(), + }; + + setState(() => _loading = true); + final resp = await _tenantService.createTenant(payload); + setState(() => _loading = false); + + if (resp == null) { + Get.snackbar("Error", "Failed to create tenant. Try again later.", + backgroundColor: Colors.red, colorText: Colors.white); + return; + } + + final data = resp['data'] ?? resp; + final tenantEnquireId = + data['tenantEnquireId'] ?? data['id'] ?? data['tenantId']; + + if (tenantEnquireId == null) { + logSafe("❌ Create tenant response missing id: $resp"); + Get.snackbar("Error", "Tenant created but server didn't return id.", + backgroundColor: Colors.red, colorText: Colors.white); + return; + } + + // ✅ Go to payment screen with all details + Get.toNamed('/payment', arguments: { + 'amount': amount, + 'description': 'Subscription for ${_orgNameCtrl.text}', + 'tenantEnquireId': tenantEnquireId, + 'planId': planId, + }); + } + + @override + Widget build(BuildContext context) { + final divider = Divider(color: Colors.grey.shade300, height: 32); + + return Scaffold( + appBar: AppBar( + title: const Text("Create Tenant"), + centerTitle: true, + elevation: 0, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Personal Info", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + TextFormField( + controller: _firstNameCtrl, + decoration: const InputDecoration( + labelText: "First Name *", + hintText: "e.g., John", + prefixIcon: Icon(Icons.person_outline), + border: OutlineInputBorder(), + ), + validator: (v) => v == null || v.isEmpty ? "Required" : null, + ), + const SizedBox(height: 12), + TextFormField( + controller: _lastNameCtrl, + decoration: const InputDecoration( + labelText: "Last Name *", + hintText: "e.g., Doe", + prefixIcon: Icon(Icons.person_outline), + border: OutlineInputBorder(), + ), + validator: (v) => v == null || v.isEmpty ? "Required" : null, + ), + divider, + const Text( + "Organization Details", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + TextFormField( + controller: _orgNameCtrl, + decoration: const InputDecoration( + labelText: "Organization Name *", + hintText: "e.g., MarcoBMS", + border: OutlineInputBorder(), + ), + validator: (v) => v == null || v.isEmpty ? "Required" : null, + ), + const SizedBox(height: 12), + TextFormField( + controller: _billingCtrl, + decoration: const InputDecoration( + labelText: "Billing Address", + hintText: "e.g., 123 Main Street, Mumbai", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _orgSizeCtrl, + decoration: const InputDecoration( + labelText: "Organization Size", + hintText: "e.g., 1-10, 11-50", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _industryIdCtrl, + decoration: const InputDecoration( + labelText: "Industry ID (UUID)", + hintText: "e.g., 3fa85f64-5717-4562-b3fc-2c963f66afa6", + border: OutlineInputBorder(), + ), + ), + divider, + const Text( + "Contact Info", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + TextFormField( + controller: _emailCtrl, + decoration: const InputDecoration( + labelText: "Email", + hintText: "e.g., john.doe@example.com", + prefixIcon: Icon(Icons.email_outlined), + border: OutlineInputBorder(), + ), + validator: (v) => + v == null || !v.contains('@') ? "Invalid email" : null, + ), + const SizedBox(height: 12), + TextFormField( + controller: _contactCtrl, + decoration: const InputDecoration( + labelText: "Contact Number", + hintText: "e.g., +91 9876543210", + prefixIcon: Icon(Icons.phone_outlined), + border: OutlineInputBorder(), + ), + ), + divider, + const Text( + "Other Details", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + TextFormField( + controller: _referenceCtrl, + decoration: const InputDecoration( + labelText: "Reference", + hintText: "e.g., Referral Name or Code", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 24), + Center( + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: _loading ? null : _submit, + child: _loading + ? const SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + "Create Tenant & Continue", + style: TextStyle(fontSize: 16), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +}