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

Files touched

--- 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 {

Review this PR

Go practice