diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 2aa9bed..aaa84ed 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ 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 64e66ae..becc875 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 899df19..7b76646 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/payment_utils.cpython-311.pyc b/core/__pycache__/payment_utils.cpython-311.pyc new file mode 100644 index 0000000..e98bfe9 Binary files /dev/null and b/core/__pycache__/payment_utils.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 125f3bd..9d3522b 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index a02e5b3..4510127 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/__pycache__/whatsapp_utils.cpython-311.pyc b/core/__pycache__/whatsapp_utils.cpython-311.pyc new file mode 100644 index 0000000..b133c19 Binary files /dev/null and b/core/__pycache__/whatsapp_utils.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py index 0f55053..552138d 100644 --- a/core/forms.py +++ b/core/forms.py @@ -69,7 +69,7 @@ class ParcelForm(forms.ModelForm): class Meta: model = Parcel fields = [ - 'description', 'weight', + 'description', 'weight', 'price', 'pickup_country', 'pickup_governate', 'pickup_city', 'pickup_address', 'delivery_country', 'delivery_governate', 'delivery_city', 'delivery_address', 'receiver_name', 'receiver_phone' @@ -77,6 +77,7 @@ class ParcelForm(forms.ModelForm): widgets = { 'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('What are you sending?')}), '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_governate': forms.Select(attrs={'class': 'form-control'}), @@ -94,6 +95,7 @@ class ParcelForm(forms.ModelForm): labels = { 'description': _('Package Description'), 'weight': _('Weight (kg)'), + 'price': _('Shipping Price (OMR)'), 'pickup_country': _('Pickup Country'), 'pickup_governate': _('Pickup Governate'), 'pickup_city': _('Pickup City'), @@ -142,4 +144,4 @@ class ParcelForm(forms.ModelForm): gov_id = int(self.data.get('delivery_governate')) self.fields['delivery_city'].queryset = City.objects.filter(governate_id=gov_id).order_by('name') except (ValueError, TypeError): - pass + pass \ No newline at end of file diff --git a/core/migrations/0004_parcel_payment_status_parcel_price_and_more.py b/core/migrations/0004_parcel_payment_status_parcel_price_and_more.py new file mode 100644 index 0000000..5e4e555 --- /dev/null +++ b/core/migrations/0004_parcel_payment_status_parcel_price_and_more.py @@ -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'), + ), + ] diff --git a/core/migrations/__pycache__/0004_parcel_payment_status_parcel_price_and_more.cpython-311.pyc b/core/migrations/__pycache__/0004_parcel_payment_status_parcel_price_and_more.cpython-311.pyc new file mode 100644 index 0000000..b0bb0f6 Binary files /dev/null and b/core/migrations/__pycache__/0004_parcel_payment_status_parcel_price_and_more.cpython-311.pyc differ 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 @@
{% trans "Pickup" %}: {{ parcel.pickup_address }}
{% trans "Delivery" %}: {{ parcel.delivery_address }}
-{% trans "Weight" %}: {{ parcel.weight }} kg
+