113 lines
3.8 KiB
Go
113 lines
3.8 KiB
Go
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
|
|
}
|