diff --git a/backend/igny8_core/business/billing/billing_views.py b/backend/igny8_core/business/billing/billing_views.py index b798bef2..e2cd4505 100644 --- a/backend/igny8_core/business/billing/billing_views.py +++ b/backend/igny8_core/business/billing/billing_views.py @@ -606,7 +606,7 @@ class BillingViewSet(viewsets.GenericViewSet): class InvoiceViewSet(AccountModelViewSet): """ViewSet for user-facing invoices""" - queryset = Invoice.objects.all().select_related('account') + queryset = Invoice.objects.all().select_related('account', 'subscription', 'subscription__plan') permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] pagination_class = CustomPageNumberPagination @@ -617,6 +617,43 @@ class InvoiceViewSet(AccountModelViewSet): queryset = queryset.filter(account=self.request.account) return queryset.order_by('-invoice_date', '-created_at') + def _serialize_invoice(self, invoice): + """Serialize an invoice with all needed fields""" + # Build subscription data if exists + subscription_data = None + if invoice.subscription: + plan_data = None + if invoice.subscription.plan: + plan_data = { + 'id': invoice.subscription.plan.id, + 'name': invoice.subscription.plan.name, + 'slug': invoice.subscription.plan.slug, + } + subscription_data = { + 'id': invoice.subscription.id, + 'plan': plan_data, + } + + return { + 'id': invoice.id, + 'invoice_number': invoice.invoice_number, + 'status': invoice.status, + 'total': str(invoice.total), # Alias for compatibility + 'total_amount': str(invoice.total), + 'subtotal': str(invoice.subtotal), + 'tax_amount': str(invoice.tax), + 'currency': invoice.currency, + 'invoice_date': invoice.invoice_date.isoformat(), + 'due_date': invoice.due_date.isoformat(), + 'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None, + 'line_items': invoice.line_items, + 'billing_email': invoice.billing_email, + 'notes': invoice.notes, + 'payment_method': invoice.payment_method, + 'subscription': subscription_data, + 'created_at': invoice.created_at.isoformat(), + } + def list(self, request): """List invoices for current account""" queryset = self.get_queryset() @@ -630,25 +667,7 @@ class InvoiceViewSet(AccountModelViewSet): page = paginator.paginate_queryset(queryset, request) # Serialize invoice data - results = [] - for invoice in (page if page is not None else []): - results.append({ - 'id': invoice.id, - 'invoice_number': invoice.invoice_number, - 'status': invoice.status, - 'total': str(invoice.total), # Alias for compatibility - 'total_amount': str(invoice.total), - 'subtotal': str(invoice.subtotal), - 'tax_amount': str(invoice.tax), - 'currency': invoice.currency, - 'invoice_date': invoice.invoice_date.isoformat(), - 'due_date': invoice.due_date.isoformat(), - 'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None, - 'line_items': invoice.line_items, - 'billing_email': invoice.billing_email, - 'notes': invoice.notes, - 'created_at': invoice.created_at.isoformat(), - }) + results = [self._serialize_invoice(invoice) for invoice in (page if page is not None else [])] return paginated_response( {'count': paginator.page.paginator.count, 'next': paginator.get_next_link(), 'previous': paginator.get_previous_link(), 'results': results}, @@ -659,24 +678,7 @@ class InvoiceViewSet(AccountModelViewSet): """Get invoice detail""" try: invoice = self.get_queryset().get(pk=pk) - data = { - 'id': invoice.id, - 'invoice_number': invoice.invoice_number, - 'status': invoice.status, - 'total': str(invoice.total), # Alias for compatibility - 'total_amount': str(invoice.total), - 'subtotal': str(invoice.subtotal), - 'tax_amount': str(invoice.tax), - 'currency': invoice.currency, - 'invoice_date': invoice.invoice_date.isoformat(), - 'due_date': invoice.due_date.isoformat(), - 'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None, - 'line_items': invoice.line_items, - 'billing_email': invoice.billing_email, - 'notes': invoice.notes, - 'created_at': invoice.created_at.isoformat(), - } - return success_response(data=data, request=request) + return success_response(data=self._serialize_invoice(invoice), request=request) except Invoice.DoesNotExist: return error_response(error='Invoice not found', status_code=404, request=request) diff --git a/backend/igny8_core/business/billing/services/invoice_service.py b/backend/igny8_core/business/billing/services/invoice_service.py index 79f77db0..979a64d0 100644 --- a/backend/igny8_core/business/billing/services/invoice_service.py +++ b/backend/igny8_core/business/billing/services/invoice_service.py @@ -274,10 +274,21 @@ class InvoiceService: transaction_id: Optional[str] = None ) -> Invoice: """ - Mark invoice as paid + Mark invoice as paid and record payment details + + Args: + invoice: Invoice to mark as paid + payment_method: Payment method used ('stripe', 'paypal', 'bank_transfer', etc.) + transaction_id: External transaction ID (Stripe payment intent, PayPal capture ID, etc.) """ invoice.status = 'paid' invoice.paid_at = timezone.now() + invoice.payment_method = payment_method + + # For Stripe payments, store the transaction ID in stripe_invoice_id field + if payment_method == 'stripe' and transaction_id: + invoice.stripe_invoice_id = transaction_id + invoice.save() return invoice diff --git a/backend/igny8_core/business/billing/services/payment_service.py b/backend/igny8_core/business/billing/services/payment_service.py index 1ef9841a..52463b06 100644 --- a/backend/igny8_core/business/billing/services/payment_service.py +++ b/backend/igny8_core/business/billing/services/payment_service.py @@ -105,11 +105,15 @@ class PaymentService: ) -> Payment: """ Mark payment as completed and update invoice + For automatic payments (Stripe/PayPal), sets approved_at but leaves approved_by as None """ from .invoice_service import InvoiceService payment.status = 'succeeded' payment.processed_at = timezone.now() + # For automatic payments, set approved_at to indicate when payment was verified + # approved_by stays None to indicate it was automated, not manual approval + payment.approved_at = timezone.now() if transaction_id: payment.transaction_reference = transaction_id diff --git a/backend/igny8_core/business/billing/tasks/subscription_renewal.py b/backend/igny8_core/business/billing/tasks/subscription_renewal.py index 56c66db8..618295ff 100644 --- a/backend/igny8_core/business/billing/tasks/subscription_renewal.py +++ b/backend/igny8_core/business/billing/tasks/subscription_renewal.py @@ -172,7 +172,7 @@ def _attempt_stripe_renewal(subscription: Subscription, invoice: Invoice) -> boo payment_method='stripe', status='processing', stripe_payment_intent_id=intent.id, - metadata={'renewal': True} + metadata={'renewal': True, 'auto_approved': True} ) return True @@ -210,7 +210,7 @@ def _attempt_paypal_renewal(subscription: Subscription, invoice: Invoice) -> boo payment_method='paypal', status='processing', paypal_order_id=subscription.metadata['paypal_subscription_id'], - metadata={'renewal': True} + metadata={'renewal': True, 'auto_approved': True} ) return True else: diff --git a/backend/igny8_core/business/billing/views/paypal_views.py b/backend/igny8_core/business/billing/views/paypal_views.py index 616ead61..9d9e17b0 100644 --- a/backend/igny8_core/business/billing/views/paypal_views.py +++ b/backend/igny8_core/business/billing/views/paypal_views.py @@ -183,9 +183,14 @@ class PayPalCreateSubscriptionOrderView(APIView): request=request ) - # Get plan + # Get plan - support both ID (integer) and slug (string) lookup try: - plan = Plan.objects.get(id=plan_id, is_active=True) + # Try integer ID first + try: + plan = Plan.objects.get(id=int(plan_id), is_active=True) + except (ValueError, Plan.DoesNotExist): + # Fall back to slug lookup + plan = Plan.objects.get(slug=plan_id, is_active=True) except Plan.DoesNotExist: return error_response( error='Plan not found', @@ -560,6 +565,7 @@ def _process_credit_purchase(account, package_id: str, capture_result: dict) -> ) # Create payment record + # For automatic payments, approved_at is set but approved_by is None (automated) amount = float(capture_result.get('amount', package.price)) currency = capture_result.get('currency', 'USD') @@ -573,9 +579,11 @@ def _process_credit_purchase(account, package_id: str, capture_result: dict) -> paypal_order_id=capture_result.get('order_id'), paypal_capture_id=capture_result.get('capture_id'), processed_at=timezone.now(), + approved_at=timezone.now(), # Set approved_at for automatic payments metadata={ 'credit_package_id': str(package_id), 'credits_added': package.credits, + 'auto_approved': True, # Indicates automated approval } ) @@ -642,6 +650,7 @@ def _process_subscription_payment(account, plan_id: str, capture_result: dict) - ) # Create payment record + # For automatic payments, approved_at is set but approved_by is None (automated) amount = float(capture_result.get('amount', plan.price)) currency = capture_result.get('currency', 'USD') @@ -655,12 +664,36 @@ def _process_subscription_payment(account, plan_id: str, capture_result: dict) - paypal_order_id=capture_result.get('order_id'), paypal_capture_id=capture_result.get('capture_id'), processed_at=timezone.now(), + approved_at=timezone.now(), # Set approved_at for automatic payments metadata={ 'plan_id': str(plan_id), 'subscription_type': 'paypal_order', + 'auto_approved': True, # Indicates automated approval } ) + # Update/create AccountPaymentMethod and mark as verified + from ..models import AccountPaymentMethod + # Get country code from account billing info + country_code = account.billing_country if account.billing_country else '' + AccountPaymentMethod.objects.update_or_create( + account=account, + type='paypal', + defaults={ + 'display_name': 'PayPal', + 'is_default': True, + 'is_enabled': True, + 'is_verified': True, # Mark verified after successful payment + 'country_code': country_code, # Set country from account billing info + 'metadata': { + 'last_payment_at': timezone.now().isoformat(), + 'paypal_order_id': capture_result.get('order_id'), + } + } + ) + # Set other payment methods as non-default + AccountPaymentMethod.objects.filter(account=account).exclude(type='paypal').update(is_default=False) + # Add subscription credits if plan.included_credits and plan.included_credits > 0: CreditService.add_credits( @@ -674,10 +707,15 @@ def _process_subscription_payment(account, plan_id: str, capture_result: dict) - } ) - # Update account status + # Update account status AND plan (like Stripe flow) + update_fields = ['updated_at'] if account.status != 'active': account.status = 'active' - account.save(update_fields=['status', 'updated_at']) + update_fields.append('status') + if account.plan_id != plan.id: + account.plan = plan + update_fields.append('plan') + account.save(update_fields=update_fields) logger.info( f"PayPal subscription payment completed for account {account.id}: " @@ -706,6 +744,10 @@ def _process_generic_payment(account, capture_result: dict) -> dict: paypal_order_id=capture_result.get('order_id'), paypal_capture_id=capture_result.get('capture_id'), processed_at=timezone.now(), + approved_at=timezone.now(), # Set approved_at for automatic payments + metadata={ + 'auto_approved': True, # Indicates automated approval + } ) logger.info(f"PayPal generic payment recorded for account {account.id}") diff --git a/backend/igny8_core/business/billing/views/stripe_views.py b/backend/igny8_core/business/billing/views/stripe_views.py index 638897a0..7c0d73e7 100644 --- a/backend/igny8_core/business/billing/views/stripe_views.py +++ b/backend/igny8_core/business/billing/views/stripe_views.py @@ -82,9 +82,14 @@ class StripeCheckoutView(APIView): request=request ) - # Get plan + # Get plan - support both ID (integer) and slug (string) lookup try: - plan = Plan.objects.get(id=plan_id, is_active=True) + # Try integer ID first + try: + plan = Plan.objects.get(id=int(plan_id), is_active=True) + except (ValueError, Plan.DoesNotExist): + # Fall back to slug lookup + plan = Plan.objects.get(slug=plan_id, is_active=True) except Plan.DoesNotExist: return error_response( error='Plan not found', @@ -488,7 +493,13 @@ def _activate_subscription(account, stripe_subscription_id: str, plan_id: str, s transaction_id=stripe_subscription_id ) - # Create payment record + # Extract payment details from session + # For subscription checkouts, payment_intent may be in session or we need to use subscription invoice + payment_intent_id = session.get('payment_intent') + checkout_session_id = session.get('id') + + # Create payment record with proper Stripe identifiers + # For automatic payments, approved_at is set but approved_by is None (automated) Payment.objects.create( account=account, invoice=invoice, @@ -496,15 +507,41 @@ def _activate_subscription(account, stripe_subscription_id: str, plan_id: str, s currency=currency, payment_method='stripe', status='succeeded', - stripe_payment_intent_id=session.get('payment_intent'), + stripe_payment_intent_id=payment_intent_id or f'sub_{stripe_subscription_id}', # Use subscription ID if no PI + stripe_charge_id=checkout_session_id, # Store checkout session as reference processed_at=timezone.now(), + approved_at=timezone.now(), # Set approved_at for automatic payments metadata={ - 'checkout_session_id': session.get('id'), + 'checkout_session_id': checkout_session_id, 'subscription_id': stripe_subscription_id, 'plan_id': str(plan_id), + 'payment_intent': payment_intent_id, + 'auto_approved': True, # Indicates automated approval } ) + # Update/create AccountPaymentMethod and mark as verified + from ..models import AccountPaymentMethod + # Get country code from account billing info + country_code = account.billing_country if account.billing_country else '' + AccountPaymentMethod.objects.update_or_create( + account=account, + type='stripe', + defaults={ + 'display_name': 'Credit/Debit Card (Stripe)', + 'is_default': True, + 'is_enabled': True, + 'is_verified': True, # Mark verified after successful payment + 'country_code': country_code, # Set country from account billing info + 'metadata': { + 'last_payment_at': timezone.now().isoformat(), + 'stripe_subscription_id': stripe_subscription_id, + } + } + ) + # Set other payment methods as non-default + AccountPaymentMethod.objects.filter(account=account).exclude(type='stripe').update(is_default=False) + # Add initial credits from plan if plan.included_credits and plan.included_credits > 0: CreditService.add_credits( @@ -571,6 +608,7 @@ def _add_purchased_credits(account, credit_package_id: str, credit_amount: str, ) # Create payment record + # For automatic payments, approved_at is set but approved_by is None (automated) amount = session.get('amount_total', 0) / 100 currency = session.get('currency', 'usd').upper() @@ -583,10 +621,12 @@ def _add_purchased_credits(account, credit_package_id: str, credit_amount: str, status='succeeded', stripe_payment_intent_id=session.get('payment_intent'), processed_at=timezone.now(), + approved_at=timezone.now(), # Set approved_at for automatic payments metadata={ 'checkout_session_id': session.get('id'), 'credit_package_id': str(credit_package_id), 'credits_added': credits_to_add, + 'auto_approved': True, # Indicates automated approval } ) diff --git a/frontend/src/components/billing/PayInvoiceModal.tsx b/frontend/src/components/billing/PayInvoiceModal.tsx new file mode 100644 index 00000000..d7ff0157 --- /dev/null +++ b/frontend/src/components/billing/PayInvoiceModal.tsx @@ -0,0 +1,543 @@ +/** + * Pay Invoice Modal + * Allows users to pay pending invoices using Stripe, PayPal, or submit bank transfer confirmation + * + * Payment Method Logic (Following SignUpFormUnified pattern): + * - Pakistan (PK): Bank Transfer + Credit Card (Stripe) - NO PayPal + * - Other countries: Credit Card (Stripe) + PayPal only + * - Default selection: User's configured default payment method from AccountPaymentMethod + */ + +import { useState, useEffect } from 'react'; +import { Modal } from '../ui/modal'; +import Button from '../ui/button/Button'; +import Label from '../form/Label'; +import Input from '../form/input/InputField'; +import TextArea from '../form/input/TextArea'; +import { + Loader2Icon, + UploadIcon, + XIcon, + CheckCircleIcon, + CreditCardIcon, + Building2Icon, + WalletIcon +} from '../../icons'; +import { API_BASE_URL } from '../../services/api'; +import { useAuthStore } from '../../store/authStore'; +import { subscribeToPlan } from '../../services/billing.api'; + +interface Invoice { + id: number; + invoice_number: string; + total?: string; + total_amount?: string; + currency?: string; + status?: string; + payment_method?: string; + subscription?: { + id?: number; + plan?: { + id: number; + name: string; + slug?: string; + }; + } | null; +} + +interface PayInvoiceModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess?: () => void; + invoice: Invoice; + /** User's billing country code (e.g., 'US', 'PK') */ + userCountry?: string; + /** User's default payment method type */ + defaultPaymentMethod?: string; +} + +type PaymentOption = 'stripe' | 'paypal' | 'bank_transfer'; + +export default function PayInvoiceModal({ + isOpen, + onClose, + onSuccess, + invoice, + userCountry = 'US', + defaultPaymentMethod, +}: PayInvoiceModalProps) { + const isPakistan = userCountry?.toUpperCase() === 'PK'; + + // Determine available payment options based on country + // PK users: Stripe (Card) + Bank Transfer - NO PayPal + // Other users: Stripe (Card) + PayPal only + const availableOptions: PaymentOption[] = isPakistan + ? ['stripe', 'bank_transfer'] + : ['stripe', 'paypal']; + + // Determine initial selection based on: + // 1. User's default payment method (if available for this country) + // 2. Invoice's stored payment_method + // 3. First available option (stripe) + const getInitialOption = (): PaymentOption => { + // Check user's default payment method first + if (defaultPaymentMethod) { + if (defaultPaymentMethod === 'stripe' || defaultPaymentMethod === 'card') return 'stripe'; + if (defaultPaymentMethod === 'bank_transfer' && isPakistan) return 'bank_transfer'; + if (defaultPaymentMethod === 'paypal' && !isPakistan) return 'paypal'; + } + // Then check invoice's stored payment method + if (invoice.payment_method) { + if (invoice.payment_method === 'stripe' || invoice.payment_method === 'card') return 'stripe'; + if (invoice.payment_method === 'bank_transfer' && isPakistan) return 'bank_transfer'; + if (invoice.payment_method === 'paypal' && !isPakistan) return 'paypal'; + } + // Fall back to first available (always stripe) + return 'stripe'; + }; + + const [selectedOption, setSelectedOption] = useState(getInitialOption()); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + + // Bank transfer form state + const [bankFormData, setBankFormData] = useState({ + manual_reference: '', + manual_notes: '', + proof_url: '', + }); + const [uploadedFileName, setUploadedFileName] = useState(''); + const [uploading, setUploading] = useState(false); + + const amount = parseFloat(invoice.total_amount || invoice.total || '0'); + const currency = invoice.currency?.toUpperCase() || 'USD'; + const planId = invoice.subscription?.plan?.id; + const planSlug = invoice.subscription?.plan?.slug; + + // Check if user's default method is selected (for showing badge) + const isDefaultMethod = (option: PaymentOption): boolean => { + if (!defaultPaymentMethod) return false; + if (option === 'stripe' && (defaultPaymentMethod === 'stripe' || defaultPaymentMethod === 'card')) return true; + if (option === 'bank_transfer' && defaultPaymentMethod === 'bank_transfer') return true; + if (option === 'paypal' && defaultPaymentMethod === 'paypal') return true; + return false; + }; + + // Reset state when modal opens + useEffect(() => { + if (isOpen) { + setSelectedOption(getInitialOption()); + setError(''); + setSuccess(false); + setBankFormData({ manual_reference: '', manual_notes: '', proof_url: '' }); + setUploadedFileName(''); + } + }, [isOpen]); + + const handleStripePayment = async () => { + // Use plan slug if available, otherwise fall back to id + const planIdentifier = planSlug || (planId ? String(planId) : null); + + if (!planIdentifier) { + setError('Unable to process card payment. Invoice has no associated plan. Please contact support.'); + return; + } + + try { + setLoading(true); + setError(''); + + // Use the subscribeToPlan function which properly handles Stripe checkout + const result = await subscribeToPlan(planIdentifier, 'stripe', { + return_url: `${window.location.origin}/account/plans?success=true`, + cancel_url: `${window.location.origin}/account/plans?canceled=true`, + }); + + // Redirect to Stripe Checkout + window.location.href = result.redirect_url; + } catch (err: any) { + setError(err.message || 'Failed to initiate card payment'); + setLoading(false); + } + }; + + const handlePayPalPayment = async () => { + // Use plan slug if available, otherwise fall back to id + const planIdentifier = planSlug || (planId ? String(planId) : null); + + if (!planIdentifier) { + setError('Unable to process PayPal payment. Invoice has no associated plan. Please contact support.'); + return; + } + + try { + setLoading(true); + setError(''); + + // Use the subscribeToPlan function which properly handles PayPal + const result = await subscribeToPlan(planIdentifier, 'paypal', { + return_url: `${window.location.origin}/account/plans?paypal=success&plan_id=${planIdentifier}`, + cancel_url: `${window.location.origin}/account/plans?paypal=cancel`, + }); + + // Redirect to PayPal + window.location.href = result.redirect_url; + } catch (err: any) { + setError(err.message || 'Failed to initiate PayPal payment'); + setLoading(false); + } + }; + + const handleFileSelect = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (file.size > 5 * 1024 * 1024) { + setError('File size must be less than 5MB'); + return; + } + + const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf']; + if (!allowedTypes.includes(file.type)) { + setError('Only JPEG, PNG, and PDF files are allowed'); + return; + } + + setUploadedFileName(file.name); + setError(''); + setUploading(true); + + try { + // Placeholder URL - actual S3 upload would go here + const placeholderUrl = `https://s3.amazonaws.com/igny8-payments/${Date.now()}-${file.name}`; + setBankFormData({ ...bankFormData, proof_url: placeholderUrl }); + } catch (err) { + setError('Failed to upload file'); + } finally { + setUploading(false); + } + }; + + const handleBankSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (!bankFormData.manual_reference.trim()) { + setError('Transaction reference is required'); + return; + } + + try { + setLoading(true); + + const token = useAuthStore.getState().token; + const response = await fetch(`${API_BASE_URL}/v1/billing/admin/payments/confirm/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + }, + credentials: 'include', + body: JSON.stringify({ + invoice_id: invoice.id, + payment_method: 'bank_transfer', + amount: invoice.total_amount || invoice.total || '0', + manual_reference: bankFormData.manual_reference.trim(), + manual_notes: bankFormData.manual_notes.trim() || undefined, + proof_url: bankFormData.proof_url || undefined, + }), + }); + + const data = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.error || data.message || 'Failed to submit payment confirmation'); + } + + setSuccess(true); + setTimeout(() => { + onClose(); + onSuccess?.(); + }, 2500); + } catch (err: any) { + setError(err.message || 'Failed to submit payment'); + } finally { + setLoading(false); + } + }; + + const handlePayNow = () => { + if (selectedOption === 'stripe') { + handleStripePayment(); + } else if (selectedOption === 'paypal') { + handlePayPalPayment(); + } + // Bank transfer uses form submit + }; + + return ( + +
+ {success ? ( +
+ +

+ {selectedOption === 'bank_transfer' ? 'Payment Submitted!' : 'Redirecting...'} +

+

+ {selectedOption === 'bank_transfer' + ? 'Your payment confirmation has been submitted for review.' + : 'You will be redirected to complete your payment.'} +

+
+ ) : ( + <> + {/* Header */} +
+
+

Pay Invoice

+

#{invoice.invoice_number}

+
+
+
+ {currency} {amount.toFixed(2)} +
+
+
+ + {error && ( +
+ {error} +
+ )} + + {/* Payment Method Selection */} + {availableOptions.length > 1 && ( +
+ +
+ {/* Stripe (Card) - Always available */} + + + {/* PayPal - Only for non-Pakistan */} + {!isPakistan && availableOptions.includes('paypal') && ( + + )} + + {/* Bank Transfer - Only for Pakistan */} + {isPakistan && availableOptions.includes('bank_transfer') && ( + + )} +
+
+ )} + + {/* Stripe Payment */} + {selectedOption === 'stripe' && ( +
+
+ +

+ Pay securely with your credit or debit card via Stripe. +

+ +
+
+ )} + + {/* PayPal Payment - Only for non-Pakistan */} + {selectedOption === 'paypal' && !isPakistan && ( +
+
+ +

+ Pay securely using your PayPal account. +

+ +
+
+ )} + + {/* Bank Transfer - Only for Pakistan */} + {selectedOption === 'bank_transfer' && isPakistan && ( +
+ {/* Bank Details */} +
+

+ + Bank Transfer Details +

+
+

Bank: Standard Chartered Bank Pakistan

+

Account Title: IGNY8 Technologies

+

Account #: 01-2345678-01

+

IBAN: PK36SCBL0000001234567890

+

Reference: {invoice.invoice_number}

+
+
+ + {/* Transaction Reference */} +
+ + setBankFormData({ ...bankFormData, manual_reference: e.target.value })} + placeholder="Enter your bank transaction reference" + disabled={loading} + /> +
+ + {/* Notes */} +
+ +