693 lines
20 KiB
Go
693 lines
20 KiB
Go
package ucp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
|
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
|
profilePkg "git.k6n.net/mats/go-cart-actor/pkg/profile"
|
|
checkoutMessages "git.k6n.net/mats/go-cart-actor/proto/checkout"
|
|
"git.k6n.net/mats/platform/money"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
// CheckoutServer is the UCP REST adapter over the checkout grain pool.
|
|
type CheckoutServer struct {
|
|
applier CheckoutApplier
|
|
profileApplier ProfileApplier // optional; when set links checkouts/orders to profiles
|
|
orderSvc OrderApplier // optional; when set creates real orders on complete
|
|
}
|
|
|
|
// NewCheckoutServer builds a UCP REST adapter over a checkout grain pool.
|
|
func NewCheckoutServer(applier CheckoutApplier, orderSvc ...OrderApplier) *CheckoutServer {
|
|
s := &CheckoutServer{applier: applier}
|
|
if len(orderSvc) > 0 {
|
|
s.orderSvc = orderSvc[0]
|
|
}
|
|
return s
|
|
}
|
|
|
|
// SetProfileApplier enables identity linking on this server.
|
|
func (s *CheckoutServer) SetProfileApplier(pa ProfileApplier) {
|
|
s.profileApplier = pa
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UCP Checkout endpoints
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// handleCreateCheckout handles POST /checkout-sessions.
|
|
//
|
|
// Initiates a checkout session from a cart. The cart id is required.
|
|
func (s *CheckoutServer) handleCreateCheckout(w http.ResponseWriter, r *http.Request) {
|
|
var req CreateCheckoutRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
|
return
|
|
}
|
|
|
|
items := req.Items
|
|
if len(items) == 0 && len(req.LineItems) > 0 {
|
|
items = req.LineItems
|
|
}
|
|
|
|
var cartIdStr string
|
|
if req.CartId != "" {
|
|
cartIdStr = req.CartId
|
|
} else if len(items) > 0 {
|
|
// Fetch cart base URL
|
|
cartBaseURL := os.Getenv("CART_INTERNAL_URL")
|
|
if cartBaseURL == "" {
|
|
cartBaseURL = "http://cart:8080"
|
|
}
|
|
|
|
// Prepare POST request to /ucp/v1/carts/ to create and seed the cart
|
|
createReqBody := CreateCartRequest{
|
|
Items: items,
|
|
}
|
|
bodyBytes, err := json.Marshal(createReqBody)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to marshal create cart request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
cartReq, err := http.NewRequestWithContext(r.Context(), "POST", fmt.Sprintf("%s/ucp/v1/carts/", cartBaseURL), bytes.NewReader(bodyBytes))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to create post cart request: "+err.Error())
|
|
return
|
|
}
|
|
cartReq.Header.Set("Content-Type", "application/json")
|
|
|
|
profile := os.Getenv("UCP_AGENT_PROFILE")
|
|
if profile == "" {
|
|
profile = DefaultUCPAgentProfile
|
|
}
|
|
cartReq.Header.Set("UCP-Agent", `profile="`+profile+`"`)
|
|
|
|
cartResp, err := client.Do(cartReq)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to create implicit cart: "+err.Error())
|
|
return
|
|
}
|
|
defer cartResp.Body.Close()
|
|
|
|
if cartResp.StatusCode != http.StatusCreated && cartResp.StatusCode != http.StatusOK {
|
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("cart service returned status: %d when creating implicit cart", cartResp.StatusCode))
|
|
return
|
|
}
|
|
|
|
var cartRespBody CartResponse
|
|
if err := json.NewDecoder(cartResp.Body).Decode(&cartRespBody); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to decode implicit cart response: "+err.Error())
|
|
return
|
|
}
|
|
cartIdStr = cartRespBody.Id
|
|
} else {
|
|
writeError(w, http.StatusBadRequest, "cartId or items/line_items is required")
|
|
return
|
|
}
|
|
|
|
cartId, ok := cart.ParseCartId(cartIdStr)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid cartId")
|
|
return
|
|
}
|
|
|
|
// Create a new checkout session id.
|
|
checkoutId, err := cart.NewCartId()
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to generate checkout id")
|
|
return
|
|
}
|
|
|
|
// Fetch cart state from cart service
|
|
cartBaseURL := os.Getenv("CART_INTERNAL_URL")
|
|
if cartBaseURL == "" {
|
|
cartBaseURL = "http://cart:8080"
|
|
}
|
|
url := fmt.Sprintf("%s/cart/byid/%s", cartBaseURL, cartId.String())
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
cartReq, err := http.NewRequestWithContext(r.Context(), "GET", url, nil)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to create cart request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
profile := os.Getenv("UCP_AGENT_PROFILE")
|
|
if profile == "" {
|
|
profile = DefaultUCPAgentProfile
|
|
}
|
|
cartReq.Header.Set("UCP-Agent", `profile="`+profile+`"`)
|
|
|
|
cartResp, err := client.Do(cartReq)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to fetch cart: "+err.Error())
|
|
return
|
|
}
|
|
defer cartResp.Body.Close()
|
|
|
|
if cartResp.StatusCode != http.StatusOK {
|
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("cart service returned status: %d", cartResp.StatusCode))
|
|
return
|
|
}
|
|
|
|
cartStateBytes, err := io.ReadAll(cartResp.Body)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to read cart state: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Initialize checkout with cart reference and state.
|
|
initMsg := &checkoutMessages.InitializeCheckout{
|
|
OrderId: "",
|
|
CartId: uint64(cartId),
|
|
CartState: &anypb.Any{
|
|
TypeUrl: "type.googleapis.com/cart.CartGrain",
|
|
Value: cartStateBytes,
|
|
},
|
|
}
|
|
|
|
if _, err := s.applier.Apply(r.Context(), uint64(checkoutId), initMsg); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to initialize checkout: "+err.Error())
|
|
return
|
|
}
|
|
|
|
g, err := s.applier.Get(r.Context(), uint64(checkoutId))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to read checkout: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Link checkout to customer profile if identity linking is enabled.
|
|
if s.profileApplier != nil && req.CustomerId != "" {
|
|
if pid, ok := profilePkg.ParseProfileId(req.CustomerId); ok {
|
|
if err := linkCheckoutToProfile(s.profileApplier, r.Context(), uint64(pid), uint64(checkoutId), uint64(cartId)); err != nil {
|
|
log.Printf("identity: failed to link checkout %d to profile %s: %v", uint64(checkoutId), req.CustomerId, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, checkoutGrainToResponse(checkoutId.String(), g))
|
|
}
|
|
|
|
// handleGetCheckout handles GET /checkout-sessions/{id}.
|
|
func (s *CheckoutServer) handleGetCheckout(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseSessionID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid checkout session id")
|
|
return
|
|
}
|
|
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "checkout session not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, checkoutGrainToResponse(r.PathValue("id"), g))
|
|
}
|
|
|
|
// handleUpdateCheckout handles PUT /checkout-sessions/{id}.
|
|
//
|
|
// Updates checkout with delivery, contact, or pickup point info.
|
|
func (s *CheckoutServer) handleUpdateCheckout(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseSessionID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid checkout session id")
|
|
return
|
|
}
|
|
|
|
var req UpdateCheckoutRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Apply delivery.
|
|
if req.Delivery != nil {
|
|
msg := &checkoutMessages.SetDelivery{
|
|
Provider: req.Delivery.Provider,
|
|
}
|
|
for _, itemId := range req.Delivery.ItemIds {
|
|
msg.Items = append(msg.Items, itemId)
|
|
}
|
|
if _, err := s.applier.Apply(r.Context(), id, msg); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to set delivery: "+err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
// Apply pickup point.
|
|
if req.PickupPoint != nil {
|
|
pp := &checkoutMessages.SetPickupPoint{
|
|
DeliveryId: req.PickupPoint.DeliveryId,
|
|
PickupPoint: &checkoutMessages.PickupPoint{
|
|
Id: req.PickupPoint.Id,
|
|
Name: req.PickupPoint.Name,
|
|
},
|
|
}
|
|
if _, err := s.applier.Apply(r.Context(), id, pp); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to set pickup point: "+err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
// Apply contact details.
|
|
if req.Contact != nil {
|
|
msg := &checkoutMessages.ContactDetailsUpdated{
|
|
Email: req.Contact.Email,
|
|
Phone: req.Contact.Phone,
|
|
Name: req.Contact.Name,
|
|
PostalCode: req.Contact.PostalCode,
|
|
}
|
|
if _, err := s.applier.Apply(r.Context(), id, msg); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to update contact details: "+err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to read checkout: "+err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, checkoutGrainToResponse(r.PathValue("id"), g))
|
|
}
|
|
|
|
// handleCompleteCheckout handles POST /checkout-sessions/{id}/complete.
|
|
//
|
|
// Completes the checkout session and creates an order. When an OrderApplier
|
|
// is configured on the server, a real order is created by calling the order
|
|
// service. Otherwise, a simplified order reference is generated locally.
|
|
func (s *CheckoutServer) handleCompleteCheckout(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseSessionID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid checkout session id")
|
|
return
|
|
}
|
|
|
|
var req CompleteCheckoutRequest
|
|
json.NewDecoder(r.Body).Decode(&req) // best-effort
|
|
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "checkout session not found")
|
|
return
|
|
}
|
|
|
|
// Default currency/country from the request or fall back to cart/cart state.
|
|
currency := req.Currency
|
|
country := req.Country
|
|
if currency == "" && g.CartState != nil {
|
|
currency = g.CartState.Currency
|
|
}
|
|
if currency == "" {
|
|
currency = "SEK"
|
|
}
|
|
|
|
var orderID string
|
|
|
|
if s.orderSvc != nil {
|
|
// Real order creation via the configured OrderApplier.
|
|
orderID, err = s.orderSvc.CreateOrder(r.Context(), id, g, req.Provider, req.PaymentToken, currency, country)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to create order: "+err.Error())
|
|
return
|
|
}
|
|
} else {
|
|
// Fallback: generate a local order reference (simplified).
|
|
var ref cart.CartId
|
|
ref, err = cart.NewCartId()
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to generate order reference")
|
|
return
|
|
}
|
|
orderID = ref.String()
|
|
}
|
|
|
|
// Record the order on the checkout grain.
|
|
if _, err := s.applier.Apply(r.Context(), id,
|
|
&checkoutMessages.OrderCreated{
|
|
OrderId: orderID,
|
|
CreatedAt: timestamppb.New(time.Now()),
|
|
},
|
|
); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to record order: "+err.Error())
|
|
return
|
|
}
|
|
|
|
g, err = s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to read checkout: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Auto-link order to customer profile if identity linking is enabled.
|
|
if s.profileApplier != nil && orderID != "" && req.CustomerId != "" {
|
|
if pid, ok := profilePkg.ParseProfileId(req.CustomerId); ok {
|
|
if err := linkOrderToProfile(s.profileApplier, r.Context(), uint64(pid), orderID, uint64(g.CartId), "placed"); err != nil {
|
|
log.Printf("identity: failed to link order %s to profile %s: %v", orderID, req.CustomerId, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, checkoutGrainToResponse(r.PathValue("id"), g))
|
|
}
|
|
|
|
// handleCancelCheckout handles POST /checkout-sessions/{id}/cancel.
|
|
//
|
|
// Cancels the checkout session.
|
|
func (s *CheckoutServer) handleCancelCheckout(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseSessionID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid checkout session id")
|
|
return
|
|
}
|
|
|
|
// Cancel all active payments.
|
|
for {
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
break
|
|
}
|
|
openPayments := g.OpenPayments()
|
|
if len(openPayments) == 0 {
|
|
break
|
|
}
|
|
for _, p := range openPayments {
|
|
s.applier.Apply(r.Context(), id, &checkoutMessages.CancelPayment{
|
|
PaymentId: p.PaymentId,
|
|
CancelledAt: timestamppb.New(time.Now()),
|
|
})
|
|
}
|
|
}
|
|
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "checkout session not found")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, checkoutGrainToResponse(r.PathValue("id"), g))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// OrderHTTPClient — an OrderApplier backed by the order service HTTP API
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// OrderHTTPClient implements OrderApplier by POSTing to an order service's
|
|
// /api/orders/from-checkout endpoint. It mirrors the existing -> order handoff
|
|
// used by the checkout service's own Klarna/Adyen callbacks.
|
|
type OrderHTTPClient struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
agentProfile string // UCP-Agent profile URI
|
|
}
|
|
|
|
// NewOrderHTTPClient returns an order applier that creates orders by calling
|
|
// the order service at baseURL (e.g. "http://order-service:8092"). It reads
|
|
// the UCP_AGENT_PROFILE env var for the profile URI, falling back to
|
|
// DefaultUCPAgentProfile from the ucp package.
|
|
func NewOrderHTTPClient(baseURL string) *OrderHTTPClient {
|
|
profile := os.Getenv("UCP_AGENT_PROFILE")
|
|
if profile == "" {
|
|
profile = DefaultUCPAgentProfile
|
|
}
|
|
return &OrderHTTPClient{
|
|
baseURL: baseURL,
|
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
|
agentProfile: profile,
|
|
}
|
|
}
|
|
|
|
// fromCheckoutReq mirrors cmd/order/handlers_checkout.go's internal type.
|
|
type fromCheckoutReq struct {
|
|
CheckoutId string `json:"checkoutId"`
|
|
IdempotencyKey string `json:"idempotencyKey"`
|
|
CartId string `json:"cartId"`
|
|
Currency string `json:"currency"`
|
|
Country string `json:"country"`
|
|
CustomerEmail string `json:"customerEmail,omitempty"`
|
|
CustomerName string `json:"customerName,omitempty"`
|
|
Lines []orderLine `json:"lines"`
|
|
Payment orderPayment `json:"payment"`
|
|
}
|
|
|
|
type orderLine struct {
|
|
Reference string `json:"reference"`
|
|
Sku string `json:"sku"`
|
|
Name string `json:"name"`
|
|
Quantity int32 `json:"quantity"`
|
|
UnitPrice money.Cents `json:"unitPrice"`
|
|
TaxRate int32 `json:"taxRate"`
|
|
DropShip bool `json:"drop_ship,omitempty"`
|
|
Location string `json:"location,omitempty"`
|
|
}
|
|
|
|
type orderPayment struct {
|
|
Provider string `json:"provider"`
|
|
Reference string `json:"reference"`
|
|
Amount money.Cents `json:"amount"`
|
|
}
|
|
|
|
// CreateOrder implements OrderApplier. It builds a from-checkout request from
|
|
// the checkout grain's state and POSTs it to the order service.
|
|
func (c *OrderHTTPClient) CreateOrder(ctx context.Context, checkoutID uint64, g *checkout.CheckoutGrain, provider, reference, currency, country string) (string, error) {
|
|
if g.CartState == nil {
|
|
return "", fmt.Errorf("order: checkout %d has no cart state", checkoutID)
|
|
}
|
|
|
|
var customerEmail, customerName string
|
|
if g.ContactDetails != nil {
|
|
if g.ContactDetails.Email != nil {
|
|
customerEmail = *g.ContactDetails.Email
|
|
}
|
|
if g.ContactDetails.Name != nil {
|
|
customerName = *g.ContactDetails.Name
|
|
}
|
|
}
|
|
|
|
// Compute total from cart items + deliveries.
|
|
var totalAmount money.Cents
|
|
lines := buildOrderLines(g)
|
|
for _, l := range lines {
|
|
totalAmount += l.UnitPrice.Mul(int64(l.Quantity))
|
|
}
|
|
|
|
req := fromCheckoutReq{
|
|
CheckoutId: fmt.Sprintf("%d", checkoutID),
|
|
IdempotencyKey: fmt.Sprintf("ucp-checkout-%d", checkoutID),
|
|
CartId: g.CartId.String(),
|
|
Currency: currency,
|
|
Country: country,
|
|
CustomerEmail: customerEmail,
|
|
CustomerName: customerName,
|
|
Lines: lines,
|
|
Payment: orderPayment{
|
|
Provider: provider,
|
|
Reference: reference,
|
|
Amount: totalAmount,
|
|
},
|
|
}
|
|
|
|
body, err := json.Marshal(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("order: marshal: %w", err)
|
|
}
|
|
|
|
url := c.baseURL + "/api/orders/from-checkout"
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return "", fmt.Errorf("order: new request: %w", err)
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
// Advertise this platform's UCP profile on all outgoing requests.
|
|
WithUCPAgentOnRequest(httpReq, c.agentProfile)
|
|
|
|
resp, err := c.httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return "", fmt.Errorf("order: do: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Both 201 and 409 (idempotent hit) are valid.
|
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusConflict {
|
|
return "", fmt.Errorf("order: %s", resp.Status)
|
|
}
|
|
|
|
var result struct {
|
|
OrderId string `json:"orderId"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return "", fmt.Errorf("order: decode: %w", err)
|
|
}
|
|
if result.OrderId == "" {
|
|
return "", fmt.Errorf("order: empty orderId in response")
|
|
}
|
|
return result.OrderId, nil
|
|
}
|
|
|
|
// buildOrderLines converts a checkout grain's cart items and delivery selections
|
|
// into order lines, matching the format expected by the order service.
|
|
func buildOrderLines(g *checkout.CheckoutGrain) []orderLine {
|
|
lines := make([]orderLine, 0, len(g.CartState.Items)+len(g.Deliveries))
|
|
for _, it := range g.CartState.Items {
|
|
if it == nil {
|
|
continue
|
|
}
|
|
name := ""
|
|
if it.Meta != nil {
|
|
name = it.Meta.Name
|
|
}
|
|
location := ""
|
|
if it.StoreId != nil {
|
|
location = *it.StoreId
|
|
}
|
|
lines = append(lines, orderLine{
|
|
Reference: it.Sku,
|
|
Sku: it.Sku,
|
|
Name: name,
|
|
Quantity: int32(it.Quantity),
|
|
UnitPrice: it.Price.IncVat,
|
|
// CartItem.Tax and OrderLine.TaxRate share the platform basis-point scale
|
|
// (2500 = 25%), so the rate passes through unchanged.
|
|
TaxRate: int32(it.Tax),
|
|
DropShip: it.DropShip,
|
|
Location: location,
|
|
})
|
|
}
|
|
for _, d := range g.Deliveries {
|
|
if d == nil || d.Price.IncVat <= 0 {
|
|
continue
|
|
}
|
|
lines = append(lines, orderLine{
|
|
Reference: d.Provider,
|
|
Sku: d.Provider,
|
|
Name: "Delivery",
|
|
Quantity: 1,
|
|
UnitPrice: d.Price.IncVat,
|
|
TaxRate: 2500, // 25% in basis points
|
|
})
|
|
}
|
|
return lines
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: parse session id from path
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func parseSessionID(r *http.Request) (uint64, bool) {
|
|
raw := r.PathValue("id")
|
|
if raw == "" {
|
|
return 0, false
|
|
}
|
|
cid, ok := cart.ParseCartId(raw)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return uint64(cid), true
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: convert CheckoutGrain → UCP CheckoutResponse
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func checkoutGrainToResponse(id string, g *checkout.CheckoutGrain) CheckoutResponse {
|
|
resp := CheckoutResponse{
|
|
Id: id,
|
|
CartId: g.CartId.String(),
|
|
Status: "active",
|
|
Deliveries: make([]DeliveryResponse, 0, len(g.Deliveries)),
|
|
Payments: make([]PaymentResponse, 0, len(g.Payments)),
|
|
Totals: Totals{Currency: "SEK"},
|
|
}
|
|
|
|
if g.OrderId != nil && *g.OrderId != "" {
|
|
resp.OrderId = g.OrderId
|
|
resp.Status = "completed"
|
|
}
|
|
|
|
if g.CartState != nil {
|
|
for _, it := range g.CartState.Items {
|
|
if it == nil {
|
|
continue
|
|
}
|
|
item := CartItem{
|
|
Sku: it.Sku,
|
|
Quantity: int(it.Quantity),
|
|
UnitPrice: it.Price.IncVat,
|
|
TotalPrice: it.TotalPrice.IncVat,
|
|
TaxRate: it.Tax,
|
|
}
|
|
if it.Meta != nil {
|
|
item.Name = it.Meta.Name
|
|
item.Image = it.Meta.Image
|
|
}
|
|
resp.Items = append(resp.Items, item)
|
|
}
|
|
if g.CartState.TotalPrice != nil {
|
|
resp.Totals.TotalIncVat = g.CartState.TotalPrice.IncVat
|
|
resp.Totals.TotalExVat = g.CartState.TotalPrice.ValueExVat()
|
|
resp.Totals.TotalVat = g.CartState.TotalPrice.TotalVat()
|
|
}
|
|
}
|
|
|
|
for _, d := range g.Deliveries {
|
|
if d == nil {
|
|
continue
|
|
}
|
|
dr := DeliveryResponse{
|
|
Id: d.Id,
|
|
Provider: d.Provider,
|
|
Price: d.Price.IncVat,
|
|
Items: d.Items,
|
|
}
|
|
if d.PickupPoint != nil {
|
|
dr.PickupPoint = &PickupPointResp{
|
|
Id: d.PickupPoint.Id,
|
|
Name: d.PickupPoint.Name,
|
|
Address: d.PickupPoint.Address,
|
|
City: d.PickupPoint.City,
|
|
Zip: d.PickupPoint.Zip,
|
|
}
|
|
}
|
|
resp.Deliveries = append(resp.Deliveries, dr)
|
|
}
|
|
|
|
if g.ContactDetails != nil {
|
|
resp.Contact = &ContactResponse{
|
|
Email: g.ContactDetails.Email,
|
|
Phone: g.ContactDetails.Phone,
|
|
Name: g.ContactDetails.Name,
|
|
PostalCode: g.ContactDetails.PostalCode,
|
|
}
|
|
}
|
|
|
|
for _, p := range g.Payments {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
resp.Payments = append(resp.Payments, PaymentResponse{
|
|
Id: p.PaymentId,
|
|
Provider: p.Provider,
|
|
Amount: money.Cents(p.Amount),
|
|
Currency: p.Currency,
|
|
Status: string(p.Status),
|
|
})
|
|
}
|
|
|
|
return resp
|
|
}
|