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):