diff --git a/core/urls.py b/core/urls.py index fc681e0..705276b 100644 --- a/core/urls.py +++ b/core/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ path('wallet/demo/', views.wallet_demo_view, name='wallet_demo'), path('wallet/demo/test-payment/', views.wallet_demo_test_payment, name='wallet_demo_test_payment'), path('wallet/demo/check-balance/', views.wallet_demo_check_balance, name='wallet_demo_check_balance'), + path('wallet/verify-payment/', views.verify_payment_view, name='verify_payment'), path('stripe/webhook/', views.stripe_webhook_view, name='stripe_webhook'), path('webhook-test/', views.webhook_test_view, name='webhook_test'), path('stripe-webhook-test/', views.stripe_webhook_test_view, name='stripe_webhook_test'), diff --git a/core/views.py b/core/views.py index 229b6dc..bbdd2ed 100644 --- a/core/views.py +++ b/core/views.py @@ -132,8 +132,36 @@ def wallet_topup_view(request): @login_required def wallet_topup_success_view(request): - """Payment success page""" - messages.success(request, 'Payment successful! Your wallet balance has been updated.') + """Payment success page with automatic payment verification (NO WEBHOOKS NEEDED)""" + session_id = request.GET.get('session_id') + + if not session_id: + messages.error(request, 'No payment session found. Please contact support if you completed a payment.') + return redirect('core:wallet') + + # Verify payment directly with Stripe API (bypasses webhook issues) + try: + from wallet.stripe_handler import StripePaymentHandler + stripe_handler = StripePaymentHandler() + + print(f"๐Ÿ’ณ [SUCCESS PAGE] Verifying payment for session: {session_id}") + result = stripe_handler.verify_payment(session_id) + + if result['success']: + if result['processed']: + messages.success(request, f'Payment successful! {result["amount"]} AED has been added to your wallet.') + print(f"โœ… [SUCCESS PAGE] Payment verified and wallet updated for user {request.user.id}") + else: + messages.info(request, 'Payment already processed. Your wallet balance is up to date.') + print(f"โ„น๏ธ [SUCCESS PAGE] Payment already processed for session {session_id}") + else: + messages.warning(request, f'Payment verification failed: {result.get("error", "Unknown error")}. Please contact support.') + print(f"โŒ [SUCCESS PAGE] Payment verification failed: {result}") + + except Exception as e: + print(f"โŒ [SUCCESS PAGE] Error verifying payment: {e}") + messages.error(request, 'Unable to verify payment. Please contact support if you completed a payment.') + return redirect('core:wallet') @@ -222,6 +250,46 @@ def wallet_demo_check_balance(request): }) +@login_required +def verify_payment_view(request): + """Manual payment verification endpoint (fallback for webhook issues)""" + if request.method == 'POST': + try: + data = json.loads(request.body) + session_id = data.get('session_id', '').strip() + + if not session_id: + return JsonResponse({'error': 'Session ID is required'}, status=400) + + # Verify payment with Stripe + from wallet.stripe_handler import StripePaymentHandler + stripe_handler = StripePaymentHandler() + + print(f"๐Ÿ” [MANUAL VERIFY] User {request.user.id} verifying session: {session_id}") + result = stripe_handler.verify_payment(session_id) + + if result['success']: + return JsonResponse({ + 'success': True, + 'processed': result['processed'], + 'amount': result.get('amount', 0), + 'message': result['message'] + }) + else: + return JsonResponse({ + 'success': False, + 'error': result['error'] + }) + + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON data'}, status=400) + except Exception as e: + print(f"โŒ [MANUAL VERIFY] Error: {e}") + return JsonResponse({'error': str(e)}, status=500) + + return JsonResponse({'error': 'Method not allowed'}, status=405) + + # Simple webhook test page def webhook_test_view(request): """Simple HTML page for webhook testing""" diff --git a/templates/core/wallet.html b/templates/core/wallet.html index e786246..4c598a5 100644 --- a/templates/core/wallet.html +++ b/templates/core/wallet.html @@ -231,6 +231,120 @@ margin: 0; } + /* Verify payment button */ + .verify-payment-btn { + background: rgba(255, 255, 255, 0.15); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); + padding: 8px 16px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + backdrop-filter: blur(10px); + } + + .verify-payment-btn:hover { + background: rgba(255, 255, 255, 0.25); + transform: translateY(-1px); + } + + /* Verify modal */ + .verify-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; + } + + .verify-modal-content { + background: white; + border-radius: 16px; + padding: 24px; + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + } + + .verify-modal-title { + font-size: 20px; + font-weight: 700; + margin-bottom: 16px; + color: var(--text-primary); + } + + .verify-input { + width: 100%; + padding: 12px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 14px; + margin: 8px 0; + font-family: 'Courier New', monospace; + } + + .verify-buttons { + display: flex; + gap: 12px; + margin-top: 20px; + } + + .verify-btn { + flex: 1; + padding: 12px 20px; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + } + + .verify-btn.primary { + background: var(--primary-blue); + color: white; + } + + .verify-btn.primary:hover { + background: var(--primary-dark); + } + + .verify-btn.secondary { + background: #f1f5f9; + color: var(--text-primary); + } + + .verify-btn.secondary:hover { + background: #e2e8f0; + } + + .verify-result { + margin-top: 16px; + padding: 12px; + border-radius: 8px; + font-size: 14px; + display: none; + } + + .verify-result.success { + background: #dcfce7; + color: #16a34a; + border: 1px solid #bbf7d0; + } + + .verify-result.error { + background: #fef2f2; + color: #dc2626; + border: 1px solid #fecaca; + } + /* Responsive adjustments */ @media (max-width: 768px) { .stats-grid { @@ -260,6 +374,11 @@ ๐Ÿ’ณ Top Up Wallet +
+ +
@@ -311,7 +430,126 @@ {% endif %} + + +
+
+

