Files
2026-06-29 12:35:32 +02:00

141 lines
6.6 KiB
Go

// Package shipping is the platform's shared shipping seam: the cross-module
// vocabulary for delivery options and the Provider interface. It holds only the
// wire value types plus the interface, so the producer (a shipping adapter like
// PostNord) and every consumer (checkout, cart, backoffice) agree on the types.
// It imports only the stdlib — never an app module, never a transport library.
//
// Dependency rule: packages under platform/ import only third-party libraries
// and other platform/ packages — never an application module. Application
// modules depend downward onto platform. This is what keeps it a stable seam.
//
// See platform/inventory for the sibling pattern (value types + interface in
// one stdlib-only package, test adapter in _test.go).
package shipping
import (
"context"
"encoding/json"
"errors"
)
// ── Provider interface ─────────────────────────────────────────────────────
// Provider is the cross-module seam for shipping delivery options. Checkout
// calls GetShippingOptions to present available delivery methods to the
// customer. Every implementation behind this interface is free to choose its
// internal provider (PostNord, DHL, Bring, etc.) — the consumer never needs
// to know.
//
// This is the policy-first seam: shipping selection policy lives here; the
// provider-specific HTTP/REST details live behind it.
type Provider interface {
// GetShippingOptions returns available delivery options for a recipient
// address. Returns an empty slice (not an error) when the provider has no
// matching options. Returns ErrNoOptions when no provider in a multi-provider
// setup could serve the address.
GetShippingOptions(ctx context.Context, req GetOptionsRequest) ([]DeliveryOptionGroup, error)
}
// ── Request types ─────────────────────────────────────────────────────────
// GetOptionsRequest is what a caller provides to get shipping options.
type GetOptionsRequest struct {
// Country is the ISO 3166-1 alpha-2 recipient country (e.g. "SE", "NO").
Country string `json:"country"`
// PostCode is the recipient postal code.
PostCode string `json:"post_code"`
// Locale is the IETF BCP 47 language tag for descriptions (e.g. "sv", "en").
// Empty means the provider chooses a default.
Locale string `json:"locale,omitempty"`
// Items optionally describes the cart items for dimension/weight-based
// filtering. May be nil or empty for providers that don't need item data.
Items []OptionItem `json:"items,omitempty"`
}
// OptionItem describes one line item for shipping calculations (weight, volume,
// category-based restrictions).
type OptionItem struct {
SKU string `json:"sku"`
Quantity int `json:"quantity"`
// WeightGrams is the item weight in grams, if known.
WeightGrams int `json:"weight_grams,omitempty"`
}
// ── Response types ────────────────────────────────────────────────────────
// DeliveryOptionGroup groups shipping options by fulfilment method (e.g. home
// delivery, pickup point, parcel locker). A provider returns one or more
// groups; each group has one default option and optional alternatives (e.g.
// different pickup point locations).
type DeliveryOptionGroup struct {
// Type is the fulfilment-method category: "home", "pickup", "locker",
// "mailbox", etc.
Type string `json:"type"`
// Name is the display name for the group (e.g. "Home delivery",
// "Pickup point"). Provider-localised to the request Locale.
Name string `json:"name"`
// DefaultOption is the recommended option within this group.
DefaultOption DeliveryOption `json:"default_option"`
// AdditionalOptions are alternatives within the same group (e.g. other
// pickup point locations, different time slots).
AdditionalOptions []DeliveryOption `json:"additional_options,omitempty"`
}
// DeliveryOption is one selectable shipping method or service point.
type DeliveryOption struct {
// ID is the provider-scoped unique identifier for this option. Used when
// the caller confirms the selection (e.g. SetCartDelivery).
ID string `json:"id"`
// Name is a short display name (e.g. "Home delivery", "PostNord Storgatan").
Name string `json:"name"`
// Description is a longer description for the checkout UI.
Description string `json:"description,omitempty"`
// DeliveryDays is a human-readable estimate (e.g. "2-3 working days",
// "1-2 days"). Provider-localised.
DeliveryDays string `json:"delivery_days,omitempty"`
// PickupLocation is set when this option is a physical pickup point.
PickupLocation *PickupLocation `json:"pickup_location,omitempty"`
}
// PickupLocation describes a physical pickup point (store, locker, post office).
type PickupLocation struct {
ID string `json:"id"`
Name string `json:"name"`
Address Address `json:"address"`
}
// Address is a simplified postal address for delivery or pickup.
type Address struct {
Street string `json:"street"`
Street2 string `json:"street2,omitempty"`
City string `json:"city"`
PostCode string `json:"post_code"`
Country string `json:"country"` // ISO 3166-1 alpha-2
}
// ── Sentinels ─────────────────────────────────────────────────────────────
// ErrNoOptions is returned by a multi-provider orchestrator when no registered
// provider could serve the given address. A single provider returns an empty
// slice (not this error) for an unsupported address.
var ErrNoOptions = errors.New("shipping: no delivery options available")
// ErrProviderUnavailable is returned when a provider's API is unreachable or
// returns a non-retryable error. The caller may fall back to another provider
// or degrade gracefully.
var ErrProviderUnavailable = errors.New("shipping: provider unavailable")
// ── Wire helpers ──────────────────────────────────────────────────────────
// Encode serialises a DeliveryOptionGroup slice for caching or event payloads.
func Encode(groups []DeliveryOptionGroup) ([]byte, error) {
return json.Marshal(groups)
}
// Decode parses a DeliveryOptionGroup slice from Encode output.
func Decode(raw []byte) ([]DeliveryOptionGroup, error) {
var out []DeliveryOptionGroup
err := json.Unmarshal(raw, &out)
return out, err
}