Add live shipping quotes from the carrier
Go · Go · intermediate · modification
Live carrier quotes for the checkout page, replacing the flat-rate table. The handler wraps the request context in a 3-second timeout, so a slow carrier costs checkout at most 3 seconds before we fall back to the 502 the frontend already handles. Errors are logged with the cause and the client only sees `carrier_unavailable`. Tested against the carrier sandbox: valid postcode, unknown postcode (empty list), and a bad API key (502).
Go 1.22 service on the standard library: `net/http` with the method-pattern `ServeMux`, `log/slog`. Package `shipping` already has `Config` (with `CarrierBaseURL` and `CarrierAPIKey`, loaded from the environment and validated at startup), `listZones`, and `writeJSON(w http.ResponseWriter, status int, v any)` in `respond.go`, which sets the JSON content type, writes the status and encodes `v`. `http.DefaultClient` is not modified anywhere in the codebase. The `http.Server` sets only `ReadHeaderTimeout`. The carrier is a trusted partner with small responses; postcodes are not treated as PII in logs.
Requirements
- `GET /api/shipping/quotes?postcode=<code>` returns `{"quotes": [{"service", "amountMinor", "currency"}]}` from the carrier's `GET {CarrierBaseURL}/v2/quotes?postcode=<code>` (bearer `CarrierAPIKey`; the carrier answers 200 with `{"quotes": [...]}`, possibly empty). An empty result is an empty list, never `null`.
- A missing or empty `postcode` answers 400 `{"error": "postcode_required"}`.
- The checkout page waits on this endpoint, and the carrier is known to accept connections and then stall. The carrier call, including reading its response, must give up after 3 seconds.
- Any carrier failure (timeout, network error, non-200 status, undecodable body) answers 502 `{"error": "carrier_unavailable"}` and logs the cause at warn level; carrier details never reach the client.
- No caching or retries in this PR.
Files touched
- internal/shipping/routes.go
- internal/shipping/quote.go
--- internal/shipping/routes.go
func Register(mux *http.ServeMux, cfg Config) {
mux.HandleFunc("GET /api/shipping/zones", listZones)
+
+ quotes := &QuoteHandler{CarrierBaseURL: cfg.CarrierBaseURL, CarrierAPIKey: cfg.CarrierAPIKey}
+ mux.HandleFunc("GET /api/shipping/quotes", quotes.Quote)
}
--- internal/shipping/quote.go
+package shipping
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+const carrierTimeout = 3 * time.Second
+
+type Quote struct {
+ Service string `json:"service"`
+ AmountMinor int64 `json:"amountMinor"`
+ Currency string `json:"currency"`
+}
+
+type QuoteHandler struct {