"""EasyPost-style rates/labels plus Pirate Ship CSV export.""" from __future__ import annotations import csv import io import logging from decimal import Decimal import requests from django.conf import settings from shipping.models import Shipment from shop.models import Order logger = logging.getLogger(__name__) class ShippingError(RuntimeError): pass def _easypost_key() -> str: return (getattr(settings, "EASYPOST_API_KEY", "") or "").strip() def quote_rates(shipment: Shipment) -> list[dict]: """Return carrier rates. Uses EasyPost when configured; otherwise a USPS stub.""" key = _easypost_key() if not key: rates = [ { "id": "stub-usps-ground", "carrier": "USPS", "service": "GroundAdvantage", "rate": "5.40", "currency": "USD", }, { "id": "stub-usps-priority", "carrier": "USPS", "service": "Priority", "rate": "9.80", "currency": "USD", }, ] shipment.rates = rates shipment.status = Shipment.Status.RATED shipment.save(update_fields=["rates", "status", "updated_at"]) return rates addr = shipment.order.shipping_address or {} response = requests.post( "https://api.easypost.com/v2/shipments", auth=(key, ""), json={ "shipment": { "to_address": { "name": shipment.order.customer_name or shipment.order.email, "street1": addr.get("line1") or "", "street2": addr.get("line2") or "", "city": addr.get("city") or "", "state": addr.get("state") or "", "zip": addr.get("zip") or "", "country": addr.get("country") or "US", }, "from_address": { "name": settings.SITE_NAME, "street1": getattr(settings, "SHIP_FROM_LINE1", "") or "", "city": getattr(settings, "SHIP_FROM_CITY", "") or "", "state": getattr(settings, "SHIP_FROM_STATE", "") or "", "zip": getattr(settings, "SHIP_FROM_ZIP", "") or "", "country": "US", }, "parcel": {"weight": shipment.weight_oz}, } }, timeout=20, ) response.raise_for_status() data = response.json() rates = data.get("rates") or [] shipment.provider_shipment_id = data.get("id") or "" shipment.rates = rates shipment.status = Shipment.Status.RATED shipment.save( update_fields=["provider_shipment_id", "rates", "status", "updated_at"] ) return rates def buy_label(shipment: Shipment, *, rate_id: str = "") -> Shipment: rates = shipment.rates or quote_rates(shipment) chosen = None if rate_id: chosen = next((r for r in rates if str(r.get("id")) == str(rate_id)), None) if chosen is None and rates: chosen = rates[0] if chosen is None: raise ShippingError("No shipping rates available.") key = _easypost_key() if key and shipment.provider_shipment_id: response = requests.post( f"https://api.easypost.com/v2/shipments/{shipment.provider_shipment_id}/buy", auth=(key, ""), json={"rate": {"id": chosen.get("id")}}, timeout=20, ) response.raise_for_status() data = response.json() postage = data.get("postage_label") or {} tracking = data.get("tracking_code") or "" shipment.label_url = postage.get("label_url") or "" shipment.tracking_number = tracking else: shipment.tracking_number = f"STUB{shipment.order.number[-6:]}" shipment.label_url = "" shipment.carrier = chosen.get("carrier") or "" shipment.service = chosen.get("service") or "" try: shipment.rate_amount = Decimal(str(chosen.get("rate") or "0")) except Exception: shipment.rate_amount = None shipment.status = Shipment.Status.LABELED shipment.save( update_fields=[ "carrier", "service", "tracking_number", "label_url", "rate_amount", "status", "updated_at", ] ) return shipment def create_shipment_for_order(order: Order, *, weight_oz: int = 16) -> Shipment: if order.status not in {Order.Status.PAID, Order.Status.FULFILLED}: raise ShippingError("Ship paid orders only.") return Shipment.objects.create(order=order, weight_oz=weight_oz) def pirate_ship_csv(orders=None) -> str: """CSV Pirate Ship can map: name, address, city, state, zip, email.""" if orders is None: shipped = Shipment.objects.filter( status=Shipment.Status.LABELED ).values_list("order_id", flat=True) orders = Order.objects.filter(status=Order.Status.PAID).exclude(pk__in=shipped) buffer = io.StringIO() writer = csv.writer(buffer) writer.writerow( [ "Order Number", "Name", "Address 1", "Address 2", "City", "State", "Zip", "Country", "Email", ] ) for order in orders: addr = order.shipping_address or {} writer.writerow( [ order.number, order.customer_name or order.email, addr.get("line1") or "", addr.get("line2") or "", addr.get("city") or "", addr.get("state") or "", addr.get("zip") or "", addr.get("country") or "US", order.email, ] ) return buffer.getvalue()