From f213aed6e731ab8528346f86df682d762b43cab5 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 25 Jan 2026 07:53:43 +0000 Subject: [PATCH] adding notifications --- config/__pycache__/settings.cpython-311.pyc | Bin 5788 -> 6626 bytes config/settings.py | 17 ++++ core/__pycache__/forms.cpython-311.pyc | Bin 11122 -> 11257 bytes core/__pycache__/models.cpython-311.pyc | Bin 10378 -> 11039 bytes .../__pycache__/payment_utils.cpython-311.pyc | Bin 0 -> 3652 bytes core/__pycache__/urls.cpython-311.pyc | Bin 1562 -> 1883 bytes core/__pycache__/views.cpython-311.pyc | Bin 7761 -> 11392 bytes .../whatsapp_utils.cpython-311.pyc | Bin 0 -> 5434 bytes core/forms.py | 6 +- ...el_payment_status_parcel_price_and_more.py | 34 +++++++ ...atus_parcel_price_and_more.cpython-311.pyc | Bin 0 -> 1884 bytes core/models.py | 14 ++- core/payment_utils.py | 66 +++++++++++++ core/templates/core/driver_dashboard.html | 7 +- core/templates/core/shipper_dashboard.html | 16 ++- core/urls.py | 9 +- core/views.py | 73 +++++++++++++- core/whatsapp_utils.py | 92 ++++++++++++++++++ 18 files changed, 322 insertions(+), 12 deletions(-) create mode 100644 core/__pycache__/payment_utils.cpython-311.pyc create mode 100644 core/__pycache__/whatsapp_utils.cpython-311.pyc create mode 100644 core/migrations/0004_parcel_payment_status_parcel_price_and_more.py create mode 100644 core/migrations/__pycache__/0004_parcel_payment_status_parcel_price_and_more.cpython-311.pyc create mode 100644 core/payment_utils.py create mode 100644 core/whatsapp_utils.py diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 2aa9bed520adad1ffee4c93792b5f3d30441d19d..aaa84edcb395ef3bc51d3a072e02ea0e4c967a2c 100644 GIT binary patch delta 887 zcmbQE`^cDYIWI340}xC*U79H&F_BM#Nr7#n#v_(`CWchzEU*+vI>jx;eHjx2!)hRg zfG7hr6&@*`XetcRRCuL$qp2`THA?Zx2Af&LoZ=g0oX3>n7iA38SC4GDe+n;}MiVrn z129yWqNxbPP+^9qA}GZkVasF|7DHk4l;9|fl#nRPl+Y-v6y{(C&9KcftjDR$*p>p zoXoP+Dy58)l7eC@ef{K&)a2~^(h|LrjKuQ9yiC3PT>Zp?O#L#$Dpi=8(!>(n%5DjS zdpL#!I|c;6JtPPh3GndubB*_Oxg`S=Qj2#A4fgbN4GxZXbawU+^$P(>Bg}C119{J7 z@*{pDM(fR@0wPR?Mee|4Q{)LEynzI20EPzn6#0Rq{6RzjhzOjlCKSb03uH0^aq)@G zT|!@&>)9BDWhN+1(Yzp|dqLm-qENsUp@0Vd8&b+Ml;&t&P_eyWAAeCQ;fhqk1qSgO z{1P2SJ(V-$R)}5XH@L!Y(7^M6nSX-WC1%MB%#t_6tQ!0q{2z$fHu!&FV-S&@U^K<@ zf~@`qp^L&MSA;D6wRr;py_@>=%T#O x6?vZv3^F%>&VuR}zbI^ZMcDGXu>B=r`-{SkSA-o8gk2K$1{(N*YjVAqEC3%a^1T26 delta 115 zcmaE4JV%#rIWI340}wbKEzOh@oyaG_^nqog#v>M?6t^gY6!$2@6ptvQ6y{(CP0!6f zY{$4Jn+X{)8gI@N5@DLWQ6!wJ5U7_Ch>N*43yFSVmg8U$GrhndbVF1DM2g8?U=W6& IB0-=k0QjUGX8-^I diff --git a/config/settings.py b/config/settings.py index abd7cb1..c276bb9 100644 --- a/config/settings.py +++ b/config/settings.py @@ -185,6 +185,23 @@ CONTACT_EMAIL_TO = [ # When both TLS and SSL flags are enabled, prefer SSL explicitly if EMAIL_USE_SSL: EMAIL_USE_TLS = False + +# Thawani Payment Settings +THAWANI_API_KEY = os.getenv("THAWANI_API_KEY", "rRQ26GcsZ60u9YCD9As60reHscS3Jt") # Placeholder Test Key +THAWANI_PUBLISHABLE_KEY = os.getenv("THAWANI_PUBLISHABLE_KEY", "HGvTMLsnssOfssSshvSOfssOfsSshv") # Placeholder +THAWANI_MODE = os.getenv("THAWANI_MODE", "test") # 'test' or 'live' + +if THAWANI_MODE == 'live': + THAWANI_API_URL = "https://checkout.thawani.om/api/v1" +else: + THAWANI_API_URL = "https://uatcheckout.thawani.om/api/v1" + +# WhatsApp Notification Settings +WHATSAPP_API_KEY = os.getenv("WHATSAPP_API_KEY", "") +WHATSAPP_PHONE_ID = os.getenv("WHATSAPP_PHONE_ID", "") +WHATSAPP_BUSINESS_ACCOUNT_ID = os.getenv("WHATSAPP_BUSINESS_ACCOUNT_ID", "") +WHATSAPP_ENABLED = os.getenv("WHATSAPP_ENABLED", "false").lower() == "true" + # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 64e66ae35548aba2abc94e8cb12199e8d2935115..becc8759334337b4ad5874b8b9ad1e0baad6a046 100644 GIT binary patch delta 632 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 literal 0 HcmV?d00001 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 125f3bdf00d23d7efdcaf7f56836a55f77f04358..9d3522b6b7592f98987f45562c816bb5c1647abd 100644 GIT binary patch delta 575 zcmbQmbDPg^IWI340}vcNRhoI0g@NHQhyw#cP{!vnCI*J-3@HpLj5!Rsj8Tk?3@J=0 z%sEWC%u&ohHUp5&0%WsJ^b2HUow&f8k#*x62gb<;jI8`}smxh0a~M+OCo3?D@+$zv zgkhGXD1yXXlz?I)a53dIY|EG#7*+!@1f=stiKeIoGia*51PN#|-eM_8EXjDu2;s1n zWu}%FZw_S2Wvs7KEJ&=(P0cIO&&2)4wGElZuC` ztrCD~DlSbEGgmselOhX^P$AP7%vW%uCPLD=o?? zE&@5fhz&%rPQJh_!E=i%v9u&39_-l3|CoJwibO$@Vjx0%vIk3mhZ2wp@>B6%Ao+or zk&*ERgUkg~bb~?o0xG(}V0-};ZGOO_%^1VRz{=U+a)DE4Md}qjmkZvp7g*vhvcz3s ziMzqV0g{lO5qd?|=z^u!1s3m%EZ$dGyrB|OGhDC87+f%Szrf;gk;UT*i^m65VSc6t LZV)U|0J;zWRl1-u delta 288 zcmcc3H;cz_IWI340}ybWD$U%(%)sy%#DM{RDC09169dC^h7^Vr#vF!R#wbQc5SuB7 zDVI5l1;}OwvRQ#_=81lRj4Trucr&tWeB;2Vn$8_1lp+_*peg?nWRfQ1EtZ1Bk_;ft zT9%nwUi^{~D7twsQ!b;BpQi9F?v$*=y!3p%(xRN=BIe2aStP`YSb!>SaV3_PWWBJoovtQ>md*1fXpZ^0Fob=85tRGFvwg$MK>6XFQB3u48j*s(dH|x+Kl|t PeEdue+#pya3p5k};*dsI diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index a02e5b311c4f1f9061891997472eddc16a70e9b7..4510127edb77d899c7d6be329fc0908f34752681 100644 GIT binary patch delta 4851 zcmbVPYj6|S72aLFR@U2+Wg*MxE;uz61hz60BY4k)Ga{uK1A&VYV||ZF3|@H zc8i+;dqh9rR(=v)h)_=M19#4elyzeT{5S2UyNA`Ywxl;I3dImPp4? zh(&$)CG&27L7$Gq#Sm98X2{7h1$)UP_>bm*3FP`=9sb-rP}N8_u+7G#a0(R-iF)w< zik<;-w1O#(QZw;HtY9h{8EpqLdW^tsfQ%6U4h(~y8Fdg2fiA-R5B^2P_x$6Na-c^E zOsRpX+kxmxAS#QelmJo#2#;H40^2|`$^w)I*^`{~%XG94{b*I4`qS2gRz=WHR?g|FEr*(PkTw;Fpv60Eb$-e#mKw&SS% z+j;WlLT+RuOkxT2I00f*w1m>e(Je4tak zYo{SpF^YQeW0ie1)Y6Z@0`w?ANLS$EB6U5q1Ao7=JMaA8EKlr&tORxvAU>>FzJ|<( z0g7fWn9~syjU~eHxOIyQAh5yK0pj`*cvER9tSvxeATvSaeuGEcqUEWlW!nkGc0#qC zz#qBmiMb6mSNarvP}K)zeef>6?QX02W;>>F!1E+~4yQbE_7MKWvlk!rIRh*sRl_ri zV~(7NjvOP^;BK=YZ*jTUT70Z3=sXMz?X=X{`tc8{f(^v1AqQGF2X$ELmsDyq{#}*7 zd)+*6&Y~+58?C!?NUu5N5keKP!@H@D<}F(`EkZ&KYsmv019lw#nV;Z6?<{q>hGnl} zZB?zUvbFUte&5@s1@YJV(s2&gdfL1qd!5sq3AUo+EGL=p7gnGCtS-mkl);Pt&IRyq zJ$ApS&vA3a8R>YPd6Qd&*;&0N=8-H#4qQY-G36{V&+0)BE$mv{wl9@Uhyv78A)bs&ad01@s7R+UmWV|% zF+oBLLS!})PYMZOqfS~TLX{8tF$s-A<#aw9Pl(~jbS9O^f-h!K9C?95k6WnLsu2|I zSoGOcRtgu7Sg4e=uAxLA8Gt5dAGOWVp*<6OMu)?D#)iYko;*bQjdUUsjTNd(oUvnj z4-JnG?b(|@G!PycnACFq#ySwpRGCXWR zvp;~=H#mt2^TdXbmS`)3Vj%Qg_-96-$SpB z+I=r1E+&=^tX6KiUD>fx+40_l+&idjIiPMipi~a2l|yR`bBG<{;BuDT$NiIB2FYEK zNG|sn#+@JHmPS@xzT2+$6<7Ov2jp+f z^|rr%#ovGP(Oc7se^~VogF1QNexeniJT|3GH2hN}4oXk6dy8z1dck_fQGLa#IGR*P zlgu~W<((Jpm(IM+%E1XmIIarE6@F6XCuM$;2wD_Jqv~js`9`e@f>p~ytAdbSzw~qX zjp|46a`omBZes*O?jq^8IHYo}f|C*pyRskfqbKrEF|X{A(`oyjrg9Ws+xUZ zYeUX}W^xAn7U#wNW@kV&JWCTRIxIpa3!6%N=XA?FK4l1cp`N%)L{LDd?S~CjU91r* za-B07l&aPR=4`kYo@xSCJ(QVyE`J`{N%JU*f(#>ks+Q7Ah3Og7OV zOs29(D0M7CJOer4j1Y-R&}nxr?kTUJeJEk4V@VMjc_D>_nFw^2BIJj>mLOQr5wO9xk- z)vsD!b6j>TTNG!9>g-q=zEce2Jc(o5LB)1ZwH>@ah~56%?yePg*9R4fdxz@Yv2^HD zlY5OZ8|K(6*4w_;6<@0ydQ|cCtG@o5Gb_IRvTy&Y&`iRtc0-s!<6;(;jSAnQ@+~so z0^ye1+J(YUG$b5=S(CcXlazVidHi8 z^i3A@@#IWuy_})EuTs;e)<8SA zUDa=w_1o_}a5DUfH@Xic$l~=~3-$<-;?YDbEXE}8V3Y*@RU$)zaEU2)+r5gVPqp;P z`aZPn#<^-UYfJ+h9mbb@RWs|(>>$KO0($`p#?z4mwET#KYeAoYuSclx^vi*)E>z5> z-~%=Zl_9h7Q`1iTf1QcWfb_fAvT2&#hR;?18^`_50}DXY;`$WK1`z^;Qns#(lL(Y` zK@zzoil8i!Eb6g52Sj}J9X1pcPmM>Vu zIry+nb&_&Nt9ZdyENtPd6bBlu`6a1nDmjB_y|$k@8IdxPbh;=;KCTcAW!mx6zBWW? z?-~+;Kv^aewANk>gA2URi;Va_*-QJdE;-5Gq6yu{y(tWS<@P$0lr+@m-n#4 z>>5Mxo{r(1=j+xuO6q`{HO|2{tT6y(9w7uN%Su=PWfma>D9gzq#&dg delta 1785 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 literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 5d7afec..6893819 100644 --- a/core/models.py +++ b/core/models.py @@ -76,12 +76,19 @@ class Parcel(models.Model): ('cancelled', _('Cancelled')), ) + PAYMENT_STATUS_CHOICES = ( + ('pending', _('Pending')), + ('paid', _('Paid')), + ('failed', _('Failed')), + ) + tracking_number = models.CharField(_('Tracking Number'), max_length=20, unique=True, blank=True) shipper = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_parcels', verbose_name=_('Shipper')) carrier = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='carried_parcels', verbose_name=_('Carrier')) description = models.TextField(_('Description')) weight = models.DecimalField(_('Weight (kg)'), max_digits=5, decimal_places=2, help_text=_("Weight in kg")) + price = models.DecimalField(_('Price (OMR)'), max_digits=10, decimal_places=3, default=0.000) # Pickup Location pickup_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, related_name='pickup_parcels', verbose_name=_('Pickup Country')) @@ -92,13 +99,16 @@ class Parcel(models.Model): # Delivery Location delivery_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_parcels', verbose_name=_('Delivery Country')) delivery_governate = models.ForeignKey(Governate, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_parcels', verbose_name=_('Delivery Governate')) - delivery_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_parcels', verbose_name=_('Delivery City')) + delivery_city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_city_parcels', verbose_name=_('Delivery City')) delivery_address = models.CharField(_('Delivery Address'), max_length=255) receiver_name = models.CharField(_('Receiver Name'), max_length=100) receiver_phone = models.CharField(_('Receiver Phone'), max_length=20) status = models.CharField(_('Status'), max_length=20, choices=STATUS_CHOICES, default='pending') + payment_status = models.CharField(_('Payment Status'), max_length=20, choices=PAYMENT_STATUS_CHOICES, default='pending') + thawani_session_id = models.CharField(_('Thawani Session ID'), max_length=255, blank=True, null=True) + created_at = models.DateTimeField(_('Created At'), auto_now_add=True) updated_at = models.DateTimeField(_('Updated At'), auto_now=True) @@ -112,4 +122,4 @@ class Parcel(models.Model): class Meta: verbose_name = _('Parcel') - verbose_name_plural = _('Parcels') \ No newline at end of file + verbose_name_plural = _('Parcels') diff --git a/core/payment_utils.py b/core/payment_utils.py new file mode 100644 index 0000000..31bd098 --- /dev/null +++ b/core/payment_utils.py @@ -0,0 +1,66 @@ +import requests +from django.conf import settings +import logging + +logger = logging.getLogger(__name__) + +class ThawaniPay: + def __init__(self): + self.api_key = settings.THAWANI_API_KEY + self.base_url = settings.THAWANI_API_URL + self.headers = { + "thawani-api-key": self.api_key, + "Content-Type": "application/json" + } + + def create_checkout_session(self, parcel, success_url, cancel_url): + endpoint = f"{self.base_url}/checkout/session" + + # Thawani expects price in baiza (1 OMR = 1000 baiza) + # We need to convert Decimal price to integer baiza + amount_baiza = int(parcel.price * 1000) + + payload = { + "client_reference_id": str(parcel.tracking_number), + "mode": "payment", + "products": [ + { + "name": f"Shipping for Parcel {parcel.tracking_number}", + "unit_amount": amount_baiza, + "quantity": 1 + } + ], + "success_url": success_url, + "cancel_url": cancel_url, + "metadata": { + "parcel_id": parcel.id, + "customer_name": parcel.shipper.get_full_name() or parcel.shipper.username, + "customer_phone": parcel.shipper.profile.phone_number + } + } + + try: + response = requests.post(endpoint, json=payload, headers=self.headers) + response.raise_for_status() + data = response.json() + if data.get("success"): + return data["data"]["session_id"] + else: + logger.error(f"Thawani Error: {data.get('description')}") + return None + except Exception as e: + logger.error(f"Thawani Request Failed: {str(e)}") + return None + + def get_checkout_session(self, session_id): + endpoint = f"{self.base_url}/checkout/session/{session_id}" + try: + response = requests.get(endpoint, headers=self.headers) + response.raise_for_status() + data = response.json() + if data.get("success"): + return data["data"] + return None + except Exception as e: + logger.error(f"Thawani Check Failed: {str(e)}") + return None diff --git a/core/templates/core/driver_dashboard.html b/core/templates/core/driver_dashboard.html index 7639897..f332690 100644 --- a/core/templates/core/driver_dashboard.html +++ b/core/templates/core/driver_dashboard.html @@ -26,7 +26,10 @@
{{ parcel.description|truncatechars:30 }}