๐Ÿ” Verify Payment

+

+ If you completed a payment but your wallet balance wasn't updated, enter the Stripe session ID below to verify it manually. +

+ + + + +

+ ๐Ÿ’ก You can find the session ID in your browser URL after completing payment, or in your email receipt. +

+ +
+ + +
+ +
+
+
{% endblock %} {% block extra_js %} + {% endblock %} \ No newline at end of file diff --git a/test_webhook_external.py b/test_webhook_external.py new file mode 100644 index 0000000..425c396 --- /dev/null +++ b/test_webhook_external.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Test external webhook accessibility +""" +import requests +import json +from datetime import datetime + +def test_webhook_accessibility(): + """Test if Railway webhook endpoints are accessible externally""" + + base_url = "https://netcop.up.railway.app" + + endpoints = [ + "/simple-webhook-test/", + "/stripe/webhook/", + ] + + print("๐Ÿงช Testing Railway webhook endpoint accessibility...") + print(f"๐Ÿ“ Base URL: {base_url}") + print("-" * 60) + + for endpoint in endpoints: + url = base_url + endpoint + + try: + print(f"\n๐Ÿ” Testing: {url}") + + # Test with POST request + response = requests.post( + url, + json={ + "test": True, + "source": "external_test_script", + "timestamp": datetime.now().isoformat() + }, + headers={ + "Content-Type": "application/json", + "User-Agent": "WebhookTester/1.0", + "X-Test-Source": "external" + }, + timeout=10 + ) + + print(f"โœ… Status: {response.status_code}") + print(f"โœ… Response: {response.text[:200]}") + + if response.status_code < 400: + print(f"โœ… SUCCESS: {endpoint} is accessible") + else: + print(f"โŒ FAILED: {endpoint} returned {response.status_code}") + + except requests.exceptions.RequestException as e: + print(f"โŒ CONNECTION ERROR: {e}") + except Exception as e: + print(f"โŒ UNEXPECTED ERROR: {e}") + + print("\n" + "=" * 60) + print("๐Ÿ External accessibility test completed") + +if __name__ == "__main__": + test_webhook_accessibility() \ No newline at end of file diff --git a/wallet/stripe_handler.py b/wallet/stripe_handler.py index 01e923a..619450b 100644 --- a/wallet/stripe_handler.py +++ b/wallet/stripe_handler.py @@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model from django.http import JsonResponse from decimal import Decimal import json +import time User = get_user_model() stripe.api_key = settings.STRIPE_SECRET_KEY @@ -17,63 +18,104 @@ class StripePaymentHandler: self.allowed_amounts = [10, 50, 100, 500] def create_checkout_session(self, user, amount, request=None): - """Create a Stripe checkout session for wallet top-up""" + """Create a Stripe checkout session for wallet top-up (Modern Integration)""" if amount not in self.allowed_amounts: raise ValueError(f"Invalid amount: {amount}. Allowed amounts: {self.allowed_amounts}") - # Build URLs based on current request domain + # Build URLs with session_id parameter for payment verification if request: - success_url = request.build_absolute_uri('/wallet/top-up/success/') + success_url = request.build_absolute_uri('/wallet/top-up/success/') + '?session_id={CHECKOUT_SESSION_ID}' cancel_url = request.build_absolute_uri('/wallet/top-up/cancel/') else: - # Fallback URLs (shouldn't happen in normal flow) - success_url = 'https://netcop.up.railway.app/wallet/top-up/success/' + # Fallback URLs + success_url = 'https://netcop.up.railway.app/wallet/top-up/success/?session_id={CHECKOUT_SESSION_ID}' cancel_url = 'https://netcop.up.railway.app/wallet/top-up/cancel/' try: - print(f"๐Ÿš€ Creating checkout session for user {user.id} ({user.email}), amount: {amount} AED") + print(f"๐Ÿš€ [MODERN] Creating checkout session for user {user.id} ({user.email}), amount: {amount} AED") print(f"๐Ÿ“ Success URL: {success_url}") print(f"๐Ÿ“ Cancel URL: {cancel_url}") + print(f"๐Ÿ“ Webhook URL: https://netcop.up.railway.app/stripe/webhook/") + # Create session with modern Stripe practices session = stripe.checkout.Session.create( + # Modern payment method configuration payment_method_types=['card'], + + # Line items configuration line_items=[{ 'price_data': { 'currency': 'aed', 'product_data': { - 'name': f'NetCop Wallet Top-up', - 'description': f'Add {amount} AED to your wallet balance' + 'name': 'NetCop Wallet Top-up', + 'description': f'Add {amount} AED to your wallet balance', + 'metadata': { + 'service': 'netcop_wallet', + 'user_id': str(user.id) + } }, - 'unit_amount': int(amount * 100), # Convert to cents + 'unit_amount': int(amount * 100), # Convert to fils (AED cents) }, 'quantity': 1, }], + + # Payment configuration mode='payment', + + # URLs with session ID parameter success_url=success_url, cancel_url=cancel_url, + + # Customer and reference data client_reference_id=str(user.id), customer_email=user.email, + + # Comprehensive metadata for webhook processing metadata={ 'user_id': str(user.id), + 'user_email': user.email, 'amount': str(amount), - 'type': 'wallet_topup' - } + 'currency': 'aed', + 'type': 'wallet_topup', + 'service': 'netcop', + 'environment': 'production' if 'railway.app' in (request.get_host() if request else '') else 'development', + 'created_at': str(int(time.time())), + 'app_version': '1.0' + }, + + # Modern Stripe features + payment_intent_data={ + 'metadata': { + 'user_id': str(user.id), + 'amount': str(amount), + 'service': 'netcop_wallet' + } + }, + + # Automatic tax and billing + automatic_tax={'enabled': False}, + + # Expiration + expires_at=int(time.time()) + (30 * 60), # 30 minutes from now ) - print(f"โœ… Session created: {session.id}") - print(f"๐Ÿ’ณ Client reference ID: {session.client_reference_id}") - print(f"๐Ÿ”— Payment URL: {session.url}") - - # โœ… WEBHOOK BYPASS: Process payment immediately after session creation - # This bypasses webhook delivery issues by simulating the webhook locally - print(f"๐Ÿ’ก BYPASS: Setting up webhook simulation for session {session.id}") + print(f"โœ… [MODERN] Session created successfully:") + print(f" ๐Ÿ’ณ Session ID: {session.id}") + print(f" ๐Ÿ‘ค Client Reference: {session.client_reference_id}") + print(f" ๐Ÿ’ฐ Amount: {amount} AED ({int(amount * 100)} fils)") + print(f" ๐Ÿ”— Payment URL: {session.url}") + print(f" โฐ Expires: {session.expires_at}") return { 'payment_url': session.url, - 'session_id': session.id + 'session_id': session.id, + 'amount': amount, + 'currency': 'aed', + 'expires_at': session.expires_at } except stripe.error.StripeError as e: + print(f"โŒ [MODERN] Stripe error: {str(e)}") raise ValueError(f"Failed to create checkout session: {str(e)}") def verify_payment(self, session_id):