# ๐ณ NetCop Wallet/Stripe Implementation Guide ## ๐ฏ Overview This guide documents how to implement a professional wallet system with Stripe Payment Intents API in the NetCop Django project. The system provides real-time payment processing **without requiring webhooks** for basic functionality. ## โจ Key Features - **Professional wallet topup interface** with Stripe Elements - **Real-time payment processing** with Payment Intents API - **Loading states and progress indicators** for better UX - **Webhook-free operation** for development and testing - **AED currency support** matching NetCop pricing - **Balance checking** before agent usage - **Transaction history** with copy/download functionality ## ๐ซ No Webhooks Required ### Why No Webhooks Needed: - **Payment Intents API** provides immediate payment status - **Frontend confirmation** happens in real-time after card processing - **Direct database updates** via confirmed payment status - **Duplicate prevention** through payment metadata checking ### Payment Flow (Webhook-Free): 1. User selects topup amount โ Frontend creates Payment Intent 2. Stripe Elements processes card securely โ Returns success/failure 3. Frontend confirms payment status โ Backend updates wallet immediately 4. User sees updated balance โ Can use agents with sufficient funds --- ## ๐๏ธ Implementation Steps ### 1. Environment Configuration Add to `.env` file: ```bash # Stripe Configuration (No webhook secret required for basic functionality) STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here STRIPE_SECRET_KEY=sk_test_your_secret_key_here # STRIPE_WEBHOOK_SECRET=whsec_... (optional for production) ``` Add to `netcop_hub/settings.py`: ```python # Stripe Configuration STRIPE_SECRET_KEY = config('STRIPE_SECRET_KEY', default='') STRIPE_PUBLISHABLE_KEY = config('STRIPE_PUBLISHABLE_KEY', default='') STRIPE_WEBHOOK_SECRET = config('STRIPE_WEBHOOK_SECRET', default='') ``` ### 2. User Model Enhancement Update `authentication/models.py` to add wallet balance: ```python from django.contrib.auth.models import AbstractUser from django.db import models from decimal import Decimal class User(AbstractUser): email = models.EmailField(unique=True) wallet_balance = models.DecimalField( max_digits=10, decimal_places=2, default=Decimal('0.00'), help_text="User wallet balance in AED" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] def has_sufficient_balance(self, amount): """Check if user has sufficient balance for a transaction""" return self.wallet_balance >= Decimal(str(amount)) def deduct_balance(self, amount, description=""): """Deduct amount from wallet balance""" if self.has_sufficient_balance(amount): self.wallet_balance -= Decimal(str(amount)) self.save() # Create transaction record from wallet.models import WalletTransaction WalletTransaction.objects.create( user=self, amount=-Decimal(str(amount)), type='agent_usage', description=description ) return True return False def add_balance(self, amount, description=""): """Add amount to wallet balance""" self.wallet_balance += Decimal(str(amount)) self.save() # Create transaction record from wallet.models import WalletTransaction WalletTransaction.objects.create( user=self, amount=Decimal(str(amount)), type='top_up', description=description ) ``` ### 3. Wallet Models Update `wallet/models.py`: ```python from django.db import models from django.contrib.auth import get_user_model import uuid User = get_user_model() class WalletTransaction(models.Model): TRANSACTION_TYPES = [ ('top_up', 'Top Up'), ('agent_usage', 'Agent Usage'), ('refund', 'Refund'), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='wallet_transactions') amount = models.DecimalField(max_digits=10, decimal_places=2) type = models.CharField(max_length=20, choices=TRANSACTION_TYPES) description = models.TextField() stripe_payment_intent_id = models.CharField(max_length=200, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-created_at'] def __str__(self): return f"{self.user.email} - {self.amount} AED ({self.type})" ``` ### 4. Wallet Views (Payment Intents API) Create `wallet/views.py`: ```python import stripe import json from django.conf import settings from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse from django.contrib.auth.decorators import login_required from django.utils import timezone from .models import WalletTransaction from decimal import Decimal from django.contrib.auth import get_user_model User = get_user_model() stripe.api_key = settings.STRIPE_SECRET_KEY @login_required def topup(request): """Professional wallet topup page""" return render(request, "wallet/topup.html", { 'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY, 'user_balance': request.user.wallet_balance }) @login_required @csrf_exempt def create_payment_intent(request): """Create Stripe Payment Intent for wallet topup""" if request.method == "POST": try: data = json.loads(request.body) amount = int(data.get("amount")) if amount < 1: return JsonResponse({"error": "Amount must be at least 1 AED"}, status=400) # Create Payment Intent intent = stripe.PaymentIntent.create( amount=amount * 100, # Convert to fils (AED cents) currency='aed', metadata={ 'user_id': request.user.id, 'amount': amount, 'email': request.user.email }, description=f"NetCop wallet top-up for {request.user.email}" ) return JsonResponse({ 'client_secret': intent.client_secret, 'amount': amount }) except Exception as e: return JsonResponse({"error": str(e)}, status=400) return JsonResponse({"error": "Invalid request method"}, status=405) @login_required @csrf_exempt def confirm_payment(request): """Confirm payment and update wallet balance""" if request.method == "POST": try: data = json.loads(request.body) payment_intent_id = data.get("payment_intent_id") # Retrieve payment intent from Stripe intent = stripe.PaymentIntent.retrieve(payment_intent_id) if intent.status == 'succeeded': user_id = int(intent.metadata['user_id']) amount = Decimal(intent.metadata['amount']) # Verify this is the correct user if user_id != request.user.id: return JsonResponse({"error": "Unauthorized"}, status=403) # Check for duplicate processing existing_transaction = WalletTransaction.objects.filter( stripe_payment_intent_id=payment_intent_id ).first() if not existing_transaction: # Update user balance using model method request.user.add_balance( amount=amount, description=f"Wallet top-up via Stripe - {amount} AED" ) # Update the transaction with Stripe ID latest_transaction = WalletTransaction.objects.filter( user=request.user, type='top_up', amount=amount ).first() if latest_transaction: latest_transaction.stripe_payment_intent_id = payment_intent_id latest_transaction.save() return JsonResponse({ "success": True, "message": f"Successfully added {amount} AED to your wallet", "new_balance": str(request.user.wallet_balance) }) else: return JsonResponse({"error": "Payment not completed"}, status=400) except Exception as e: return JsonResponse({"error": str(e)}, status=400) return JsonResponse({"error": "Invalid request method"}, status=405) @login_required def transaction_history(request): """View transaction history""" transactions = request.user.wallet_transactions.all()[:50] return render(request, "wallet/history.html", { 'transactions': transactions, 'current_balance': request.user.wallet_balance }) ``` ### 5. Wallet URLs Create `wallet/urls.py`: ```python from django.urls import path from . import views app_name = 'wallet' urlpatterns = [ path('', views.topup, name='topup'), path('create-payment-intent/', views.create_payment_intent, name='create_payment_intent'), path('confirm-payment/', views.confirm_payment, name='confirm_payment'), path('history/', views.transaction_history, name='history'), ] ``` ### 6. Professional Topup Template Create `templates/wallet/topup.html`: ```html {% extends 'base.html' %} {% block title %}Top Up Wallet - NetCop Hub{% endblock %} {% block content %}
Add funds to your wallet to use AI agents
Current Balance
{{ user_balance|floatformat:2 }} AED
Welcome, {{ user.username }}!
๐ฐ {{ user.wallet_balance|floatformat:2 }} AED {% endif %} ``` ### 8. Update Main URLs Add wallet URLs to `netcop_hub/urls.py`: ```python urlpatterns = [ path('admin/', admin.site.urls), path('auth/', include('authentication.urls')), path('wallet/', include('wallet.urls')), # Add this line # ... other URLs ] ``` --- ## ๐งช Testing Guide ### 1. Database Migration ```bash python manage.py makemigrations python manage.py migrate ``` ### 2. Test with Stripe Test Cards - **Successful payment**: `4242 4242 4242 4242` - **Requires authentication**: `4000 0025 0000 3155` - **Declined card**: `4000 0000 0000 9995` ### 3. Testing Checklist - [ ] User can access wallet topup page - [ ] Amount selection buttons work - [ ] Card form validates properly - [ ] Payment processing shows loading states - [ ] Successful payments update balance immediately - [ ] Failed payments show error messages - [ ] Balance displays in navigation - [ ] Transaction history is recorded --- ## ๐ Advanced Features (Optional) ### Agent Integration Update agent views to check wallet balance: ```python @login_required def use_agent(request, agent_slug): agent = get_object_or_404(BaseAgent, slug=agent_slug) if not request.user.has_sufficient_balance(agent.price): return JsonResponse({ 'error': f'Insufficient balance. Need {agent.price} AED.', 'redirect_url': reverse('wallet:topup') }, status=400) # Deduct balance before processing request.user.deduct_balance( amount=agent.price, description=f"Used {agent.name} agent" ) # Process agent request... ``` ### Transaction History Page Create `templates/wallet/history.html`: ```html {% extends 'base.html' %} {% block content %}Current Balance: {{ current_balance }} AED