{% trans "Pickup" %}: {{ parcel.pickup_address }}

{% trans "Delivery" %}: {{ parcel.delivery_address }}

-

{% trans "Weight" %}: {{ parcel.weight }} kg

+
+ {% trans "Weight" %}: {{ parcel.weight }} kg + {{ parcel.price }} OMR +
{% csrf_token %} @@ -79,4 +82,4 @@ -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/shipper_dashboard.html b/core/templates/core/shipper_dashboard.html index 9eb4ad4..32d54ce 100644 --- a/core/templates/core/shipper_dashboard.html +++ b/core/templates/core/shipper_dashboard.html @@ -23,6 +23,20 @@
{{ parcel.description|truncatechars:30 }}

{% trans "From" %}: {{ parcel.pickup_address }}

{% trans "To" %}: {{ parcel.delivery_address }}

+ +
+ {{ parcel.price }} OMR + + {{ parcel.get_payment_status_display }} + +
+ + {% if parcel.payment_status == 'pending' %} + + {% trans "Pay Now" %} + + {% endif %} +

{% trans "Receiver" %}: {{ parcel.receiver_name }}

{% trans "Carrier" %}: {% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting for pickup" %}{% endif %}

@@ -38,4 +52,4 @@ {% endif %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 3b347d2..50c776d 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from django.contrib.auth import views as auth_views from . import views +from django.contrib.auth import views as auth_views urlpatterns = [ path('', views.index, name='index'), @@ -16,4 +16,9 @@ urlpatterns = [ # AJAX for locations path('ajax/get-governates/', views.get_governates, name='get_governates'), path('ajax/get-cities/', views.get_cities, name='get_cities'), -] + + # Thawani Payment + path('payment/initiate//', views.initiate_payment, name='initiate_payment'), + path('payment/success/', views.payment_success, name='payment_success'), + path('payment/cancel/', views.payment_cancel, name='payment_cancel'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 09b0517..f174adf 100644 --- a/core/views.py +++ b/core/views.py @@ -7,6 +7,15 @@ from .forms import UserRegistrationForm, ParcelForm from django.utils.translation import gettext_lazy as _ from django.contrib import messages from django.http import JsonResponse +from django.urls import reverse +from .payment_utils import ThawaniPay +from django.conf import settings +from .whatsapp_utils import ( + notify_shipment_created, + notify_payment_received, + notify_driver_assigned, + notify_status_change +) def index(request): tracking_id = request.GET.get('tracking_id') @@ -45,7 +54,7 @@ def dashboard(request): return render(request, 'core/shipper_dashboard.html', {'parcels': parcels}) else: # Car Owner view - available_parcels = Parcel.objects.filter(status='pending').order_by('-created_at') + available_parcels = Parcel.objects.filter(status='pending', payment_status='paid').order_by('-created_at') my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status='delivered').order_by('-created_at') return render(request, 'core/driver_dashboard.html', { 'available_parcels': available_parcels, @@ -65,6 +74,10 @@ def shipment_request(request): parcel = form.save(commit=False) parcel.shipper = request.user parcel.save() + + # WhatsApp Notification + notify_shipment_created(parcel) + messages.success(request, _("Shipment requested successfully! Tracking ID: ") + parcel.tracking_number) return redirect('dashboard') else: @@ -78,10 +91,14 @@ def accept_parcel(request, parcel_id): messages.error(request, _("Only car owners can accept shipments.")) return redirect('dashboard') - parcel = get_object_or_404(Parcel, id=parcel_id, status='pending') + parcel = get_object_or_404(Parcel, id=parcel_id, status='pending', payment_status='paid') parcel.carrier = request.user parcel.status = 'picked_up' parcel.save() + + # WhatsApp Notification + notify_driver_assigned(parcel) + messages.success(request, _("You have accepted the shipment!")) return redirect('dashboard') @@ -93,9 +110,59 @@ def update_status(request, parcel_id): if new_status in dict(Parcel.STATUS_CHOICES): parcel.status = new_status parcel.save() + + # WhatsApp Notification + notify_status_change(parcel) + messages.success(request, _("Status updated successfully!")) return redirect('dashboard') +@login_required +def initiate_payment(request, parcel_id): + parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user, payment_status='pending') + + thawani = ThawaniPay() + success_url = request.build_absolute_uri(reverse('payment_success')) + f"?session_id={{CHECKOUT_SESSION_ID}}&parcel_id={parcel.id}" + cancel_url = request.build_absolute_uri(reverse('payment_cancel')) + f"?parcel_id={parcel.id}" + + session_id = thawani.create_checkout_session(parcel, success_url, cancel_url) + + if session_id: + parcel.thawani_session_id = session_id + parcel.save() + checkout_url = f"{settings.THAWANI_API_URL.replace('/api/v1', '')}/pay/{session_id}?key={settings.THAWANI_PUBLISHABLE_KEY}" + return redirect(checkout_url) + else: + messages.error(request, _("Could not initiate payment. Please try again later.")) + return redirect('dashboard') + +@login_required +def payment_success(request): + session_id = request.GET.get('session_id') + parcel_id = request.GET.get('parcel_id') + parcel = get_object_or_404(Parcel, id=parcel_id, shipper=request.user) + + thawani = ThawaniPay() + session_data = thawani.get_checkout_session(session_id) + + if session_data and session_data.get('payment_status') == 'paid': + parcel.payment_status = 'paid' + parcel.save() + + # WhatsApp Notification + notify_payment_received(parcel) + + messages.success(request, _("Payment successful! Your shipment is now active.")) + else: + messages.warning(request, _("Payment status is pending or failed. Please check your dashboard.")) + + return redirect('dashboard') + +@login_required +def payment_cancel(request): + messages.info(request, _("Payment was cancelled.")) + return redirect('dashboard') + def article_detail(request): return render(request, 'core/article_detail.html') @@ -107,4 +174,4 @@ def get_governates(request): def get_cities(request): governate_id = request.GET.get('governate_id') cities = City.objects.filter(governate_id=governate_id).values('id', 'name') - return JsonResponse(list(cities), safe=False) + return JsonResponse(list(cities), safe=False) \ No newline at end of file diff --git a/core/whatsapp_utils.py b/core/whatsapp_utils.py new file mode 100644 index 0000000..d89b48e --- /dev/null +++ b/core/whatsapp_utils.py @@ -0,0 +1,92 @@ +import requests +import logging +from django.conf import settings + +logger = logging.getLogger(__name__) + +def send_whatsapp_message(phone_number, message): + """ + Sends a WhatsApp message using the configured gateway. + This implementation assumes Meta WhatsApp Business API (Graph API). + """ + if not settings.WHATSAPP_ENABLED: + logger.info("WhatsApp notifications are disabled.") + return False + + if not settings.WHATSAPP_API_KEY or not settings.WHATSAPP_PHONE_ID: + logger.warning("WhatsApp API configuration is missing.") + return False + + # Normalize phone number (ensure it has country code and no +) + clean_phone = "".join(filter(str.isdigit, str(phone_number))) + + url = f"https://graph.facebook.com/v17.0/{settings.WHATSAPP_PHONE_ID}/messages" + + headers = { + "Authorization": f"Bearer {settings.WHATSAPP_API_KEY}", + "Content-Type": "application/json", + } + + payload = { + "messaging_product": "whatsapp", + "to": clean_phone, + "type": "text", + "text": {"body": message} + } + + try: + response = requests.post(url, headers=headers, json=payload, timeout=10) + response_data = response.json() + + if response.status_code == 200: + logger.info(f"WhatsApp message sent to {clean_phone}") + return True + else: + logger.error(f"WhatsApp API error: {response.status_code} - {response_data}") + return False + except Exception as e: + logger.error(f"Failed to send WhatsApp message: {str(e)}") + return False + +def notify_shipment_created(parcel): + """Notifies the shipper that the shipment request was received.""" + shipper_name = parcel.shipper.get_full_name() or parcel.shipper.username + message = f"""Hello {shipper_name}, + +Your shipment request for '{parcel.description}' has been received. +Tracking Number: {parcel.tracking_number} +Status: {parcel.get_status_display()} + +Please proceed to payment to make it visible to drivers.""" + return send_whatsapp_message(parcel.shipper.profile.phone_number, message) + +def notify_payment_received(parcel): + """Notifies the shipper and receiver about successful payment.""" + # Notify Shipper + shipper_name = parcel.shipper.get_full_name() or parcel.shipper.username + shipper_msg = f"""Payment successful for shipment {parcel.tracking_number}. +Your shipment is now visible to available drivers.""" + send_whatsapp_message(parcel.shipper.profile.phone_number, shipper_msg) + + # Notify Receiver + receiver_msg = f"""Hello {parcel.receiver_name}, + +A shipment is coming your way from {shipper_name}. +Tracking Number: {parcel.tracking_number} +Status: {parcel.get_status_display()}""" + send_whatsapp_message(parcel.receiver_phone, receiver_msg) + +def notify_driver_assigned(parcel): + """Notifies the shipper and receiver that a driver has picked up the parcel.""" + driver_name = parcel.carrier.get_full_name() or parcel.carrier.username + msg = f"""Shipment {parcel.tracking_number} has been picked up by {driver_name}. +Status: {parcel.get_status_display()}""" + send_whatsapp_message(parcel.shipper.profile.phone_number, msg) + send_whatsapp_message(parcel.receiver_phone, msg) + +def notify_status_change(parcel): + """Notifies parties about general status updates (In Transit, Delivered).""" + msg = f"""Update for shipment {parcel.tracking_number}: +New Status: {parcel.get_status_display()}""" + send_whatsapp_message(parcel.shipper.profile.phone_number, msg) + send_whatsapp_message(parcel.receiver_phone, msg)