Thwani gateway

This commit is contained in:
Flatlogic Bot 2026-01-24 06:43:23 +00:00
parent 3b66669105
commit 8ab62e0598
12 changed files with 229 additions and 35 deletions

View File

@ -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.

View File

@ -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'),
),
]

View File

@ -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
View 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}"

View File

@ -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"),

View File

@ -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})