Template
Add shopper accounts, reviews, tracking, and seed_demo (#9)
Closes #9. Shop-gated buyer accounts, purchase reviews, Stripe customer ids, shipment tracking, slim public contact form, and a template-neutral seed_demo command.
This commit is contained in:
@@ -7,8 +7,11 @@ 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 +138,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 +189,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 / 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
|
||||
|
||||
Reference in New Issue
Block a user