From 8ab62e059836a64353bf19977b010e62dade1d69 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 24 Jan 2026 06:43:23 +0000 Subject: [PATCH] Thwani gateway --- config/__pycache__/settings.cpython-311.pyc | Bin 6569 -> 6915 bytes config/settings.py | 6 +- core/__pycache__/models.cpython-311.pyc | Bin 31877 -> 32172 bytes core/__pycache__/thawani.cpython-311.pyc | Bin 0 -> 3569 bytes core/__pycache__/urls.cpython-311.pyc | Bin 3049 -> 3350 bytes core/__pycache__/views.cpython-311.pyc | Bin 34163 -> 39682 bytes ...n_payment_status_transaction_session_id.py | 23 +++ ...tus_transaction_session_id.cpython-311.pyc | Bin 0 -> 1057 bytes core/models.py | 2 + core/thawani.py | 50 +++++ core/urls.py | 3 + core/views.py | 180 ++++++++++++++---- 12 files changed, 229 insertions(+), 35 deletions(-) create mode 100644 core/__pycache__/thawani.cpython-311.pyc create mode 100644 core/migrations/0020_transaction_payment_status_transaction_session_id.py create mode 100644 core/migrations/__pycache__/0020_transaction_payment_status_transaction_session_id.cpython-311.pyc create mode 100644 core/thawani.py diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 29253b3fbdcd715eb79b90baf296917e40ca8a33..abf36826ce96937b09edb8266f7131aa97184301 100644 GIT binary patch delta 466 zcmZ2!+-%0ToR^o20SI2DmSkR+n8+u=#Kg8yV-{;Y6GJL<7FY@-of4W7wv35^VKop# zK$Hubitv;OG!?FBDk4*&&{VjkFb6YeMki`BGB9wtWF_XM=PMMamXu`XZNANVk-Pqu zP>6?PxTBwEe6Xu?kZVZ1w`=4rxuT#zBQy8p;wUqNQp?C7*J4ZewD2@n%K}TwR7=BK z;xIh{p-w)Y!5)rIKCTdr9`0r49%dN^mXTQ{mX>8^mJx|1StY?129ddDW|p@EU>cnq zgI(i8gM5^#I}f5j-G55Jd0;iC-Kxx%nxjIjMF<6M$TxR~d>QY-Sgo$>RS(fI&=tLeP}R3-ZPn zOhYe;`WFlXFA4`; N5e~Y*AOJyN>i{^3eU1PC delta 100 zcmZoRTWQR!7ZU&g diff --git a/config/settings.py b/config/settings.py index bfd74de..0905d2c 100644 --- a/config/settings.py +++ b/config/settings.py @@ -213,4 +213,8 @@ MESSAGE_TAGS = { messages.SUCCESS: 'success', messages.WARNING: 'warning', messages.ERROR: 'danger', -} \ No newline at end of file +} +# 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') diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index ca66217db4b5de4685b43debc3c52c0ecdbe4f7e..4611a8f91fca6ea9fe58e2938836c5f92409f40d 100644 GIT binary patch delta 1316 zcmZ`(U2NM_6!vve_eV+8uqG|*mL^@=G^mY7#~6c!@zbu_*xGJMX&R)uZtSI@X_8&r zX^D_1Z8z-&RP9V4B>sj_5n3Ns8K#McX)luy573DUu}Db31Fy6aFJqg=J&w?5G_G|& zefK-xIljI>K6{6(-X*RtoX!T0wcDzm$sJj9U8o5}#Hsv@DjJhUPB%n#PE8qlo{F=1 zI;|Dtlq#B+YPt`TFX8*X>*N8v)Bg$25i@ecS4$e;#EIXC(-a4Lh#(M-qk=g(w8#^| z{Ntp@M()GC_*vqEg^}k-2(FLl!~y$W{f_S;a3-_^KI*DBTSs3ZjXjw9B*N1O`=OZV z7_cL~!sbfD$d-{UAovkN2qlDGj2=MPjYA(oo5fJ`nZGB#A-s>kje!>PYI4-p(T2Jo z!G*Q}V{sZq+K%E)WP6}@{Kdu&9P>DKBix6@@t5pDCc;*J970kAv_c!{14A+d2YS0< zQ|h%xEJv5l)4gomM0#Ou;zRP3#ScJA?zBHsal8rjlV3-xza9(ygzvk9a1@6ovV+JT zE=U+PJKUYz7ksz`$Iw%?4pG#i=Bd;WPonUd`Xh;%#nZLKJ;VZGpOg# zZ-v<}vtDURo8ncxtNs-`Q(3RHq=$Kh*SBW|sxsRjnd!AMoByq(c&AqPY)?$KR9jYq zRgTAbZh%udmN=zzh3)cX+-a*Ncb?}?6D71%muywKmbm1u^V}Rs?v8QcF#MU@>MAwF zR87m|bN-`!l>>8+C6-*n@*6X1PWQ)jSvNHL9{iGD+?m0ZbHu5f)8v%Sk~I1RlLjd} z5ssD`gYgf{_Y3v>j!{O#0_|kA%66HiHMS2X-QSFQsqsG*BAQ%OKQsmLd?3AQbR@StXn$1d5 zn9#RbNMoIQp>mnOXUCn delta 1014 zcmZ`&OGs2<6u#%q!?%q28q-npF*@WUGp$fDv&q78%x4Ygc$=5zq;~HeQE4H*aF=z; zHiA%5i%g8aHbO#yMD3(-F^jNOyHIE&N%fyQ3q$IM|KmH~`Of{%{qMO;KVfhsuj;3NT8bAtAQ&SkqiiLCg~CjNECP$BG4a|eh!()D1~c1qcIxso zNzWpPCtEgWZE_9KBP6a9%fXiJI#V9SBE<;=d-%BftUi~ExH8m@4X%<1YORSkY>G2O zJw9*+jWJYn6iq$H^~K~&u&JV4%Ht#`!;PK=DA#x;PD)n&$*|)#+OEH@Id}{7piMe= zm*4`0jMyn+`xj#)O^503^1S`4xJaG@_fbt+HIv;A5vuW>_Y<_Rd0zx1H1UMZ7>ekX z6iHQOxeb^7*+vK1ui$5Y9(1x@zg>4ItZ+GG@XLe2A-Nfw`|aSv`TlirS!7*-TCp~n zx1%jj%2MW2Zhp>pcsWxXTTx^pfmX_q?S`AN7A3O~ixRIKSt-(p>C@@=3Cl-jpchw1 zwzIUqNSj>kbG1-SCxK6^#t)+wxWm4UPU!0M!2#RCzj4lbzi3Cx{rHbV`%(S?lg9RC diff --git a/core/__pycache__/thawani.cpython-311.pyc b/core/__pycache__/thawani.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c035ac92e9fc77986ce6f0b6cea60496898c335 GIT binary patch literal 3569 zcmc&%O>7&-6`uX$pI0I+t0FC1)mpZq(6vYyEzrP-<&XkV|42$|*(fX&U95LU;?fkC zo*hOqLA5&MAQVBH1_qSW37{VArf?29^x#7daa$BUE(-z`77!4iz4XSyJp?)R%`SH( zSyIuWhtBRk&dhuB=J)&N@z;HQ2?XhHn3^9(5&A1D1WD{=Ui=U;o5(;0S49i{FSo!! znXmH8!h*md9$~H$*dr`G;ShQXpPv>YJo+9ogm;i3KH)(FUr!S?xOtS7{?3YXSYhv*Wf8l{A=HER~K!zMatLteA!yY`NrKwjW+Ul73&RZk^G89 z?%8!s$9df%I3J9-{=ki?s#!Cssy2uA9fw>{9|OPk0*H0AGkoUptImZtz8Gr{mpa3x z4e{ON&gd(TSDlO3|5R*`zS$Xlb3^RJPVZz!AIF^YuRBveYG>3=Mg_~hFGtUQ^pow= zxvnH+5?urWYUK@hUVwL`hx-SG@cjJ}rnQM+cbSQo9>J;mm|x@Lh=ykd?b-bn-{QbN z)P4HWSz8lY!d7^00 z#V%Wos-c)Qz1lGFB}Mn>NF{g?6I|1=Y8n@mI7XmUl%4Pm+YGH_!CO77iEy=s{Rn)XXY2E-7w|V7qSB zY~0KQ#+VR`C_()tWiH!C`oSHuz2Cq#wOz4p+0^U9O%crm=q_4BwE^u7o3K4~6L?j} zbw(Gqo4V<@D+N_&9GXGUpnURVuQL+!0bb9_Jak1%6CY!}2j>L$>@(8Cd@|26;J5W0LUp4mx{K3v`?8pN@pZV#GbMlR6-vgVPD&`MJWWX-W9KBthYDic$`xs1^rc68SJ{Pv+#K`oy?6qDMD50pK4K6wqb*fP3I7 zWu0l&5X*Dfgojg-W|$D*^7Wb$w)Gw;$uP^EV&XItBOn5t!R_pE8jun>!;}naWDLa1 zU>gL-2ay(x&yTrshuJW<_OCzy%wj#5m7M<4vtbbMGOR^>thKhLKO6k*X(v6^PEU2x zQ~w>(c!snTJNHHe- zE+#?#aIl<*76g z*f0t1=^8dBdrpm%2jhHGx&k59w__*L4=0`EEWBG4|MxT6`R)xr>%?Zs39y{YMp@`p)0VNSx(QWXhWibI zJk^y|^?pOE`Wi9-w&W}m6HH`5{04a*ypa;d@Sb3?hJ72vdiZqts04vlC3f6!I`+Ch>nU;(>e$}fD%Xo7Gs}GSv$dBv$68 z=9TCdmnJ8t78mQ^;xEZaEKkhKjE9O;@xzoPC*~!m=IGzzgQ&Knae6RKp2$ AMF0Q* diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index a0cf697b84ea884f0494b4263cbb5fb9e6293cd8..c73e35d9bca2285fe248fd4f9a7c43ef14e95dbc 100644 GIT binary patch delta 12398 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 IpMewp9}h!vpa1{> diff --git a/core/migrations/0020_transaction_payment_status_transaction_session_id.py b/core/migrations/0020_transaction_payment_status_transaction_session_id.py new file mode 100644 index 0000000..0eb5899 --- /dev/null +++ b/core/migrations/0020_transaction_payment_status_transaction_session_id.py @@ -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'), + ), + ] diff --git a/core/migrations/__pycache__/0020_transaction_payment_status_transaction_session_id.cpython-311.pyc b/core/migrations/__pycache__/0020_transaction_payment_status_transaction_session_id.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dfc66543405abcbe30bffeec262e5acf31aaf33f GIT binary patch literal 1057 zcmah{%TLrm7@ujoW!q&9G$bAbgNVkx(3N;GAtpwm(I_#&VA9K^+ZjriKGr@IJaO>g z!2^j0{sY9wAK;NcVM+EtCf+=8D}fUi{ieIiYfYSXzJ9;&{ph!9RY!1svDEt_A@o~- z{wejC<2hiq5JeRGD8LpDBum0bLeCJDJ|QYI+?4xH+ra%eLzYaHMN}&t3Uy85df>US zoqAE2a5ac1^Ao4Zujh#FBdfsM;nNvL!8S0;<%Dkro+%)m20p1eL<6o6UB^=)a&;jk~Fr%#CAl- zymAp-YuW1o3saJ$cA6z`4rig*kJnyzrIU+=w5IST6 z%#|o$X4c5UG&9*6bFwsw%@0w$nzU?(nPOz-`Ojh2>$CNX!y}i+bxC#J-%M*gRv}+O z1e1IPvT^q0D$?%cvNWb{yv%i^jepiVMN9T-!acRR+^&e+eXy$9{3<+kDO z8*ayN^C3mkj}VX(L9RGXTyVU-H{X8iv<|{&!cz}AcmVP0m)^IG4zIOZFFz&&DBw?=4g;?>Yb74 MBcV9?hqlQ37g9|c&j0`b literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 08412f8..ce891d1 100644 --- a/core/models.py +++ b/core/models.py @@ -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) diff --git a/core/thawani.py b/core/thawani.py new file mode 100644 index 0000000..fce1a15 --- /dev/null +++ b/core/thawani.py @@ -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}" diff --git a/core/urls.py b/core/urls.py index 471f4cf..28f8bc3 100644 --- a/core/urls.py +++ b/core/urls.py @@ -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//", views.transaction_receipt, name="transaction_receipt"), path("admin/financials/", views.admin_financials, name="admin_financials"), diff --git a/core/views.py b/core/views.py index 43eb66c..30a464b 100644 --- a/core/views.py +++ b/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}) \ No newline at end of file + return render(request, 'core/app_settings.html', {'form': form})