adding notifications
This commit is contained in:
parent
95e2847b94
commit
f213aed6e7
Binary file not shown.
@ -185,6 +185,23 @@ CONTACT_EMAIL_TO = [
|
|||||||
# When both TLS and SSL flags are enabled, prefer SSL explicitly
|
# When both TLS and SSL flags are enabled, prefer SSL explicitly
|
||||||
if EMAIL_USE_SSL:
|
if EMAIL_USE_SSL:
|
||||||
EMAIL_USE_TLS = False
|
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
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/payment_utils.cpython-311.pyc
Normal file
BIN
core/__pycache__/payment_utils.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/whatsapp_utils.cpython-311.pyc
Normal file
BIN
core/__pycache__/whatsapp_utils.cpython-311.pyc
Normal file
Binary file not shown.
@ -69,7 +69,7 @@ class ParcelForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Parcel
|
model = Parcel
|
||||||
fields = [
|
fields = [
|
||||||
'description', 'weight',
|
'description', 'weight', 'price',
|
||||||
'pickup_country', 'pickup_governate', 'pickup_city', 'pickup_address',
|
'pickup_country', 'pickup_governate', 'pickup_city', 'pickup_address',
|
||||||
'delivery_country', 'delivery_governate', 'delivery_city', 'delivery_address',
|
'delivery_country', 'delivery_governate', 'delivery_city', 'delivery_address',
|
||||||
'receiver_name', 'receiver_phone'
|
'receiver_name', 'receiver_phone'
|
||||||
@ -77,6 +77,7 @@ class ParcelForm(forms.ModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('What are you sending?')}),
|
'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('What are you sending?')}),
|
||||||
'weight': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
|
'weight': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
|
||||||
|
'price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
||||||
|
|
||||||
'pickup_country': forms.Select(attrs={'class': 'form-control'}),
|
'pickup_country': forms.Select(attrs={'class': 'form-control'}),
|
||||||
'pickup_governate': forms.Select(attrs={'class': 'form-control'}),
|
'pickup_governate': forms.Select(attrs={'class': 'form-control'}),
|
||||||
@ -94,6 +95,7 @@ class ParcelForm(forms.ModelForm):
|
|||||||
labels = {
|
labels = {
|
||||||
'description': _('Package Description'),
|
'description': _('Package Description'),
|
||||||
'weight': _('Weight (kg)'),
|
'weight': _('Weight (kg)'),
|
||||||
|
'price': _('Shipping Price (OMR)'),
|
||||||
'pickup_country': _('Pickup Country'),
|
'pickup_country': _('Pickup Country'),
|
||||||
'pickup_governate': _('Pickup Governate'),
|
'pickup_governate': _('Pickup Governate'),
|
||||||
'pickup_city': _('Pickup City'),
|
'pickup_city': _('Pickup City'),
|
||||||
|
|||||||
@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-01-25 07:48
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_city_country_parcel_delivery_city_parcel_pickup_city_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='parcel',
|
||||||
|
name='payment_status',
|
||||||
|
field=models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('failed', 'Failed')], default='pending', max_length=20, verbose_name='Payment Status'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='parcel',
|
||||||
|
name='price',
|
||||||
|
field=models.DecimalField(decimal_places=3, default=0.0, max_digits=10, verbose_name='Price (OMR)'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='parcel',
|
||||||
|
name='thawani_session_id',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Thawani Session ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='parcel',
|
||||||
|
name='delivery_city',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='delivery_city_parcels', to='core.city', verbose_name='Delivery City'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -76,12 +76,19 @@ class Parcel(models.Model):
|
|||||||
('cancelled', _('Cancelled')),
|
('cancelled', _('Cancelled')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PAYMENT_STATUS_CHOICES = (
|
||||||
|
('pending', _('Pending')),
|
||||||
|
('paid', _('Paid')),
|
||||||
|
('failed', _('Failed')),
|
||||||
|
)
|
||||||
|
|
||||||
tracking_number = models.CharField(_('Tracking Number'), max_length=20, unique=True, blank=True)
|
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'))
|
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'))
|
carrier = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='carried_parcels', verbose_name=_('Carrier'))
|
||||||
|
|
||||||
description = models.TextField(_('Description'))
|
description = models.TextField(_('Description'))
|
||||||
weight = models.DecimalField(_('Weight (kg)'), max_digits=5, decimal_places=2, help_text=_("Weight in kg"))
|
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 Location
|
||||||
pickup_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, related_name='pickup_parcels', verbose_name=_('Pickup Country'))
|
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 Location
|
||||||
delivery_country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True, blank=True, related_name='delivery_parcels', verbose_name=_('Delivery Country'))
|
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_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)
|
delivery_address = models.CharField(_('Delivery Address'), max_length=255)
|
||||||
|
|
||||||
receiver_name = models.CharField(_('Receiver Name'), max_length=100)
|
receiver_name = models.CharField(_('Receiver Name'), max_length=100)
|
||||||
receiver_phone = models.CharField(_('Receiver Phone'), max_length=20)
|
receiver_phone = models.CharField(_('Receiver Phone'), max_length=20)
|
||||||
|
|
||||||
status = models.CharField(_('Status'), max_length=20, choices=STATUS_CHOICES, default='pending')
|
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)
|
created_at = models.DateTimeField(_('Created At'), auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(_('Updated At'), auto_now=True)
|
updated_at = models.DateTimeField(_('Updated At'), auto_now=True)
|
||||||
|
|
||||||
|
|||||||
66
core/payment_utils.py
Normal file
66
core/payment_utils.py
Normal file
@ -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
|
||||||
@ -26,7 +26,10 @@
|
|||||||
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
|
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
|
||||||
<p class="card-text mb-1 small"><strong>{% trans "Pickup" %}:</strong> {{ parcel.pickup_address }}</p>
|
<p class="card-text mb-1 small"><strong>{% trans "Pickup" %}:</strong> {{ parcel.pickup_address }}</p>
|
||||||
<p class="card-text mb-3 small"><strong>{% trans "Delivery" %}:</strong> {{ parcel.delivery_address }}</p>
|
<p class="card-text mb-3 small"><strong>{% trans "Delivery" %}:</strong> {{ parcel.delivery_address }}</p>
|
||||||
<p class="card-text mb-3 small text-muted"><strong>{% trans "Weight" %}:</strong> {{ parcel.weight }} kg</p>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="text-muted small"><strong>{% trans "Weight" %}:</strong> {{ parcel.weight }} kg</span>
|
||||||
|
<span class="text-primary fw-bold">{{ parcel.price }} OMR</span>
|
||||||
|
</div>
|
||||||
<form action="{% url 'accept_parcel' parcel.id %}" method="POST">
|
<form action="{% url 'accept_parcel' parcel.id %}" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn btn-masarx-primary w-100">{% trans "Accept Shipment" %}</button>
|
<button type="submit" class="btn btn-masarx-primary w-100">{% trans "Accept Shipment" %}</button>
|
||||||
|
|||||||
@ -23,6 +23,20 @@
|
|||||||
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
|
<h5 class="card-title">{{ parcel.description|truncatechars:30 }}</h5>
|
||||||
<p class="card-text mb-1 small text-muted"><i class="fas fa-map-marker-alt"></i> <strong>{% trans "From" %}:</strong> {{ parcel.pickup_address }}</p>
|
<p class="card-text mb-1 small text-muted"><i class="fas fa-map-marker-alt"></i> <strong>{% trans "From" %}:</strong> {{ parcel.pickup_address }}</p>
|
||||||
<p class="card-text mb-3 small text-muted"><i class="fas fa-flag-checkered"></i> <strong>{% trans "To" %}:</strong> {{ parcel.delivery_address }}</p>
|
<p class="card-text mb-3 small text-muted"><i class="fas fa-flag-checkered"></i> <strong>{% trans "To" %}:</strong> {{ parcel.delivery_address }}</p>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="text-primary fw-bold">{{ parcel.price }} OMR</span>
|
||||||
|
<span class="badge {% if parcel.payment_status == 'paid' %}bg-success{% else %}bg-secondary{% endif %}">
|
||||||
|
{{ parcel.get_payment_status_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if parcel.payment_status == 'pending' %}
|
||||||
|
<a href="{% url 'initiate_payment' parcel.id %}" class="btn btn-sm btn-outline-primary w-100 mb-3">
|
||||||
|
<i class="fas fa-credit-card me-1"></i> {% trans "Pay Now" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<p class="card-text small mb-0"><strong>{% trans "Receiver" %}:</strong> {{ parcel.receiver_name }}</p>
|
<p class="card-text small mb-0"><strong>{% trans "Receiver" %}:</strong> {{ parcel.receiver_name }}</p>
|
||||||
<p class="card-text small"><strong>{% trans "Carrier" %}:</strong> {% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting for pickup" %}{% endif %}</p>
|
<p class="card-text small"><strong>{% trans "Carrier" %}:</strong> {% if parcel.carrier %}{{ parcel.carrier.get_full_name|default:parcel.carrier.username }}{% else %}{% trans "Waiting for pickup" %}{% endif %}</p>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.contrib.auth import views as auth_views
|
|
||||||
from . import views
|
from . import views
|
||||||
|
from django.contrib.auth import views as auth_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.index, name='index'),
|
path('', views.index, name='index'),
|
||||||
@ -16,4 +16,9 @@ urlpatterns = [
|
|||||||
# AJAX for locations
|
# AJAX for locations
|
||||||
path('ajax/get-governates/', views.get_governates, name='get_governates'),
|
path('ajax/get-governates/', views.get_governates, name='get_governates'),
|
||||||
path('ajax/get-cities/', views.get_cities, name='get_cities'),
|
path('ajax/get-cities/', views.get_cities, name='get_cities'),
|
||||||
|
|
||||||
|
# Thawani Payment
|
||||||
|
path('payment/initiate/<int:parcel_id>/', views.initiate_payment, name='initiate_payment'),
|
||||||
|
path('payment/success/', views.payment_success, name='payment_success'),
|
||||||
|
path('payment/cancel/', views.payment_cancel, name='payment_cancel'),
|
||||||
]
|
]
|
||||||
@ -7,6 +7,15 @@ from .forms import UserRegistrationForm, ParcelForm
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import JsonResponse
|
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):
|
def index(request):
|
||||||
tracking_id = request.GET.get('tracking_id')
|
tracking_id = request.GET.get('tracking_id')
|
||||||
@ -45,7 +54,7 @@ def dashboard(request):
|
|||||||
return render(request, 'core/shipper_dashboard.html', {'parcels': parcels})
|
return render(request, 'core/shipper_dashboard.html', {'parcels': parcels})
|
||||||
else:
|
else:
|
||||||
# Car Owner view
|
# 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')
|
my_parcels = Parcel.objects.filter(carrier=request.user).exclude(status='delivered').order_by('-created_at')
|
||||||
return render(request, 'core/driver_dashboard.html', {
|
return render(request, 'core/driver_dashboard.html', {
|
||||||
'available_parcels': available_parcels,
|
'available_parcels': available_parcels,
|
||||||
@ -65,6 +74,10 @@ def shipment_request(request):
|
|||||||
parcel = form.save(commit=False)
|
parcel = form.save(commit=False)
|
||||||
parcel.shipper = request.user
|
parcel.shipper = request.user
|
||||||
parcel.save()
|
parcel.save()
|
||||||
|
|
||||||
|
# WhatsApp Notification
|
||||||
|
notify_shipment_created(parcel)
|
||||||
|
|
||||||
messages.success(request, _("Shipment requested successfully! Tracking ID: ") + parcel.tracking_number)
|
messages.success(request, _("Shipment requested successfully! Tracking ID: ") + parcel.tracking_number)
|
||||||
return redirect('dashboard')
|
return redirect('dashboard')
|
||||||
else:
|
else:
|
||||||
@ -78,10 +91,14 @@ def accept_parcel(request, parcel_id):
|
|||||||
messages.error(request, _("Only car owners can accept shipments."))
|
messages.error(request, _("Only car owners can accept shipments."))
|
||||||
return redirect('dashboard')
|
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.carrier = request.user
|
||||||
parcel.status = 'picked_up'
|
parcel.status = 'picked_up'
|
||||||
parcel.save()
|
parcel.save()
|
||||||
|
|
||||||
|
# WhatsApp Notification
|
||||||
|
notify_driver_assigned(parcel)
|
||||||
|
|
||||||
messages.success(request, _("You have accepted the shipment!"))
|
messages.success(request, _("You have accepted the shipment!"))
|
||||||
return redirect('dashboard')
|
return redirect('dashboard')
|
||||||
|
|
||||||
@ -93,9 +110,59 @@ def update_status(request, parcel_id):
|
|||||||
if new_status in dict(Parcel.STATUS_CHOICES):
|
if new_status in dict(Parcel.STATUS_CHOICES):
|
||||||
parcel.status = new_status
|
parcel.status = new_status
|
||||||
parcel.save()
|
parcel.save()
|
||||||
|
|
||||||
|
# WhatsApp Notification
|
||||||
|
notify_status_change(parcel)
|
||||||
|
|
||||||
messages.success(request, _("Status updated successfully!"))
|
messages.success(request, _("Status updated successfully!"))
|
||||||
return redirect('dashboard')
|
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):
|
def article_detail(request):
|
||||||
return render(request, 'core/article_detail.html')
|
return render(request, 'core/article_detail.html')
|
||||||
|
|
||||||
|
|||||||
92
core/whatsapp_utils.py
Normal file
92
core/whatsapp_utils.py
Normal file
@ -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)
|
||||||
Loading…
x
Reference in New Issue
Block a user