bank trnasfer deteiasl udaptes and issues fixed
This commit is contained in:
@@ -87,7 +87,7 @@ class PaymentMethodConfigSerializer(serializers.ModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'country_code', 'payment_method', 'payment_method_display',
|
'id', 'country_code', 'payment_method', 'payment_method_display',
|
||||||
'is_enabled', 'display_name', 'instructions',
|
'is_enabled', 'display_name', 'instructions',
|
||||||
'bank_name', 'account_number', 'swift_code',
|
'bank_name', 'account_title', 'account_number', 'routing_number', 'swift_code', 'iban',
|
||||||
'wallet_type', 'wallet_id', 'sort_order'
|
'wallet_type', 'wallet_id', 'sort_order'
|
||||||
]
|
]
|
||||||
read_only_fields = ['id']
|
read_only_fields = ['id']
|
||||||
|
|||||||
@@ -642,34 +642,154 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
|||||||
return Response({'results': data})
|
return Response({'results': data})
|
||||||
|
|
||||||
def approve_payment(self, request, pk):
|
def approve_payment(self, request, pk):
|
||||||
"""Approve a pending payment"""
|
"""Approve a pending payment - activates account, subscription, and adds credits"""
|
||||||
|
from django.db import transaction
|
||||||
from igny8_core.business.billing.models import Payment
|
from igny8_core.business.billing.models import Payment
|
||||||
|
from igny8_core.modules.billing.services import CreditService
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payment = Payment.objects.get(pk=pk, status='pending_approval')
|
with transaction.atomic():
|
||||||
payment.status = 'completed'
|
# Get payment with related objects
|
||||||
payment.processed_at = timezone.now()
|
payment = Payment.objects.select_related(
|
||||||
payment.save()
|
'invoice',
|
||||||
|
'invoice__subscription',
|
||||||
# If payment has an invoice, mark it as paid
|
'invoice__subscription__plan',
|
||||||
if payment.invoice:
|
'account',
|
||||||
payment.invoice.status = 'paid'
|
'account__subscription',
|
||||||
payment.invoice.paid_at = timezone.now()
|
'account__subscription__plan',
|
||||||
payment.invoice.save()
|
'account__plan'
|
||||||
|
).get(pk=pk, status='pending_approval')
|
||||||
return Response({'success': True, 'message': 'Payment approved'})
|
|
||||||
|
admin_notes = request.data.get('notes', '')
|
||||||
|
|
||||||
|
# 1. Update Payment status
|
||||||
|
payment.status = 'succeeded'
|
||||||
|
payment.processed_at = timezone.now()
|
||||||
|
payment.approved_by = request.user
|
||||||
|
payment.approved_at = timezone.now()
|
||||||
|
if admin_notes:
|
||||||
|
payment.admin_notes = admin_notes
|
||||||
|
payment.save()
|
||||||
|
|
||||||
|
invoice = payment.invoice
|
||||||
|
account = payment.account
|
||||||
|
|
||||||
|
# 2. Mark invoice as paid
|
||||||
|
if invoice:
|
||||||
|
invoice.status = 'paid'
|
||||||
|
invoice.paid_at = timezone.now()
|
||||||
|
invoice.save()
|
||||||
|
|
||||||
|
# 3. Get and activate subscription
|
||||||
|
subscription = None
|
||||||
|
if invoice and hasattr(invoice, 'subscription') and invoice.subscription:
|
||||||
|
subscription = invoice.subscription
|
||||||
|
elif account and hasattr(account, 'subscription'):
|
||||||
|
try:
|
||||||
|
subscription = account.subscription
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if subscription:
|
||||||
|
subscription.status = 'active'
|
||||||
|
subscription.external_payment_id = payment.manual_reference
|
||||||
|
subscription.save(update_fields=['status', 'external_payment_id'])
|
||||||
|
|
||||||
|
# 4. CRITICAL: Set account status to active
|
||||||
|
account.status = 'active'
|
||||||
|
account.save(update_fields=['status'])
|
||||||
|
|
||||||
|
# 5. Add credits if plan has included credits
|
||||||
|
credits_added = 0
|
||||||
|
try:
|
||||||
|
plan = None
|
||||||
|
if subscription and subscription.plan:
|
||||||
|
plan = subscription.plan
|
||||||
|
elif account and account.plan:
|
||||||
|
plan = account.plan
|
||||||
|
|
||||||
|
if plan and plan.included_credits > 0:
|
||||||
|
credits_added = plan.included_credits
|
||||||
|
CreditService.add_credits(
|
||||||
|
account=account,
|
||||||
|
amount=credits_added,
|
||||||
|
transaction_type='subscription',
|
||||||
|
description=f'{plan.name} plan credits - Invoice {invoice.invoice_number if invoice else "N/A"}',
|
||||||
|
metadata={
|
||||||
|
'subscription_id': subscription.id if subscription else None,
|
||||||
|
'invoice_id': invoice.id if invoice else None,
|
||||||
|
'payment_id': payment.id,
|
||||||
|
'plan_id': plan.id,
|
||||||
|
'approved_by': request.user.email
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as credit_error:
|
||||||
|
logger.error(f'Credit addition failed for payment {payment.id}: {credit_error}', exc_info=True)
|
||||||
|
# Don't fail the approval if credits fail - account is still activated
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f'Payment approved: Payment {payment.id}, Account {account.id} set to active, '
|
||||||
|
f'{credits_added} credits added'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. Send approval email
|
||||||
|
try:
|
||||||
|
from igny8_core.business.billing.services.email_service import BillingEmailService
|
||||||
|
BillingEmailService.send_payment_approved_email(payment, account, subscription)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to send payment approved email: {str(e)}')
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Payment approved. Account activated.',
|
||||||
|
'payment': {
|
||||||
|
'id': payment.id,
|
||||||
|
'status': payment.status,
|
||||||
|
'account_status': account.status,
|
||||||
|
'credits_added': credits_added
|
||||||
|
}
|
||||||
|
})
|
||||||
except Payment.DoesNotExist:
|
except Payment.DoesNotExist:
|
||||||
return Response({'error': 'Payment not found or not pending'}, status=404)
|
return Response({'error': 'Payment not found or not pending'}, status=404)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Error approving payment {pk}: {str(e)}', exc_info=True)
|
||||||
|
return Response({'error': f'Failed to approve payment: {str(e)}'}, status=500)
|
||||||
|
|
||||||
def reject_payment(self, request, pk):
|
def reject_payment(self, request, pk):
|
||||||
"""Reject a pending payment"""
|
"""Reject a pending payment"""
|
||||||
from igny8_core.business.billing.models import Payment
|
from igny8_core.business.billing.models import Payment
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payment = Payment.objects.get(pk=pk, status='pending_approval')
|
payment = Payment.objects.select_related('account').get(pk=pk, status='pending_approval')
|
||||||
|
rejection_reason = request.data.get('reason', 'Rejected by admin')
|
||||||
|
|
||||||
payment.status = 'failed'
|
payment.status = 'failed'
|
||||||
payment.failed_at = timezone.now()
|
payment.failed_at = timezone.now()
|
||||||
payment.failure_reason = request.data.get('reason', 'Rejected by admin')
|
payment.failure_reason = rejection_reason
|
||||||
|
payment.approved_by = request.user
|
||||||
|
payment.approved_at = timezone.now()
|
||||||
|
payment.admin_notes = rejection_reason
|
||||||
payment.save()
|
payment.save()
|
||||||
|
|
||||||
|
# Update account status to allow retry (if not already active)
|
||||||
|
account = payment.account
|
||||||
|
if account and account.status != 'active':
|
||||||
|
account.status = 'pending_payment'
|
||||||
|
account.save(update_fields=['status'])
|
||||||
|
|
||||||
|
logger.info(f'Payment rejected: Payment {payment.id}, Reason: {rejection_reason}')
|
||||||
|
|
||||||
|
# Send rejection email
|
||||||
|
try:
|
||||||
|
from igny8_core.business.billing.services.email_service import BillingEmailService
|
||||||
|
BillingEmailService.send_payment_rejected_email(payment, account, rejection_reason)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Failed to send payment rejected email: {str(e)}')
|
||||||
|
|
||||||
return Response({'success': True, 'message': 'Payment rejected'})
|
return Response({'success': True, 'message': 'Payment rejected'})
|
||||||
except Payment.DoesNotExist:
|
except Payment.DoesNotExist:
|
||||||
return Response({'error': 'Payment not found or not pending'}, status=404)
|
return Response({'error': 'Payment not found or not pending'}, status=404)
|
||||||
|
|||||||
@@ -36,12 +36,18 @@ interface BankDetails {
|
|||||||
|
|
||||||
interface BankTransferFormProps {
|
interface BankTransferFormProps {
|
||||||
invoice: Invoice;
|
invoice: Invoice;
|
||||||
|
planPrice?: string;
|
||||||
|
planPricePKR?: string;
|
||||||
|
userCountry?: string;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BankTransferForm({
|
export default function BankTransferForm({
|
||||||
invoice,
|
invoice,
|
||||||
|
planPrice,
|
||||||
|
planPricePKR,
|
||||||
|
userCountry,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: BankTransferFormProps) {
|
}: BankTransferFormProps) {
|
||||||
@@ -61,44 +67,26 @@ export default function BankTransferForm({
|
|||||||
const loadBankDetails = async () => {
|
const loadBankDetails = async () => {
|
||||||
setBankDetailsLoading(true);
|
setBankDetailsLoading(true);
|
||||||
try {
|
try {
|
||||||
const { results } = await getAvailablePaymentMethods();
|
const { results } = await getAvailablePaymentMethods(userCountry);
|
||||||
// Find bank_transfer method config
|
// Find bank_transfer method config
|
||||||
const bankMethod = results.find(
|
const bankMethod = results.find(
|
||||||
(m) => m.type === 'bank_transfer' && m.is_enabled
|
(m) => m.type === 'bank_transfer' && m.is_enabled
|
||||||
);
|
) as any;
|
||||||
|
|
||||||
// Cast to any to access extended bank_details properties
|
if (bankMethod?.bank_name && (bankMethod?.account_title || bankMethod?.account_number)) {
|
||||||
// Backend may return additional fields not in the TypeScript type
|
|
||||||
const details = bankMethod?.bank_details as any;
|
|
||||||
|
|
||||||
if (details) {
|
|
||||||
setBankDetails({
|
setBankDetails({
|
||||||
bank_name: details.bank_name || 'Bank ABC',
|
bank_name: bankMethod.bank_name,
|
||||||
account_title: details.account_title || details.account_name || 'IGNY8',
|
account_title: bankMethod.account_title || bankMethod.account_name || '',
|
||||||
account_number: details.account_number || '',
|
account_number: bankMethod.account_number || '',
|
||||||
iban: details.iban,
|
iban: bankMethod.iban,
|
||||||
swift_code: details.swift_code,
|
swift_code: bankMethod.swift_code,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Fallback hardcoded details - should be replaced with backend config
|
setBankDetails(null);
|
||||||
setBankDetails({
|
|
||||||
bank_name: 'MCB Bank Limited',
|
|
||||||
account_title: 'IGNY8 Technologies',
|
|
||||||
account_number: '0000123456789',
|
|
||||||
iban: 'PK00MUCB0000000123456789',
|
|
||||||
swift_code: 'MUCBPKKAXXX',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load bank details:', error);
|
console.error('Failed to load bank details:', error);
|
||||||
// Use fallback
|
setBankDetails(null);
|
||||||
setBankDetails({
|
|
||||||
bank_name: 'MCB Bank Limited',
|
|
||||||
account_title: 'IGNY8 Technologies',
|
|
||||||
account_number: '0000123456789',
|
|
||||||
iban: 'PK00MUCB0000000123456789',
|
|
||||||
swift_code: 'MUCBPKKAXXX',
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setBankDetailsLoading(false);
|
setBankDetailsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -280,18 +268,26 @@ export default function BankTransferForm({
|
|||||||
{/* Amount */}
|
{/* Amount */}
|
||||||
<div className="flex justify-between items-center pt-3 border-t border-gray-200 dark:border-gray-700">
|
<div className="flex justify-between items-center pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
<span className="font-medium text-gray-900 dark:text-white">Amount to Transfer</span>
|
<span className="font-medium text-gray-900 dark:text-white">Amount to Transfer</span>
|
||||||
<span className="text-xl font-bold text-brand-600 dark:text-brand-400">
|
<div className="text-right">
|
||||||
{invoice.currency === 'PKR' ? 'PKR ' : '$'}
|
<div className="text-xl font-bold text-brand-600 dark:text-brand-400">
|
||||||
{(() => {
|
${(() => {
|
||||||
const amount = parseFloat(String(invoice.total_amount || invoice.total || 0));
|
const amount = parseFloat(String(planPrice || invoice.total_amount || invoice.total || 0));
|
||||||
// Round PKR to nearest thousand
|
return Number.isFinite(amount) ? amount.toFixed(2) : '0.00';
|
||||||
if (invoice.currency === 'PKR') {
|
})()} USD
|
||||||
const rounded = Math.round(amount / 1000) * 1000;
|
</div>
|
||||||
|
<div className="text-sm font-medium text-brand-500">
|
||||||
|
≈ PKR {(() => {
|
||||||
|
const pkrAmount = planPricePKR
|
||||||
|
? parseFloat(planPricePKR)
|
||||||
|
: (() => {
|
||||||
|
const usdAmount = parseFloat(String(planPrice || invoice.total_amount || invoice.total || 0));
|
||||||
|
return Number.isFinite(usdAmount) ? usdAmount * 278 : 0;
|
||||||
|
})();
|
||||||
|
const rounded = Math.round(pkrAmount / 1000) * 1000;
|
||||||
return rounded.toLocaleString();
|
return rounded.toLocaleString();
|
||||||
}
|
})()}
|
||||||
return amount.toFixed(2);
|
</div>
|
||||||
})()}
|
</div>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -330,7 +326,7 @@ export default function BankTransferForm({
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
Cancel
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -25,7 +25,15 @@ import {
|
|||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
import { API_BASE_URL } from '../../services/api';
|
import { API_BASE_URL } from '../../services/api';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
import { subscribeToPlan } from '../../services/billing.api';
|
import { subscribeToPlan, getAvailablePaymentMethods } from '../../services/billing.api';
|
||||||
|
|
||||||
|
interface BankDetails {
|
||||||
|
bank_name: string;
|
||||||
|
account_title: string;
|
||||||
|
account_number: string;
|
||||||
|
iban?: string;
|
||||||
|
swift_code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Invoice {
|
interface Invoice {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -109,6 +117,10 @@ export default function PayInvoiceModal({
|
|||||||
});
|
});
|
||||||
const [uploadedFileName, setUploadedFileName] = useState('');
|
const [uploadedFileName, setUploadedFileName] = useState('');
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
|
// Bank details loaded from backend
|
||||||
|
const [bankDetails, setBankDetails] = useState<BankDetails | null>(null);
|
||||||
|
const [bankDetailsLoading, setBankDetailsLoading] = useState(false);
|
||||||
|
|
||||||
const amount = parseFloat(invoice.total_amount || invoice.total || '0');
|
const amount = parseFloat(invoice.total_amount || invoice.total || '0');
|
||||||
const currency = invoice.currency?.toUpperCase() || 'USD';
|
const currency = invoice.currency?.toUpperCase() || 'USD';
|
||||||
@@ -132,9 +144,42 @@ export default function PayInvoiceModal({
|
|||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
setBankFormData({ manual_reference: '', manual_notes: '', proof_url: '' });
|
setBankFormData({ manual_reference: '', manual_notes: '', proof_url: '' });
|
||||||
setUploadedFileName('');
|
setUploadedFileName('');
|
||||||
|
setBankDetails(null);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Load bank details from backend when bank_transfer is selected for PK users
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !isPakistan || selectedOption !== 'bank_transfer') return;
|
||||||
|
if (bankDetails) return; // Already loaded
|
||||||
|
|
||||||
|
const loadBankDetails = async () => {
|
||||||
|
setBankDetailsLoading(true);
|
||||||
|
try {
|
||||||
|
const { results } = await getAvailablePaymentMethods(userCountry);
|
||||||
|
const bankMethod = results.find(
|
||||||
|
(m) => m.type === 'bank_transfer' && m.is_enabled
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
if (bankMethod?.bank_name && (bankMethod?.account_title || bankMethod?.account_number)) {
|
||||||
|
setBankDetails({
|
||||||
|
bank_name: bankMethod.bank_name,
|
||||||
|
account_title: bankMethod.account_title || '',
|
||||||
|
account_number: bankMethod.account_number || '',
|
||||||
|
iban: bankMethod.iban,
|
||||||
|
swift_code: bankMethod.swift_code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load bank details:', err);
|
||||||
|
} finally {
|
||||||
|
setBankDetailsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadBankDetails();
|
||||||
|
}, [isOpen, isPakistan, selectedOption, userCountry, bankDetails]);
|
||||||
|
|
||||||
const handleStripePayment = async () => {
|
const handleStripePayment = async () => {
|
||||||
// Use plan slug if available, otherwise fall back to id
|
// Use plan slug if available, otherwise fall back to id
|
||||||
const planIdentifier = planSlug || (planId ? String(planId) : null);
|
const planIdentifier = planSlug || (planId ? String(planId) : null);
|
||||||
@@ -434,13 +479,24 @@ export default function PayInvoiceModal({
|
|||||||
<Building2Icon className="w-5 h-5" />
|
<Building2Icon className="w-5 h-5" />
|
||||||
Bank Transfer Details
|
Bank Transfer Details
|
||||||
</h4>
|
</h4>
|
||||||
<div className="text-sm text-brand-800 dark:text-brand-200 space-y-1">
|
{bankDetailsLoading ? (
|
||||||
<p><span className="font-medium">Bank:</span> Standard Chartered Bank Pakistan</p>
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
<p><span className="font-medium">Account Title:</span> IGNY8 Technologies</p>
|
<Loader2Icon className="w-4 h-4 animate-spin" />
|
||||||
<p><span className="font-medium">Account #:</span> 01-2345678-01</p>
|
Loading bank details...
|
||||||
<p><span className="font-medium">IBAN:</span> PK36SCBL0000001234567890</p>
|
</div>
|
||||||
<p><span className="font-medium">Reference:</span> {invoice.invoice_number}</p>
|
) : bankDetails ? (
|
||||||
</div>
|
<div className="text-sm text-brand-800 dark:text-brand-200 space-y-1">
|
||||||
|
<p><span className="font-medium">Bank:</span> {bankDetails.bank_name}</p>
|
||||||
|
<p><span className="font-medium">Account Title:</span> {bankDetails.account_title}</p>
|
||||||
|
<p><span className="font-medium">Account #:</span> {bankDetails.account_number}</p>
|
||||||
|
{bankDetails.iban && <p><span className="font-medium">IBAN:</span> {bankDetails.iban}</p>}
|
||||||
|
<p><span className="font-medium">Reference:</span> {invoice.invoice_number}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-error-600 dark:text-error-400">
|
||||||
|
Bank details not available. Please contact support.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Transaction Reference */}
|
{/* Transaction Reference */}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
AlertCircleIcon,
|
AlertCircleIcon,
|
||||||
Loader2Icon,
|
Loader2Icon,
|
||||||
ArrowLeftIcon,
|
|
||||||
LockIcon,
|
LockIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
@@ -86,6 +85,7 @@ export default function PendingPaymentView({
|
|||||||
}: PendingPaymentViewProps) {
|
}: PendingPaymentViewProps) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const refreshUser = useAuthStore((state) => state.refreshUser);
|
const refreshUser = useAuthStore((state) => state.refreshUser);
|
||||||
|
const user = useAuthStore((state) => state.user);
|
||||||
const [selectedGateway, setSelectedGateway] = useState<PaymentGateway>('stripe');
|
const [selectedGateway, setSelectedGateway] = useState<PaymentGateway>('stripe');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [gatewaysLoading, setGatewaysLoading] = useState(true);
|
const [gatewaysLoading, setGatewaysLoading] = useState(true);
|
||||||
@@ -105,40 +105,64 @@ export default function PendingPaymentView({
|
|||||||
|
|
||||||
// Check if bank transfer has been approved
|
// Check if bank transfer has been approved
|
||||||
const checkPaymentStatus = useCallback(async () => {
|
const checkPaymentStatus = useCallback(async () => {
|
||||||
if (!bankTransferSubmitted) return;
|
if (!bankTransferSubmitted || checkingStatus) return;
|
||||||
|
|
||||||
setCheckingStatus(true);
|
setCheckingStatus(true);
|
||||||
try {
|
try {
|
||||||
// Refresh user data from backend
|
// Refresh user data from backend
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
|
|
||||||
|
// Get fresh account status from store after refresh
|
||||||
|
const accountStatus = useAuthStore.getState().user?.account?.status;
|
||||||
|
|
||||||
// Also check payments to see if any succeeded
|
// Only consider active - the account must be explicitly set to active after payment approval
|
||||||
const { results: payments } = await getPayments();
|
if (accountStatus === 'active') {
|
||||||
const hasSucceededPayment = payments.some(
|
|
||||||
(p: any) => p.status === 'succeeded' || p.status === 'completed'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasSucceededPayment) {
|
|
||||||
toast?.success?.('Payment approved! Your account is now active.');
|
toast?.success?.('Payment approved! Your account is now active.');
|
||||||
onPaymentSuccess();
|
onPaymentSuccess();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the specific invoice's payment was approved
|
||||||
|
// Only check payments for THIS invoice, not all payments
|
||||||
|
if (invoice) {
|
||||||
|
const { results: payments } = await getPayments();
|
||||||
|
const invoicePayment = payments.find(
|
||||||
|
(p: any) => p.invoice_id === invoice.id && p.status === 'succeeded'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invoicePayment) {
|
||||||
|
// Payment succeeded but account might not be refreshed yet, refresh again
|
||||||
|
await refreshUser();
|
||||||
|
const updatedStatus = useAuthStore.getState().user?.account?.status;
|
||||||
|
if (updatedStatus === 'active') {
|
||||||
|
toast?.success?.('Payment approved! Your account is now active.');
|
||||||
|
onPaymentSuccess();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still pending - no action needed
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to check payment status:', error);
|
console.error('Failed to check payment status:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setCheckingStatus(false);
|
setCheckingStatus(false);
|
||||||
}
|
}
|
||||||
}, [bankTransferSubmitted, refreshUser, onPaymentSuccess, toast]);
|
}, [bankTransferSubmitted, checkingStatus, refreshUser, onPaymentSuccess, toast, invoice]);
|
||||||
|
|
||||||
// Auto-check status every 30 seconds when awaiting approval
|
// Auto-check status every 30 seconds when awaiting approval
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bankTransferSubmitted) return;
|
if (!bankTransferSubmitted) return;
|
||||||
|
let interval: ReturnType<typeof setInterval> | undefined;
|
||||||
// Check immediately on mount
|
const timeout = setTimeout(() => {
|
||||||
checkPaymentStatus();
|
checkPaymentStatus();
|
||||||
|
interval = setInterval(checkPaymentStatus, 30000);
|
||||||
// Then poll every 30 seconds
|
}, 30000);
|
||||||
const interval = setInterval(checkPaymentStatus, 30000);
|
|
||||||
return () => clearInterval(interval);
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (interval) clearInterval(interval);
|
||||||
|
};
|
||||||
}, [bankTransferSubmitted, checkPaymentStatus]);
|
}, [bankTransferSubmitted, checkPaymentStatus]);
|
||||||
|
|
||||||
// Load available payment gateways
|
// Load available payment gateways
|
||||||
@@ -244,16 +268,11 @@ export default function PendingPaymentView({
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto">
|
||||||
<button
|
|
||||||
onClick={() => setShowBankTransfer(false)}
|
|
||||||
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-6"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon className="w-4 h-4" />
|
|
||||||
Back to payment options
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<BankTransferForm
|
<BankTransferForm
|
||||||
invoice={invoice}
|
invoice={invoice}
|
||||||
|
planPrice={planPrice}
|
||||||
|
planPricePKR={planPricePKR}
|
||||||
|
userCountry={userCountry}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setShowBankTransfer(false);
|
setShowBankTransfer(false);
|
||||||
setBankTransferSubmitted(true);
|
setBankTransferSubmitted(true);
|
||||||
@@ -273,14 +292,6 @@ export default function PendingPaymentView({
|
|||||||
<div className="max-w-xl mx-auto">
|
<div className="max-w-xl mx-auto">
|
||||||
{/* Header with Awaiting Badge */}
|
{/* Header with Awaiting Badge */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-amber-100 dark:bg-amber-900/30 mb-4 animate-pulse">
|
|
||||||
<Loader2Icon className="w-10 h-10 text-amber-600 dark:text-amber-400 animate-spin" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center gap-2 mb-3">
|
|
||||||
<Badge variant="soft" tone="warning" size="md">
|
|
||||||
Awaiting Approval
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
Payment Submitted!
|
Payment Submitted!
|
||||||
</h1>
|
</h1>
|
||||||
@@ -377,6 +388,15 @@ export default function PendingPaymentView({
|
|||||||
Status is checked automatically every 30 seconds
|
Status is checked automatically every 30 seconds
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-col items-center gap-2">
|
||||||
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-amber-100 dark:bg-amber-900/30 animate-pulse">
|
||||||
|
<Loader2Icon className="w-8 h-8 text-amber-600 dark:text-amber-400 animate-spin" />
|
||||||
|
</div>
|
||||||
|
<Badge variant="soft" tone="warning" size="md">
|
||||||
|
Awaiting Approval
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Disabled Payment Options Notice */}
|
{/* Disabled Payment Options Notice */}
|
||||||
<div className="mt-6 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl opacity-60">
|
<div className="mt-6 p-4 bg-gray-100 dark:bg-gray-800 rounded-xl opacity-60">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
|||||||
@@ -189,6 +189,12 @@ export interface PaymentMethod {
|
|||||||
is_verified?: boolean;
|
is_verified?: boolean;
|
||||||
instructions?: string;
|
instructions?: string;
|
||||||
country_code?: string;
|
country_code?: string;
|
||||||
|
bank_name?: string;
|
||||||
|
account_title?: string;
|
||||||
|
account_number?: string;
|
||||||
|
routing_number?: string;
|
||||||
|
swift_code?: string;
|
||||||
|
iban?: string;
|
||||||
bank_details?: {
|
bank_details?: {
|
||||||
bank_name?: string;
|
bank_name?: string;
|
||||||
account_number?: string;
|
account_number?: string;
|
||||||
@@ -686,12 +692,13 @@ export async function removeTeamMember(memberId: number): Promise<{
|
|||||||
|
|
||||||
// Get GLOBAL payment method configs (system-wide available payment options like stripe, paypal, bank_transfer)
|
// Get GLOBAL payment method configs (system-wide available payment options like stripe, paypal, bank_transfer)
|
||||||
// This is used on Plans page to show what payment methods are available to choose
|
// This is used on Plans page to show what payment methods are available to choose
|
||||||
export async function getAvailablePaymentMethods(): Promise<{
|
export async function getAvailablePaymentMethods(countryCode?: string): Promise<{
|
||||||
results: PaymentMethod[];
|
results: PaymentMethod[];
|
||||||
count: number;
|
count: number;
|
||||||
}> {
|
}> {
|
||||||
// Call the payment-configs endpoint which returns global PaymentMethodConfig records
|
// Call the payment-configs endpoint which returns global PaymentMethodConfig records
|
||||||
const response = await fetchAPI('/v1/billing/payment-configs/payment-methods/');
|
const params = countryCode ? `?country_code=${countryCode}` : '';
|
||||||
|
const response = await fetchAPI(`/v1/billing/payment-configs/payment-methods/${params}`);
|
||||||
// Return all payment methods - stripe, paypal, bank_transfer, manual, local_wallet
|
// Return all payment methods - stripe, paypal, bank_transfer, manual, local_wallet
|
||||||
const results = Array.isArray(response.results) ? response.results : [];
|
const results = Array.isArray(response.results) ? response.results : [];
|
||||||
// Map payment_method to type for consistent API
|
// Map payment_method to type for consistent API
|
||||||
|
|||||||
Reference in New Issue
Block a user