package main import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" ) // OrderClient is a minimal HTTP client for the order service. It is the // checkout service's counterpart to the CartClient — the same pattern, but // calling into order-service endpoints instead of cart-service ones. // // Currently only CreateOrder is implemented (the from-checkout handoff). // Add GetOrder/ListOrders methods as the backoffice order UI evolves. type OrderClient struct { httpClient *http.Client baseURL string // e.g. "http://order-service:8092" } // NewOrderClient returns a client that talks to the order service at baseURL. func NewOrderClient(baseURL string) *OrderClient { return &OrderClient{ httpClient: &http.Client{Timeout: 30 * time.Second}, baseURL: baseURL, } } // CreateOrderReq is the JSON payload POSTed to the order service's // /api/orders/from-checkout endpoint. Every field is required except where // noted. See cmd/order/handlers_checkout.go for the full definition. type CreateOrderReq struct { CheckoutId string `json:"checkoutId"` IdempotencyKey string `json:"idempotencyKey"` CartId string `json:"cartId"` Currency string `json:"currency"` Locale string `json:"locale,omitempty"` Country string `json:"country"` CustomerEmail string `json:"customerEmail,omitempty"` CustomerName string `json:"customerName,omitempty"` Lines []OrderLine `json:"lines"` Payment struct { Provider string `json:"provider"` Reference string `json:"reference"` Amount int64 `json:"amount"` } `json:"payment"` } // OrderLine is one orderable item in the CreateOrderReq, matching the // lineReq type in the order service. type OrderLine struct { Reference string `json:"reference"` Sku string `json:"sku"` Name string `json:"name"` Quantity int32 `json:"quantity"` UnitPrice int64 `json:"unitPrice"` TaxRate int32 `json:"taxRate"` } // CreateOrderResult is the success response from POST /api/orders/from-checkout. type CreateOrderResult struct { OrderId string `json:"orderId"` Flow json.RawMessage `json:"flow,omitempty"` Order json.RawMessage `json:"order,omitempty"` // Existing is true when the response is a 409 Conflict — the idempotency // key was already used and this is the previously-created order. Existing bool `json:"existing,omitempty"` } // CreateOrder sends a settled checkout's data to the order service and returns // the created (or previously-created, via idempotency) order. An idempotencyKey // should be generated per checkout (e.g. "checkout-{checkoutId}") to guard // against retries. flowName overrides the default flow; use "" for the default. func (c *OrderClient) CreateOrder(ctx context.Context, req CreateOrderReq, flowName string) (*CreateOrderResult, error) { url := c.baseURL + "/api/orders/from-checkout" if flowName != "" { url += "?flow=" + flowName } body, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("order client: marshal: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("order client: new request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("order client: do: %w", err) } defer resp.Body.Close() // Both 201 (created) and 409 (idempotent hit) are valid responses. if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusConflict { return nil, fmt.Errorf("order client: %s", resp.Status) } var result CreateOrderResult if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("order client: decode: %w", err) } result.Existing = resp.StatusCode == http.StatusConflict return &result, nil }