generated from westfarn/web_django_template
Add customer accounts, shipment tracking, and purchase reviews.
CI / test (pull_request) Successful in 35s
CI / test (pull_request) Successful in 35s
Shoppers can register, save shipping details, and view order history while cards stay on Stripe. EasyPost tracker updates (including numbers from Pirate Ship) and 1–5 star reviews are limited to buyers. The contact form now only asks for email and a message.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from core.registry import (
|
||||
register_dashboard_collector,
|
||||
register_dispatcher,
|
||||
register_feature,
|
||||
register_portal_nav,
|
||||
)
|
||||
@@ -15,6 +16,7 @@ def register() -> None:
|
||||
order=50,
|
||||
)
|
||||
register_dashboard_collector(_dashboard)
|
||||
register_dispatcher(_sync_tracking)
|
||||
|
||||
|
||||
def _dashboard(request) -> dict:
|
||||
@@ -29,3 +31,10 @@ def _dashboard(request) -> dict:
|
||||
.exclude(pk__in=labeled)
|
||||
.count()
|
||||
}
|
||||
|
||||
|
||||
def _sync_tracking() -> int:
|
||||
from shipping.services import sync_open_tracking
|
||||
|
||||
return sync_open_tracking()
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 6.1
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shipping", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="last_tracked_at",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="tracker_id",
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="tracking_events",
|
||||
field=models.JSONField(blank=True, default=list),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="tracking_status",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("unknown", "Unknown"),
|
||||
("pre_transit", "Pre-transit"),
|
||||
("in_transit", "In transit"),
|
||||
("out_for_delivery", "Out for delivery"),
|
||||
("delivered", "Delivered"),
|
||||
("available_for_pickup", "Available for pickup"),
|
||||
("return_to_sender", "Return to sender"),
|
||||
("failure", "Exception"),
|
||||
("cancelled", "Cancelled"),
|
||||
("error", "Error"),
|
||||
],
|
||||
default="unknown",
|
||||
max_length=32,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="tracking_url",
|
||||
field=models.URLField(blank=True),
|
||||
),
|
||||
]
|
||||
+25
-1
@@ -11,6 +11,18 @@ class Shipment(UUIDPrimaryKeyModel, TimeStampedModel):
|
||||
LABELED = "labeled", "Labeled"
|
||||
VOID = "void", "Void"
|
||||
|
||||
class TrackingStatus(models.TextChoices):
|
||||
UNKNOWN = "unknown", "Unknown"
|
||||
PRE_TRANSIT = "pre_transit", "Pre-transit"
|
||||
IN_TRANSIT = "in_transit", "In transit"
|
||||
OUT_FOR_DELIVERY = "out_for_delivery", "Out for delivery"
|
||||
DELIVERED = "delivered", "Delivered"
|
||||
AVAILABLE_FOR_PICKUP = "available_for_pickup", "Available for pickup"
|
||||
RETURN_TO_SENDER = "return_to_sender", "Return to sender"
|
||||
FAILURE = "failure", "Exception"
|
||||
CANCELLED = "cancelled", "Cancelled"
|
||||
ERROR = "error", "Error"
|
||||
|
||||
order = models.ForeignKey(Order, on_delete=models.PROTECT, related_name="shipments")
|
||||
status = models.CharField(
|
||||
max_length=16, choices=Status.choices, default=Status.DRAFT
|
||||
@@ -18,8 +30,20 @@ class Shipment(UUIDPrimaryKeyModel, TimeStampedModel):
|
||||
carrier = models.CharField(max_length=32, blank=True)
|
||||
service = models.CharField(max_length=64, blank=True)
|
||||
tracking_number = models.CharField(max_length=64, blank=True)
|
||||
tracking_status = models.CharField(
|
||||
max_length=32,
|
||||
choices=TrackingStatus.choices,
|
||||
default=TrackingStatus.UNKNOWN,
|
||||
blank=True,
|
||||
)
|
||||
tracking_url = models.URLField(blank=True)
|
||||
tracker_id = models.CharField(max_length=255, blank=True)
|
||||
tracking_events = models.JSONField(default=list, blank=True)
|
||||
last_tracked_at = models.DateTimeField(null=True, blank=True)
|
||||
label_url = models.URLField(blank=True)
|
||||
rate_amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||
rate_amount = models.DecimalField(
|
||||
max_digits=10, decimal_places=2, null=True, blank=True
|
||||
)
|
||||
currency = models.CharField(max_length=8, default="usd")
|
||||
provider_shipment_id = models.CharField(max_length=255, blank=True)
|
||||
rates = models.JSONField(default=list, blank=True)
|
||||
|
||||
@@ -6,9 +6,11 @@ import csv
|
||||
import io
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from shipping.models import Shipment
|
||||
from shop.models import Order
|
||||
@@ -135,6 +137,10 @@ def buy_label(shipment: Shipment, *, rate_id: str = "") -> Shipment:
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
try:
|
||||
refresh_tracking(shipment)
|
||||
except Exception:
|
||||
logger.exception("tracking refresh failed after label buy for %s", shipment.pk)
|
||||
return shipment
|
||||
|
||||
|
||||
@@ -182,3 +188,169 @@ def pirate_ship_csv(orders=None) -> str:
|
||||
]
|
||||
)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def public_tracking_url(carrier: str, tracking_number: str) -> str:
|
||||
code = quote((tracking_number or "").strip())
|
||||
if not code:
|
||||
return ""
|
||||
name = (carrier or "").upper()
|
||||
if "USPS" in name:
|
||||
return f"https://tools.usps.com/go/TrackConfirmAction?tLabels={code}"
|
||||
if "UPS" in name:
|
||||
return f"https://www.ups.com/track?tracknum={code}"
|
||||
if "FEDEX" in name or "FDX" in name:
|
||||
return f"https://www.fedex.com/fedextrack/?trknbr={code}"
|
||||
if "DHL" in name:
|
||||
return f"https://www.dhl.com/en/express/tracking.html?AWB={code}"
|
||||
return f"https://www.google.com/search?q={quote((tracking_number or '') + ' tracking')}"
|
||||
|
||||
|
||||
def _normalize_tracking_status(raw: str) -> str:
|
||||
value = (raw or "").strip().lower().replace(" ", "_")
|
||||
aliases = {
|
||||
"pretransit": Shipment.TrackingStatus.PRE_TRANSIT,
|
||||
"pre_transit": Shipment.TrackingStatus.PRE_TRANSIT,
|
||||
"in_transit": Shipment.TrackingStatus.IN_TRANSIT,
|
||||
"out_for_delivery": Shipment.TrackingStatus.OUT_FOR_DELIVERY,
|
||||
"delivered": Shipment.TrackingStatus.DELIVERED,
|
||||
"available_for_pickup": Shipment.TrackingStatus.AVAILABLE_FOR_PICKUP,
|
||||
"return_to_sender": Shipment.TrackingStatus.RETURN_TO_SENDER,
|
||||
"failure": Shipment.TrackingStatus.FAILURE,
|
||||
"cancelled": Shipment.TrackingStatus.CANCELLED,
|
||||
"canceled": Shipment.TrackingStatus.CANCELLED,
|
||||
"error": Shipment.TrackingStatus.ERROR,
|
||||
"unknown": Shipment.TrackingStatus.UNKNOWN,
|
||||
}
|
||||
return aliases.get(value, Shipment.TrackingStatus.UNKNOWN)
|
||||
|
||||
|
||||
def apply_tracker_payload(shipment: Shipment, payload: dict) -> Shipment:
|
||||
"""Apply EasyPost tracker (or compatible) JSON onto a shipment."""
|
||||
data = payload or {}
|
||||
tracking = (data.get("tracking_code") or data.get("tracking_number") or "").strip()
|
||||
if tracking:
|
||||
shipment.tracking_number = tracking
|
||||
tracker_id = (data.get("id") or "").strip()
|
||||
if tracker_id.startswith("trk_"):
|
||||
shipment.tracker_id = tracker_id
|
||||
carrier = (data.get("carrier") or "").strip()
|
||||
if carrier and not shipment.carrier:
|
||||
shipment.carrier = carrier
|
||||
shipment.tracking_status = _normalize_tracking_status(data.get("status") or "")
|
||||
public_url = (data.get("public_url") or "").strip()
|
||||
shipment.tracking_url = public_url or public_tracking_url(
|
||||
shipment.carrier, shipment.tracking_number
|
||||
)
|
||||
events = []
|
||||
for item in data.get("tracking_details") or []:
|
||||
loc = item.get("tracking_location") or {}
|
||||
place = " ".join(
|
||||
part
|
||||
for part in [loc.get("city") or "", loc.get("state") or ""]
|
||||
if part
|
||||
).strip()
|
||||
events.append(
|
||||
{
|
||||
"status": item.get("status") or "",
|
||||
"message": item.get("message") or "",
|
||||
"datetime": item.get("datetime") or "",
|
||||
"location": place,
|
||||
}
|
||||
)
|
||||
if events:
|
||||
shipment.tracking_events = events
|
||||
shipment.last_tracked_at = timezone.now()
|
||||
shipment.save(
|
||||
update_fields=[
|
||||
"tracking_number",
|
||||
"tracker_id",
|
||||
"carrier",
|
||||
"tracking_status",
|
||||
"tracking_url",
|
||||
"tracking_events",
|
||||
"last_tracked_at",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return shipment
|
||||
|
||||
|
||||
def refresh_tracking(shipment: Shipment) -> Shipment:
|
||||
"""Pull latest scan events from EasyPost, or stub status without an API key.
|
||||
|
||||
Works for EasyPost-bought labels and for tracking numbers pasted from
|
||||
Pirate Ship / Shippo / the carrier — EasyPost's tracker API looks up
|
||||
USPS, UPS, FedEx, and DHL by number.
|
||||
"""
|
||||
tracking = (shipment.tracking_number or "").strip()
|
||||
if not tracking:
|
||||
return shipment
|
||||
key = _easypost_key()
|
||||
if not key:
|
||||
if not shipment.tracking_status or shipment.tracking_status == Shipment.TrackingStatus.UNKNOWN:
|
||||
shipment.tracking_status = Shipment.TrackingStatus.PRE_TRANSIT
|
||||
shipment.tracking_url = shipment.tracking_url or public_tracking_url(
|
||||
shipment.carrier, tracking
|
||||
)
|
||||
shipment.last_tracked_at = timezone.now()
|
||||
shipment.save(
|
||||
update_fields=[
|
||||
"tracking_status",
|
||||
"tracking_url",
|
||||
"last_tracked_at",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return shipment
|
||||
|
||||
payload = {"tracker": {"tracking_code": tracking}}
|
||||
if shipment.carrier:
|
||||
payload["tracker"]["carrier"] = shipment.carrier
|
||||
response = requests.post(
|
||||
"https://api.easypost.com/v2/trackers",
|
||||
auth=(key, ""),
|
||||
json=payload,
|
||||
timeout=20,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return apply_tracker_payload(shipment, response.json())
|
||||
|
||||
|
||||
def attach_tracking(
|
||||
shipment: Shipment, *, tracking_number: str, carrier: str = ""
|
||||
) -> Shipment:
|
||||
tracking_number = (tracking_number or "").strip()
|
||||
if not tracking_number:
|
||||
raise ShippingError("Tracking number is required.")
|
||||
shipment.tracking_number = tracking_number
|
||||
if carrier:
|
||||
shipment.carrier = carrier.strip()
|
||||
if shipment.status != Shipment.Status.LABELED:
|
||||
shipment.status = Shipment.Status.LABELED
|
||||
shipment.save(
|
||||
update_fields=["tracking_number", "carrier", "status", "updated_at"]
|
||||
)
|
||||
return refresh_tracking(shipment)
|
||||
|
||||
|
||||
def sync_open_tracking() -> int:
|
||||
"""Refresh labeled, not-yet-delivered shipments (dispatch_due)."""
|
||||
done = 0
|
||||
qs = (
|
||||
Shipment.objects.filter(status=Shipment.Status.LABELED)
|
||||
.exclude(tracking_number="")
|
||||
.exclude(
|
||||
tracking_status__in=[
|
||||
Shipment.TrackingStatus.DELIVERED,
|
||||
Shipment.TrackingStatus.CANCELLED,
|
||||
]
|
||||
)[:50]
|
||||
)
|
||||
for shipment in qs:
|
||||
try:
|
||||
refresh_tracking(shipment)
|
||||
done += 1
|
||||
except Exception:
|
||||
logger.exception("tracking sync failed for %s", shipment.pk)
|
||||
return done
|
||||
|
||||
@@ -3,8 +3,52 @@
|
||||
{% block topbar_title %}{{ shipment.order.number }}{% endblock %}
|
||||
{% block portal_content %}
|
||||
<p>{{ shipment.order.email }} · {{ shipment.get_status_display }}</p>
|
||||
{% if shipment.tracking_number %}<p>Tracking: {{ shipment.tracking_number }}</p>{% endif %}
|
||||
{% if shipment.tracking_number %}
|
||||
<p>
|
||||
Tracking: {{ shipment.tracking_number }}
|
||||
{% if shipment.tracking_status %} · {{ shipment.get_tracking_status_display }}{% endif %}
|
||||
</p>
|
||||
{% if shipment.tracking_url %}<p><a href="{{ shipment.tracking_url }}" target="_blank" rel="noopener">Track package</a></p>{% endif %}
|
||||
<form method="post" action="{% url 'shipping:shipment_refresh_tracking' shipment.pk %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-ghost btn-sm" type="submit">Refresh tracking</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if shipment.label_url %}<p><a href="{{ shipment.label_url }}">Download label</a></p>{% endif %}
|
||||
|
||||
<h3>Add tracking</h3>
|
||||
<p class="hint-block">Paste a number from Pirate Ship, Shippo, or the carrier. EasyPost looks up scan events when an API key is set.</p>
|
||||
<form method="post" action="{% url 'shipping:shipment_attach_tracking' shipment.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label for="id_tracking_number">Tracking number</label>
|
||||
<input id="id_tracking_number" name="tracking_number" value="{{ shipment.tracking_number }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="id_carrier">Carrier</label>
|
||||
<input id="id_carrier" name="carrier" value="{{ shipment.carrier }}" placeholder="USPS, UPS, FedEx">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Save tracking</button>
|
||||
</form>
|
||||
|
||||
{% if shipment.tracking_events %}
|
||||
<h3>Scan history</h3>
|
||||
<table class="table">
|
||||
<thead><tr><th>When</th><th>Status</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
{% for event in shipment.tracking_events %}
|
||||
<tr>
|
||||
<td>{{ event.datetime }}</td>
|
||||
<td>{{ event.status }}</td>
|
||||
<td>{{ event.message }}{% if event.location %} · {{ event.location }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if shipment.status != 'labeled' %}
|
||||
<form method="post" action="{% url 'shipping:shipment_buy' shipment.pk %}">
|
||||
{% csrf_token %}
|
||||
|
||||
+54
-1
@@ -1,4 +1,5 @@
|
||||
from decimal import Decimal
|
||||
import json
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import Client, TestCase
|
||||
@@ -49,11 +50,63 @@ class ShippingServiceTests(TestCase):
|
||||
buy_label(shipment)
|
||||
self.assertNotIn(self.order.number, pirate_ship_csv())
|
||||
|
||||
def test_stub_buy_sets_pre_transit_tracking(self):
|
||||
shipment = create_shipment_for_order(self.order)
|
||||
quote_rates(shipment)
|
||||
buy_label(shipment)
|
||||
shipment.refresh_from_db()
|
||||
self.assertTrue(shipment.tracking_number)
|
||||
self.assertEqual(shipment.tracking_status, Shipment.TrackingStatus.PRE_TRANSIT)
|
||||
self.assertTrue(shipment.tracking_url)
|
||||
|
||||
def test_attach_tracking_from_pirate_ship(self):
|
||||
from shipping.services import attach_tracking
|
||||
|
||||
shipment = create_shipment_for_order(self.order)
|
||||
attach_tracking(shipment, tracking_number="9400111899223197428490", carrier="USPS")
|
||||
shipment.refresh_from_db()
|
||||
self.assertEqual(shipment.status, Shipment.Status.LABELED)
|
||||
self.assertEqual(shipment.tracking_status, Shipment.TrackingStatus.PRE_TRANSIT)
|
||||
self.assertIn("usps.com", shipment.tracking_url.lower())
|
||||
|
||||
def test_easypost_webhook_updates_status(self):
|
||||
shipment = create_shipment_for_order(self.order)
|
||||
shipment.tracking_number = "EZ1000000001"
|
||||
shipment.status = Shipment.Status.LABELED
|
||||
shipment.save()
|
||||
payload = {
|
||||
"description": "tracker.updated",
|
||||
"result": {
|
||||
"id": "trk_test",
|
||||
"tracking_code": "EZ1000000001",
|
||||
"status": "in_transit",
|
||||
"public_url": "https://track.easypost.com/djE0",
|
||||
"tracking_details": [
|
||||
{
|
||||
"status": "in_transit",
|
||||
"message": "Departed facility",
|
||||
"datetime": "2026-09-07T12:00:00Z",
|
||||
"tracking_location": {"city": "Chicago", "state": "IL"},
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
response = Client().post(
|
||||
reverse("shipping:easypost_webhook"),
|
||||
data=json.dumps(payload),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
shipment.refresh_from_db()
|
||||
self.assertEqual(shipment.tracking_status, Shipment.TrackingStatus.IN_TRANSIT)
|
||||
self.assertEqual(shipment.tracker_id, "trk_test")
|
||||
self.assertEqual(shipment.tracking_events[0]["location"], "Chicago IL")
|
||||
|
||||
|
||||
class ShippingPortalTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user("shipper", password="test-pass-123")
|
||||
self.user = User.objects.create_user("shipper", password="test-pass-123", is_staff=True)
|
||||
self.client = Client()
|
||||
self.client.login(username="shipper", password="test-pass-123")
|
||||
self.order = Order.objects.create(
|
||||
|
||||
@@ -8,6 +8,17 @@ urlpatterns = [
|
||||
path("", views.shipment_list, name="shipment_list"),
|
||||
path("new/", views.shipment_create, name="shipment_create"),
|
||||
path("export/pirate-ship.csv", views.pirate_ship_export, name="pirate_ship_export"),
|
||||
path("webhooks/easypost/", views.easypost_webhook, name="easypost_webhook"),
|
||||
path("<uuid:pk>/", views.shipment_detail, name="shipment_detail"),
|
||||
path("<uuid:pk>/buy/", views.shipment_buy, name="shipment_buy"),
|
||||
path(
|
||||
"<uuid:pk>/tracking/",
|
||||
views.shipment_attach_tracking,
|
||||
name="shipment_attach_tracking",
|
||||
),
|
||||
path(
|
||||
"<uuid:pk>/tracking/refresh/",
|
||||
views.shipment_refresh_tracking,
|
||||
name="shipment_refresh_tracking",
|
||||
),
|
||||
]
|
||||
|
||||
+74
-1
@@ -1,18 +1,24 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods, require_POST
|
||||
|
||||
from shipping.models import Shipment
|
||||
from shipping.services import (
|
||||
ShippingError,
|
||||
apply_tracker_payload,
|
||||
attach_tracking,
|
||||
buy_label,
|
||||
create_shipment_for_order,
|
||||
pirate_ship_csv,
|
||||
quote_rates,
|
||||
refresh_tracking,
|
||||
)
|
||||
from shop.models import Order
|
||||
|
||||
@@ -77,6 +83,40 @@ def shipment_buy(request, pk):
|
||||
return redirect("shipping:shipment_detail", pk=shipment.pk)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def shipment_attach_tracking(request, pk):
|
||||
shipment = get_object_or_404(Shipment, pk=pk)
|
||||
try:
|
||||
attach_tracking(
|
||||
shipment,
|
||||
tracking_number=request.POST.get("tracking_number") or "",
|
||||
carrier=request.POST.get("carrier") or "",
|
||||
)
|
||||
except ShippingError as exc:
|
||||
messages.error(request, str(exc))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.exception("attach tracking failed")
|
||||
messages.error(request, f"Could not save tracking: {exc}")
|
||||
else:
|
||||
messages.success(request, "Tracking saved.")
|
||||
return redirect("shipping:shipment_detail", pk=shipment.pk)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def shipment_refresh_tracking(request, pk):
|
||||
shipment = get_object_or_404(Shipment, pk=pk)
|
||||
try:
|
||||
refresh_tracking(shipment)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.exception("refresh tracking failed")
|
||||
messages.error(request, f"Could not refresh tracking: {exc}")
|
||||
else:
|
||||
messages.success(request, "Tracking updated.")
|
||||
return redirect("shipping:shipment_detail", pk=shipment.pk)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def pirate_ship_export(request):
|
||||
@@ -84,3 +124,36 @@ def pirate_ship_export(request):
|
||||
response = HttpResponse(body, content_type="text/csv")
|
||||
response["Content-Disposition"] = 'attachment; filename="pirate-ship-orders.csv"'
|
||||
return response
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def easypost_webhook(request):
|
||||
secret = (getattr(settings, "EASYPOST_WEBHOOK_SECRET", "") or "").strip()
|
||||
if secret:
|
||||
got = (
|
||||
request.headers.get("X-Webhook-Secret")
|
||||
or request.GET.get("token")
|
||||
or ""
|
||||
).strip()
|
||||
if got != secret:
|
||||
return HttpResponseBadRequest("invalid secret")
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest("invalid json")
|
||||
result = payload.get("result") or payload
|
||||
if not isinstance(result, dict):
|
||||
return HttpResponse("ok")
|
||||
tracker_id = (result.get("id") or "").strip()
|
||||
tracking = (result.get("tracking_code") or result.get("tracking_number") or "").strip()
|
||||
shipment = None
|
||||
if tracker_id:
|
||||
shipment = Shipment.objects.filter(tracker_id=tracker_id).first()
|
||||
if shipment is None and tracking:
|
||||
shipment = Shipment.objects.filter(tracking_number=tracking).first()
|
||||
if shipment is None:
|
||||
return HttpResponse("ok")
|
||||
apply_tracker_payload(shipment, result)
|
||||
logger.info("easypost tracker updated shipment %s", shipment.pk)
|
||||
return HttpResponse("ok")
|
||||
|
||||
Reference in New Issue
Block a user