6.2 KiB
Carrier integrations
This eshop manages delivery options as plain rows in the shipping_methods
table (admin UI at /admin/shipping — add / edit price + toggle / remove). A
delivery option is just a name, a price, and two flags. None of that talks to
a carrier yet — it only decides what the customer can pick and how much they
pay.
Integrating a real carrier (Packeta, DPD, DHL) means wiring two separate concerns on top of an existing delivery option:
- Pickup-point selection (checkout, browser-side) — only for carriers that deliver to pickup points / lockers. The customer picks a point via the carrier's JS map widget; the chosen id + name land in the order.
- Shipment creation (server-side, after the order is placed) — you call the carrier's HTTP API to register the parcel, then store the returned tracking number and print the label.
These are independent: you can ship to a Packeta pickup point manually (no API) just by enabling the pickup widget, and you can create DHL labels via API for a home-delivery option that has no pickup point at all.
❗ This is not a many-to-many / database relationship between your tables. A carrier is an external HTTP API you call from the server. The only schema you add is a few columns (which carrier a method maps to; a tracking number on the order) — see "Shared groundwork" below.
What already exists in the codebase
| Piece | Where | Status |
|---|---|---|
| Delivery option CRUD | src/controllers/admin_shipping.rs, assets/views/admin/shipping/index.html |
✅ done |
shipping_methods table (code, name, price_cents, requires_pickup_point, enabled, position) |
migration/.../m20260616_150755_shipping_methods.rs |
✅ done |
| Carrier choice + pickup fields on checkout | assets/views/shop/checkout.html (carrier_code, pickup_point_id, pickup_point_name) |
✅ done |
| Order stores carrier + pickup point | orders table (carrier_code, carrier_name, pickup_point_id, pickup_point_name, shipping_cents) |
✅ done |
| Settings lookup | src/shared/settings.rs → reads settings.* from config/*.yaml |
✅ done |
| Packeta pickup-point widget | assets/views/shop/checkout.html (loads when packeta_api_key set) |
✅ scaffolded |
shipping_methods.carrier (which API a method maps to) |
migration/.../m20260617_000001_* + admin add-form dropdown |
✅ done |
| Tracking / shipment id / label on order | migration/.../m20260617_000002_* (orders.tracking_number, shipment_id, label_url) |
✅ done |
| Manual "Send to carrier" admin action | src/controllers/admin_orders.rs (ship), order detail page |
✅ done |
| Carrier client dispatch | src/integrations/ (create_shipment) |
✅ done |
| Packeta shipment client | src/integrations/packeta.rs (real createPacket) |
✅ done |
| DPD / DHL shipment clients | src/integrations/dpd.rs, dhl.rs |
🟡 credential-guarded stub — fill in HTTP call per contract |
Shipments are created only when an admin clicks "Send to carrier" on the order page — never automatically at checkout. Packeta is wired end-to-end (needs just the API password + sender label). DPD/DHL run through the same flow but their HTTP body must be finalised against your contract (clearly marked TODOs in each file).
Shared groundwork (do this once, before any carrier's API step)
The pickup-widget half needs nothing new. The shipment-creation half needs:
-
An HTTP client dependency. Add to
Cargo.toml:reqwest = { version = "0.12", features = ["json"] }(Loco already pulls
tokio/serde/serde_json.) -
A place for carrier clients. Create
src/integrations/mod.rsand a file per carrier (packeta.rs,dpd.rs,dhl.rs). Registerpub mod integrations;insrc/lib.rs(next topub mod controllers;etc.). -
Map a delivery option to a carrier. Add a
carriercolumn toshipping_methodsso each admin-created option knows which API (if any) to call. Generate the migration:cargo loco generate migration add_carrier_to_shipping_methods carrier:stringValues:
none(manual, the default),packeta,dpd,dhl. Then add a<select name="carrier">to the add-form inassets/views/admin/shipping/index.htmland persist it inadmin_shipping::create. -
Store the tracking number / label on the order. Generate:
cargo loco generate migration add_tracking_to_orders \ tracking_number:string shipment_id:string label_url:string -
A "Create shipment" admin action. In the admin order detail (
src/controllers/admin_orders.rs), add a button/handler that: looks up the order'scarrier_code→ finds theshipping_methods.carrier→ calls the matchingintegrations::<carrier>::create_shipment(...)→ savestracking_number+label_urlback onto the order. Optionally do this automatically inorders::place, but a manual admin trigger is safer to start (you can review the order first).
After the groundwork, each carrier file implements one async function roughly like:
pub struct ShipmentRequest<'a> {
pub order_number: &'a str,
pub recipient_name: &'a str,
pub email: &'a str,
pub phone: Option<&'a str>,
pub address: Option<&'a str>,
pub city: Option<&'a str>,
pub zip: Option<&'a str>,
pub country: Option<&'a str>,
pub pickup_point_id: Option<&'a str>,
pub cod_cents: i64, // 0 unless cash-on-delivery
pub currency: &'a str,
pub weight_grams: i32,
}
pub struct ShipmentResult {
pub shipment_id: String,
pub tracking_number: String,
pub label_url: Option<String>,
}
pub async fn create_shipment(ctx: &AppContext, req: ShipmentRequest<'_>) -> loco_rs::Result<ShipmentResult> { ... }
Read next
packeta.md— Packeta / Zásilkovna (pickup points + home, SK/CZ-centric)dpd.md— DPD (home delivery + Pickup parcelshops)dhl.md— DHL (international, Parcel/Express)
⚠️ Carrier APIs change. Treat the endpoint names, field names, and auth details here as a map of the moving parts, and confirm exact request formats against each carrier's current developer portal before coding.