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.SUCCESS: 'success',
|
||||||
messages.WARNING: 'warning',
|
messages.WARNING: 'warning',
|
||||||
messages.ERROR: 'danger',
|
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)
|
payment_method = models.CharField(_('Payment Method'), max_length=100, blank=True)
|
||||||
reference_number = models.CharField(_('Reference Number'), 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)
|
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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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("terms-of-service/", views.terms_of_service, name="terms_of_service"),
|
||||||
path("subscription-expired/", views.subscription_expired, name="subscription_expired"),
|
path("subscription-expired/", views.subscription_expired, name="subscription_expired"),
|
||||||
path("subscription-renew/", views.renew_subscription, name="renew_subscription"),
|
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("financial-history/", views.financial_history, name="financial_history"),
|
||||||
path("receipt/<str:receipt_number>/", views.transaction_receipt, name="transaction_receipt"),
|
path("receipt/<str:receipt_number>/", views.transaction_receipt, name="transaction_receipt"),
|
||||||
path("admin/financials/", views.admin_financials, name="admin_financials"),
|
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 datetime import timedelta
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth import login, authenticate, logout
|
from django.contrib.auth import login, authenticate, logout
|
||||||
from django.utils import timezone
|
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 .models import Profile, Truck, Shipment, Bid, Message, OTPCode, Country, City, AppSetting, Banner, HomeSection, Transaction
|
||||||
from .forms import (
|
from .forms import (
|
||||||
TruckForm, ShipmentForm, BidForm, UserRegistrationForm,
|
TruckForm, ShipmentForm, BidForm, UserRegistrationForm,
|
||||||
@ -16,6 +18,8 @@ from .whatsapp import send_whatsapp_message
|
|||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.http import HttpResponse
|
||||||
import json
|
import json
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
@ -108,17 +112,15 @@ def verify_otp_registration(request):
|
|||||||
profile.phone_number = registration_data['phone_number']
|
profile.phone_number = registration_data['phone_number']
|
||||||
profile.country_code = registration_data['country_code']
|
profile.country_code = registration_data['country_code']
|
||||||
profile.subscription_plan = registration_data.get('subscription_plan', 'NONE')
|
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()
|
profile.save()
|
||||||
|
|
||||||
login(request, user)
|
login(request, user)
|
||||||
if 'registration_data' in request.session:
|
if 'registration_data' in request.session:
|
||||||
del request.session['registration_data']
|
del request.session['registration_data']
|
||||||
|
|
||||||
|
if profile.subscription_plan != 'NONE':
|
||||||
|
return thawani_checkout(request, profile.subscription_plan)
|
||||||
|
|
||||||
messages.success(request, _("Registration successful. Welcome!"))
|
messages.success(request, _("Registration successful. Welcome!"))
|
||||||
return redirect('dashboard')
|
return redirect('dashboard')
|
||||||
else:
|
else:
|
||||||
@ -486,17 +488,87 @@ def renew_subscription(request):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = RenewSubscriptionForm(request.POST)
|
form = RenewSubscriptionForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
profile = request.user.profile
|
|
||||||
plan = form.cleaned_data['subscription_plan']
|
plan = form.cleaned_data['subscription_plan']
|
||||||
|
return thawani_checkout(request, plan)
|
||||||
# Calculate amount based on role and plan
|
return redirect('subscription_expired')
|
||||||
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
|
|
||||||
|
|
||||||
|
@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.subscription_plan = plan
|
||||||
profile.is_subscription_active = True
|
profile.is_subscription_active = True
|
||||||
|
|
||||||
@ -507,37 +579,77 @@ def renew_subscription(request):
|
|||||||
|
|
||||||
profile.save()
|
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
|
# Notifications
|
||||||
expiry_date = profile.subscription_expiry.strftime('%Y-%m-%d')
|
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}
|
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:
|
if profile.full_phone_number:
|
||||||
send_whatsapp_message(profile.full_phone_number, msg)
|
send_whatsapp_message(profile.full_phone_number, msg)
|
||||||
|
|
||||||
# Email
|
if transaction.user.email:
|
||||||
if request.user.email:
|
|
||||||
send_mail(
|
send_mail(
|
||||||
_("Subscription Renewed - MASAR CARGO"),
|
_("Subscription Activated - MASAR CARGO"),
|
||||||
msg,
|
msg,
|
||||||
settings.DEFAULT_FROM_EMAIL,
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
[request.user.email],
|
[transaction.user.email],
|
||||||
fail_silently=True,
|
fail_silently=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
messages.success(request, _("Subscription renewed successfully!"))
|
messages.success(request, _("Payment successful! Your subscription is now active."))
|
||||||
return redirect('dashboard')
|
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
|
@login_required
|
||||||
def financial_history(request):
|
def financial_history(request):
|
||||||
@ -602,4 +714,4 @@ def admin_app_settings(request):
|
|||||||
else:
|
else:
|
||||||
form = AppSettingForm(instance=settings_obj)
|
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