Thwani gateway
This commit is contained in:
parent
3b66669105
commit
8ab62e0598
Binary file not shown.
@ -213,4 +213,8 @@ MESSAGE_TAGS = {
|
||||
messages.SUCCESS: 'success',
|
||||
messages.WARNING: 'warning',
|
||||
messages.ERROR: 'danger',
|
||||
}
|
||||
}
|
||||
# Thawani Payment Gateway Configuration
|
||||
THAWANI_SECRET_KEY = os.getenv('THAWANI_SECRET_KEY', 'rRQ26GcsZ60u9YREs9GfWfE9p99e91')
|
||||
THAWANI_PUBLISHABLE_KEY = os.getenv('THAWANI_PUBLISHABLE_KEY', 'HGv7H6h09Yjt99v69XatjtS80Ym669')
|
||||
THAWANI_BASE_URL = os.getenv('THAWANI_BASE_URL', 'https://uatcheckout.thawani.om')
|
||||
|
||||
Binary file not shown.
BIN
core/__pycache__/thawani.cpython-311.pyc
Normal file
BIN
core/__pycache__/thawani.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 06:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0019_transaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='payment_status',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Payment Status'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='session_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Session ID'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -400,6 +400,8 @@ class Transaction(models.Model):
|
||||
payment_method = models.CharField(_('Payment Method'), max_length=100, blank=True)
|
||||
reference_number = models.CharField(_('Reference Number'), max_length=100, blank=True)
|
||||
receipt_number = models.CharField(_('Receipt Number'), max_length=20, unique=True, blank=True)
|
||||
session_id = models.CharField(_("Session ID"), max_length=255, blank=True, null=True)
|
||||
payment_status = models.CharField(_("Payment Status"), max_length=50, blank=True, null=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
50
core/thawani.py
Normal file
50
core/thawani.py
Normal file
@ -0,0 +1,50 @@
|
||||
import requests
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ThawaniClient:
|
||||
def __init__(self):
|
||||
self.secret_key = getattr(settings, 'THAWANI_SECRET_KEY', '')
|
||||
self.publishable_key = getattr(settings, 'THAWANI_PUBLISHABLE_KEY', '')
|
||||
self.base_url = getattr(settings, 'THAWANI_BASE_URL', 'https://uatcheckout.thawani.om')
|
||||
self.headers = {
|
||||
'thawani-api-key': self.secret_key,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
def create_checkout_session(self, payload):
|
||||
"""
|
||||
Create a checkout session with Thawani.
|
||||
Payload should include: client_reference_id, products, success_url, cancel_url, metadata.
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/checkout/session"
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers=self.headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Thawani session creation failed: {e}")
|
||||
if hasattr(e, 'response') and e.response:
|
||||
logger.error(f"Thawani error response: {e.response.text}")
|
||||
return None
|
||||
|
||||
def get_checkout_session(self, session_id):
|
||||
"""
|
||||
Retrieve a checkout session to check its status.
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/checkout/session/{session_id}"
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Thawani session retrieval failed: {e}")
|
||||
return None
|
||||
|
||||
def get_payment_url(self, session_id):
|
||||
"""
|
||||
Construct the payment URL to redirect the user.
|
||||
"""
|
||||
return f"{self.base_url}/pay/{session_id}?key={self.publishable_key}"
|
||||
@ -24,6 +24,9 @@ urlpatterns = [
|
||||
path("terms-of-service/", views.terms_of_service, name="terms_of_service"),
|
||||
path("subscription-expired/", views.subscription_expired, name="subscription_expired"),
|
||||
path("subscription-renew/", views.renew_subscription, name="renew_subscription"),
|
||||
path("payment/success/", views.thawani_success, name="thawani_success"),
|
||||
path("payment/cancel/", views.thawani_cancel, name="thawani_cancel"),
|
||||
path("payment/webhook/", views.thawani_webhook, name="thawani_webhook"),
|
||||
path("financial-history/", views.financial_history, name="financial_history"),
|
||||
path("receipt/<str:receipt_number>/", views.transaction_receipt, name="transaction_receipt"),
|
||||
path("admin/financials/", views.admin_financials, name="admin_financials"),
|
||||
|
||||
180
core/views.py
180
core/views.py
@ -1,8 +1,10 @@
|
||||
from .thawani import ThawaniClient
|
||||
from datetime import timedelta
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth import login, authenticate, logout
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
from .models import Profile, Truck, Shipment, Bid, Message, OTPCode, Country, City, AppSetting, Banner, HomeSection, Transaction
|
||||
from .forms import (
|
||||
TruckForm, ShipmentForm, BidForm, UserRegistrationForm,
|
||||
@ -16,6 +18,8 @@ from .whatsapp import send_whatsapp_message
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import HttpResponse
|
||||
import json
|
||||
|
||||
def home(request):
|
||||
@ -108,17 +112,15 @@ def verify_otp_registration(request):
|
||||
profile.phone_number = registration_data['phone_number']
|
||||
profile.country_code = registration_data['country_code']
|
||||
profile.subscription_plan = registration_data.get('subscription_plan', 'NONE')
|
||||
if profile.subscription_plan != 'NONE':
|
||||
profile.is_subscription_active = True
|
||||
if profile.subscription_plan == 'MONTHLY':
|
||||
profile.subscription_expiry = timezone.now().date() + timedelta(days=30)
|
||||
elif profile.subscription_plan == 'ANNUAL':
|
||||
profile.subscription_expiry = timezone.now().date() + timedelta(days=365)
|
||||
profile.save()
|
||||
|
||||
login(request, user)
|
||||
if 'registration_data' in request.session:
|
||||
del request.session['registration_data']
|
||||
|
||||
if profile.subscription_plan != 'NONE':
|
||||
return thawani_checkout(request, profile.subscription_plan)
|
||||
|
||||
messages.success(request, _("Registration successful. Welcome!"))
|
||||
return redirect('dashboard')
|
||||
else:
|
||||
@ -486,17 +488,87 @@ def renew_subscription(request):
|
||||
if request.method == 'POST':
|
||||
form = RenewSubscriptionForm(request.POST)
|
||||
if form.is_valid():
|
||||
profile = request.user.profile
|
||||
plan = form.cleaned_data['subscription_plan']
|
||||
|
||||
# Calculate amount based on role and plan
|
||||
app_settings = AppSetting.objects.first()
|
||||
amount = 0
|
||||
if profile.role == 'SHIPPER':
|
||||
amount = app_settings.shipper_monthly_fee if plan == 'MONTHLY' else app_settings.shipper_annual_fee
|
||||
elif profile.role == 'TRUCK_OWNER':
|
||||
amount = app_settings.truck_owner_monthly_fee if plan == 'MONTHLY' else app_settings.truck_owner_annual_fee
|
||||
return thawani_checkout(request, plan)
|
||||
return redirect('subscription_expired')
|
||||
|
||||
@login_required
|
||||
def thawani_checkout(request, plan):
|
||||
profile = request.user.profile
|
||||
app_settings = AppSetting.objects.first()
|
||||
|
||||
amount = 0
|
||||
if profile.role == 'SHIPPER':
|
||||
amount = app_settings.shipper_monthly_fee if plan == 'MONTHLY' else app_settings.shipper_annual_fee
|
||||
elif profile.role == 'TRUCK_OWNER':
|
||||
amount = app_settings.truck_owner_monthly_fee if plan == 'MONTHLY' else app_settings.truck_owner_annual_fee
|
||||
|
||||
# Thawani expects amount in baisa (1 OMR = 1000 baisa)
|
||||
amount_in_baisa = int(amount * 1000)
|
||||
|
||||
client = ThawaniClient()
|
||||
|
||||
# Generate a unique reference
|
||||
transaction = Transaction.objects.create(
|
||||
user=request.user,
|
||||
amount=amount,
|
||||
transaction_type='PAYMENT',
|
||||
status='PENDING',
|
||||
description=f"Subscription: {plan}",
|
||||
payment_method="Thawani"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"client_reference_id": transaction.receipt_number,
|
||||
"products": [
|
||||
{
|
||||
"name": f"MASAR CARGO {plan} Subscription",
|
||||
"unit_amount": amount_in_baisa,
|
||||
"quantity": 1
|
||||
}
|
||||
],
|
||||
"success_url": request.build_absolute_uri(reverse('thawani_success')),
|
||||
"cancel_url": request.build_absolute_uri(reverse('thawani_cancel')),
|
||||
"metadata": {
|
||||
"plan": plan,
|
||||
"user_id": request.user.id,
|
||||
"transaction_id": transaction.id
|
||||
}
|
||||
}
|
||||
|
||||
session = client.create_checkout_session(payload)
|
||||
if session and session.get('success'):
|
||||
session_id = session['data']['session_id']
|
||||
transaction.session_id = session_id
|
||||
transaction.save()
|
||||
return redirect(client.get_payment_url(session_id))
|
||||
else:
|
||||
messages.error(request, _("Failed to initiate payment. Please try again later."))
|
||||
return redirect('dashboard')
|
||||
|
||||
@login_required
|
||||
def thawani_success(request):
|
||||
session_id = request.GET.get('session_id')
|
||||
if not session_id:
|
||||
# Fallback to looking for the last pending transaction for this user
|
||||
transaction = Transaction.objects.filter(user=request.user, status='PENDING', payment_method='Thawani').first()
|
||||
else:
|
||||
transaction = get_object_or_404(Transaction, session_id=session_id, user=request.user)
|
||||
|
||||
client = ThawaniClient()
|
||||
session_data = client.get_checkout_session(transaction.session_id)
|
||||
|
||||
if session_data and session_data.get('success'):
|
||||
payment_status = session_data['data']['payment_status']
|
||||
transaction.payment_status = payment_status
|
||||
|
||||
if payment_status == 'paid':
|
||||
transaction.status = 'COMPLETED'
|
||||
transaction.save()
|
||||
|
||||
# Activate Subscription
|
||||
profile = transaction.user.profile
|
||||
plan = session_data['data']['metadata'].get('plan', profile.subscription_plan)
|
||||
profile.subscription_plan = plan
|
||||
profile.is_subscription_active = True
|
||||
|
||||
@ -507,37 +579,77 @@ def renew_subscription(request):
|
||||
|
||||
profile.save()
|
||||
|
||||
# Create Transaction record
|
||||
Transaction.objects.create(
|
||||
user=request.user,
|
||||
amount=amount,
|
||||
transaction_type='PAYMENT',
|
||||
status='COMPLETED',
|
||||
description=f"Subscription Renewal: {plan}",
|
||||
payment_method="Online Payment"
|
||||
)
|
||||
|
||||
# Notifications
|
||||
expiry_date = profile.subscription_expiry.strftime('%Y-%m-%d')
|
||||
msg = _("Your subscription for MASAR CARGO has been successfully renewed! Your new expiry date is %(date)s. Thank you for using our service.") % {"date": expiry_date}
|
||||
|
||||
# WhatsApp
|
||||
if profile.full_phone_number:
|
||||
send_whatsapp_message(profile.full_phone_number, msg)
|
||||
|
||||
# Email
|
||||
if request.user.email:
|
||||
if transaction.user.email:
|
||||
send_mail(
|
||||
_("Subscription Renewed - MASAR CARGO"),
|
||||
_("Subscription Activated - MASAR CARGO"),
|
||||
msg,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[request.user.email],
|
||||
[transaction.user.email],
|
||||
fail_silently=True,
|
||||
)
|
||||
|
||||
messages.success(request, _("Subscription renewed successfully!"))
|
||||
|
||||
messages.success(request, _("Payment successful! Your subscription is now active."))
|
||||
return redirect('dashboard')
|
||||
return redirect('subscription_expired')
|
||||
else:
|
||||
transaction.status = 'FAILED'
|
||||
transaction.save()
|
||||
messages.error(request, _("Payment was not successful. Status: %(status)s") % {'status': payment_status})
|
||||
else:
|
||||
messages.error(request, _("Failed to verify payment status."))
|
||||
|
||||
return redirect('dashboard')
|
||||
|
||||
@login_required
|
||||
def thawani_cancel(request):
|
||||
messages.warning(request, _("Payment was cancelled."))
|
||||
return redirect('dashboard')
|
||||
|
||||
@csrf_exempt
|
||||
def thawani_webhook(request):
|
||||
"""
|
||||
Handle asynchronous status updates from Thawani.
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
event_type = data.get('event_type')
|
||||
|
||||
if event_type == 'checkout.completed':
|
||||
session_id = data['data']['session_id']
|
||||
payment_status = data['data']['payment_status']
|
||||
|
||||
transaction = Transaction.objects.filter(session_id=session_id).first()
|
||||
if transaction and transaction.status == 'PENDING':
|
||||
transaction.payment_status = payment_status
|
||||
if payment_status == 'paid':
|
||||
transaction.status = 'COMPLETED'
|
||||
transaction.save()
|
||||
|
||||
# Activate Subscription
|
||||
profile = transaction.user.profile
|
||||
plan = data['data']['metadata'].get('plan', profile.subscription_plan)
|
||||
profile.subscription_plan = plan
|
||||
profile.is_subscription_active = True
|
||||
if plan == 'MONTHLY':
|
||||
profile.subscription_expiry = timezone.now().date() + timedelta(days=30)
|
||||
elif plan == 'ANNUAL':
|
||||
profile.subscription_expiry = timezone.now().date() + timedelta(days=365)
|
||||
profile.save()
|
||||
else:
|
||||
transaction.status = 'FAILED'
|
||||
transaction.save()
|
||||
|
||||
return HttpResponse(status=200)
|
||||
except Exception as e:
|
||||
return HttpResponse(status=400)
|
||||
return HttpResponse(status=405)
|
||||
|
||||
@login_required
|
||||
def financial_history(request):
|
||||
@ -602,4 +714,4 @@ def admin_app_settings(request):
|
||||
else:
|
||||
form = AppSettingForm(instance=settings_obj)
|
||||
|
||||
return render(request, 'core/app_settings.html', {'form': form})
|
||||
return render(request, 'core/app_settings.html', {'form': form})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user