Files
go-cart-actor/cmd/cart/pool-server.go
T
mats 9f1eca07e9
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s
Build and Publish / BuildAndDeployArm64 (push) Failing after 7s
parent item mutations
2026-07-08 11:39:42 +02:00

819 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"sync"
"time"
"git.k6n.net/mats/go-cart-actor/pkg/actor"
"git.k6n.net/mats/go-cart-actor/pkg/cart"
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
"git.k6n.net/mats/go-cart-actor/pkg/voucher"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)
// Pool metrics (cart_grain_spawned_total, cart_grain_lookups_total,
// cart_mutations_total, cart_remote_negotiation_total,
// cart_connected_remotes, …) are bumped centrally by SimpleGrainPool
// — see pkg/actor/simple_grain_pool.go. The cart Metrics is
// registered once in cmd/cart/main.go and shared with DiskStorage
// via SetMetrics, so the per-handler counters this file used to
// maintain are no longer needed.
type PoolServer struct {
actor.GrainPool[cart.CartGrain]
pod_name string
// idx is the bus-fed catalog projection cache, consulted at HTTP-fetch
// sites (AddSkuToCartHandler, buildItemGroups) to overlay authoritative
// price / tax_class / display fields onto AddItem before mutation. nil is
// tolerated so handlers still serve if the bus consumer is unavailable
// (orphan / quarantine / CI), with a plain cache-miss fall-through.
idx *catalogProjectionCache
}
func NewPoolServer(pool actor.GrainPool[cart.CartGrain], pod_name string, idx *catalogProjectionCache) *PoolServer {
srv := &PoolServer{
GrainPool: pool,
pod_name: pod_name,
idx: idx,
}
return srv
}
func (s *PoolServer) ApplyLocal(ctx context.Context, id cart.CartId, mutation ...proto.Message) (*actor.MutationResult[cart.CartGrain], error) {
return s.Apply(ctx, uint64(id), mutation...)
}
func (s *PoolServer) GetCartHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
grain, err := s.Get(r.Context(), uint64(id))
if err != nil {
return err
}
return s.WriteResult(w, grain)
}
func (s *PoolServer) AddSkuToCartHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
sku := r.PathValue("sku")
country := getCountryFromHost(r.Host)
if s.idx == nil {
log.Printf("No index present")
}
if s.idx != nil && s.idx.IsDeleted(sku) {
// Bus-deleted SKU — skip the HTTP fetch and reject the add. Mirrors the
// product-fetcher's "product service returned %d for sku %s" shape so
// upstream code that pattern-matches that string still works, AND we
// publish StatusNotFound so the HTTP response carries the right code
// (the handler's error-return path otherwise renders as 500). http.Error
// is the idiomatic stdlib call here — it does the encoding-safe text body
// (no fmt.Sprintf/JSON-string-escaping fragility).
http.Error(w, fmt.Sprintf("product service returned %d for sku %s (bus-deleted)", 404, sku), http.StatusNotFound)
return nil
}
// Cache-only fast path: AddSkuToCartHandler is a SINGLE top-level add
// (qty=1, no children). When the projection carries full authoritative
// fields, we skip PRODUCT_BASE_URL entirely and build the AddItem
// directly from the cache. This eliminates one HTTP round-trip per
// add-to-cart under steady-state bus-fed conditions.
var msg *messages.AddItem
if s.idx != nil {
if p, ok := s.idx.Get(sku); ok && HasRequiredFields(p) {
msg = BuildAddItemFromCache(sku, 1, country, nil, p)
}
}
if msg == nil {
if s.idx == nil {
log.Printf("No item found in index %s", sku)
}
var err error
msg, err = GetItemAddMessage(r.Context(), sku, 1, country, nil)
if err != nil {
return err
}
if s.idx != nil {
if p, ok := s.idx.Get(sku); ok {
ApplyProjectionOverlay(msg, p)
}
}
}
data, err := s.ApplyLocal(r.Context(), id, msg)
if err != nil {
return err
}
return s.WriteResult(w, data)
}
// Pool metrics (cart_grain_spawned_total, cart_grain_lookups_total,
// cart_mutations_total, cart_remote_negotiation_total,
// cart_connected_remotes, …) are bumped centrally by SimpleGrainPool
// — see pkg/actor/simple_grain_pool.go. The cart Metrics is
// registered once in cmd/cart/main.go and shared with DiskStorage
// via SetMetrics, so the per-handler counters this file used to
// maintain are no longer needed.
func (s *PoolServer) WriteResult(w http.ResponseWriter, result any) error {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("X-Pod-Name", s.pod_name)
if result == nil {
w.WriteHeader(http.StatusInternalServerError)
return nil
}
w.WriteHeader(http.StatusOK)
enc := json.NewEncoder(w)
err := enc.Encode(result)
return err
}
func (s *PoolServer) DeleteItemHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
itemIdString := r.PathValue("itemId")
itemId, err := strconv.ParseInt(itemIdString, 10, 64)
if err != nil {
return err
}
data, err := s.ApplyLocal(r.Context(), id, &messages.RemoveItem{Id: uint32(itemId)})
if err != nil {
return err
}
return s.WriteResult(w, data)
}
func (s *PoolServer) QuantityChangeHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
changeQuantity := messages.ChangeQuantity{}
err := json.NewDecoder(r.Body).Decode(&changeQuantity)
if err != nil {
return err
}
reply, err := s.ApplyLocal(r.Context(), id, &changeQuantity)
if err != nil {
return err
}
return s.WriteResult(w, reply)
}
type Item struct {
Sku string `json:"sku"`
Quantity int `json:"quantity"`
StoreId *string `json:"storeId,omitempty"`
// Children are sub-articles (accessories, services, insurance, ...) priced
// relative to this item. They are added as separate cart lines whose
// ParentId points to this item's line-item id.
Children []Item `json:"children,omitempty"`
// CustomFields are optional user-supplied input fields for this line.
CustomFields map[string]string `json:"customFields,omitempty"`
}
type SetCartItems struct {
Country string `json:"country"`
Items []Item `json:"items"`
}
// itemGroup is a top-level item together with its child sub-articles. The child
// AddItem messages are built with their price already derived from the parent
// product; their ParentId is filled in after the parent line is applied.
type itemGroup struct {
parent *messages.AddItem
children []*messages.AddItem
}
// buildItemGroups fetches every product and builds the AddItem messages. Groups
// are built concurrently; within a group the parent is fetched first so it can
// drive child pricing, then children are fetched concurrently. Input order is
// preserved. Items that fail to fetch are skipped (logged), matching the prior
// best-effort behavior.
//
// idx is the bus-fed projection cache; when non-nil, every AddItem built here
// has its cache-covered fields (price/tax_class/display/inventory_tracked/
// drop_ship) overlaid onto the HTTP-fetched answer. Cache-hit calls naturally
// become authoritative for what the projection carries; HTTP stays for
// dimensions (parent width/height for child pricing), seller/orgPrice and the
// dynamic ExtraJson product data. Skipped cleanly when idx is nil.
//
// Cache-only fast path: a parent with NO children AND a cache hit that
// satisfies HasRequiredFields skips PRODUCT_BASE_URL entirely — BuildAddItem-
// FromCache constructs the line directly from the Projection. This eliminates
// the per-add HTTP round-trip under steady-state bus-fed conditions for the
// common top-level add case. The HTTP path continues to handle:
// - cache miss (cold-grace; a SKU not yet bus-fed)
// - HasRequiredFields==false (partial bus delivery, e.g. TaxClass missing
// because the producer didn't classify)
// - parents WITH children (configurator behavior preservation — child
// price = child per-m² rate × parent area, and parent area comes from
// HTTP-fetched width/height that the Projection doesn't carry yet; a
// future schema extension can unblock a child cache-only path).
func buildItemGroups(ctx context.Context, items []Item, country string, idx *catalogProjectionCache) []itemGroup {
groups := make([]itemGroup, len(items))
wg := sync.WaitGroup{}
for i, itm := range items {
wg.Go(func() {
if idx != nil && idx.IsDeleted(itm.Sku) {
log.Printf("error adding item %s: bus-deleted (skipping)", itm.Sku)
return
}
// Cache-only fast path: see package doc above for the eligibility
// contract.
var parentMsg *messages.AddItem
var parentProduct *ProductItem
if len(itm.Children) == 0 && idx != nil {
if p, ok := idx.Get(itm.Sku); ok && HasRequiredFields(p) {
parentMsg = BuildAddItemFromCache(itm.Sku, itm.Quantity, country, itm.StoreId, p)
}
}
if parentMsg == nil {
var err error
parentMsg, parentProduct, err = BuildItemMessage(ctx, itm.Sku, itm.Quantity, country, itm.StoreId, nil)
if err != nil {
log.Printf("error adding item %s: %v", itm.Sku, err)
return
}
if idx != nil {
if p, ok := idx.Get(itm.Sku); ok {
ApplyProjectionOverlay(parentMsg, p)
}
}
}
parentMsg.CustomFields = itm.CustomFields
groups[i].parent = parentMsg
if len(itm.Children) == 0 {
return
}
children := make([]*messages.AddItem, len(itm.Children))
cwg := sync.WaitGroup{}
for j, child := range itm.Children {
cwg.Go(func() {
if idx != nil && idx.IsDeleted(child.Sku) {
log.Printf("error adding child %s of %s: bus-deleted (skipping)", child.Sku, itm.Sku)
return
}
childMsg, _, err := BuildItemMessage(ctx, child.Sku, child.Quantity, country, child.StoreId, parentProduct)
if err != nil {
log.Printf("error adding child %s of %s: %v", child.Sku, itm.Sku, err)
return
}
if idx != nil {
if p, ok := idx.Get(child.Sku); ok {
ApplyProjectionOverlay(childMsg, p)
}
}
childMsg.CustomFields = child.CustomFields
children[j] = childMsg
})
}
cwg.Wait()
for _, c := range children {
if c != nil {
groups[i].children = append(groups[i].children, c)
}
}
})
}
wg.Wait()
return groups
}
// applyItemGroups applies each group in dependency order: the parent first, then
// its children with ParentId set to the parent's resolved line-item id. Returns
// the last mutation result for rendering.
func (s *PoolServer) applyItemGroups(ctx context.Context, id cart.CartId, groups []itemGroup) (*actor.MutationResult[cart.CartGrain], error) {
var last *actor.MutationResult[cart.CartGrain]
for _, g := range groups {
if g.parent == nil {
continue
}
res, err := s.ApplyLocal(ctx, id, g.parent)
if err != nil {
return nil, err
}
last = res
if len(g.children) == 0 {
continue
}
parentLineId, ok := parentLineId(&res.Result, g.parent)
if !ok {
log.Printf("could not resolve parent line for item %d (sku %s); skipping %d children",
g.parent.ItemId, g.parent.Sku, len(g.children))
continue
}
childMsgs := make([]proto.Message, len(g.children))
for i, c := range g.children {
c.ParentId = &parentLineId
childMsgs[i] = c
}
res, err = s.ApplyLocal(ctx, id, childMsgs...)
if err != nil {
return nil, err
}
last = res
}
return last, nil
}
// parentLineId finds the line-item id of the just-applied parent: the resident
// item with the parent's catalog ItemId, matching store, and no parent of its
// own. AddItem merges by sku+store, so at most one such line exists.
func parentLineId(grain *cart.CartGrain, parent *messages.AddItem) (uint32, bool) {
for _, it := range grain.Items {
if it == nil || it.ParentId != nil {
continue
}
if it.ItemId != parent.ItemId {
continue
}
sameStore := (it.StoreId == nil && parent.StoreId == nil) ||
(it.StoreId != nil && parent.StoreId != nil && *it.StoreId == *parent.StoreId)
if sameStore {
return it.Id, true
}
}
return 0, false
}
func (s *PoolServer) SetCartItemsHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
setCartItems := SetCartItems{}
err := json.NewDecoder(r.Body).Decode(&setCartItems)
if err != nil {
return err
}
if _, err := s.ApplyLocal(r.Context(), id, &messages.ClearCartRequest{}); err != nil {
return err
}
groups := buildItemGroups(r.Context(), setCartItems.Items, setCartItems.Country, s.idx)
reply, err := s.applyItemGroups(r.Context(), id, groups)
if err != nil {
return err
}
if reply == nil {
// Cart was cleared but nothing added; return current (empty) state.
grain, err := s.Get(r.Context(), uint64(id))
if err != nil {
return err
}
return s.WriteResult(w, grain)
}
return s.WriteResult(w, reply)
}
func (s *PoolServer) AddMultipleItemHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
setCartItems := SetCartItems{}
err := json.NewDecoder(r.Body).Decode(&setCartItems)
if err != nil {
return err
}
groups := buildItemGroups(r.Context(), setCartItems.Items, setCartItems.Country, s.idx)
reply, err := s.applyItemGroups(r.Context(), id, groups)
if err != nil {
return err
}
if reply == nil {
grain, err := s.Get(r.Context(), uint64(id))
if err != nil {
return err
}
return s.WriteResult(w, grain)
}
return s.WriteResult(w, reply)
}
type AddRequest struct {
Sku string `json:"sku"`
Quantity int32 `json:"quantity"`
Country string `json:"country"`
StoreId *string `json:"storeId"`
// Children are sub-articles priced relative to this item (see Item.Children).
Children []Item `json:"children,omitempty"`
// CustomFields are optional user-supplied input fields for this line.
CustomFields map[string]string `json:"customFields,omitempty"`
}
func (s *PoolServer) AddSkuRequestHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
addRequest := AddRequest{Quantity: 1}
err := json.NewDecoder(r.Body).Decode(&addRequest)
if err != nil {
return err
}
item := Item{
Sku: addRequest.Sku,
Quantity: int(addRequest.Quantity),
StoreId: addRequest.StoreId,
Children: addRequest.Children,
CustomFields: addRequest.CustomFields,
}
groups := buildItemGroups(r.Context(), []Item{item}, addRequest.Country, s.idx)
reply, err := s.applyItemGroups(r.Context(), id, groups)
if err != nil {
return err
}
if reply == nil {
grain, err := s.Get(r.Context(), uint64(id))
if err != nil {
return err
}
return s.WriteResult(w, grain)
}
return s.WriteResult(w, reply)
}
// func (s *PoolServer) HandleConfirmation(w http.ResponseWriter, r *http.Request, id CartId) error {
// orderId := r.PathValue("orderId")
// if orderId == "" {
// return fmt.Errorf("orderId is empty")
// }
// order, err := KlarnaInstance.GetOrder(orderId)
// if err != nil {
// return err
// }
// w.Header().Set("Content-Type", "application/json")
// w.Header().Set("X-Pod-Name", s.pod_name)
// w.Header().Set("Cache-Control", "no-cache")
// w.Header().Set("Access-Control-Allow-Origin", "*")
// w.WriteHeader(http.StatusOK)
// return json.NewEncoder(w).Encode(order)
// }
// func (s *PoolServer) HandleCheckout(w http.ResponseWriter, r *http.Request, id CartId) error {
// klarnaOrder, err := s.CreateOrUpdateCheckout(r.Host, id)
// if err != nil {
// return err
// }
// s.ApplyCheckoutStarted(klarnaOrder, id)
// w.Header().Set("Content-Type", "application/json")
// return json.NewEncoder(w).Encode(klarnaOrder)
// }
//
// Removed leftover legacy block after CookieCartIdHandler (obsolete code referencing cid/legacy)
func (s *PoolServer) RemoveCartCookie(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
// Clear cart cookie (breaking change: do not issue a new legacy id here)
http.SetCookie(w, &http.Cookie{
Name: "cartid",
Value: "",
Path: "/",
Secure: r.TLS != nil,
HttpOnly: true,
Expires: time.Unix(0, 0),
SameSite: http.SameSiteLaxMode,
})
w.WriteHeader(http.StatusOK)
return nil
}
func (s *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error) func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error {
return func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error {
if ownerHost, ok := s.OwnerHost(uint64(cartId)); ok {
ctx, span := tracer.Start(r.Context(), "proxy")
defer span.End()
span.SetAttributes(attribute.String("cartid", cartId.String()))
hostAttr := attribute.String("other host", ownerHost.Name())
span.SetAttributes(hostAttr)
proxyCalls.Add(ctx, 1, metric.WithAttributes(hostAttr))
handled, err := ownerHost.Proxy(uint64(cartId), w, r, nil)
if err == nil && handled {
return nil
}
}
_, span := tracer.Start(r.Context(), "own")
span.SetAttributes(attribute.String("cartid", cartId.String()))
defer span.End()
return fn(w, r, cartId)
}
}
var (
tracer = otel.Tracer(name)
meter = otel.Meter(name)
proxyCalls metric.Int64Counter
)
func init() {
var err error
proxyCalls, err = meter.Int64Counter("proxy.calls",
metric.WithDescription("Number of proxy calls"),
metric.WithUnit("{calls}"))
if err != nil {
panic(err)
}
}
type AddVoucherRequest struct {
VoucherCode string `json:"code"`
}
func (s *PoolServer) AddVoucherHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
data := &AddVoucherRequest{}
json.NewDecoder(r.Body).Decode(data)
v := voucher.Service{}
msg, err := v.GetVoucher(data.VoucherCode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return err
}
reply, err := s.ApplyLocal(r.Context(), cartId, msg)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return err
}
s.WriteResult(w, reply)
return nil
}
type SubscriptionDetailsRequest struct {
Id *string `json:"id,omitempty"`
OfferingCode string `json:"offeringCode,omitempty"`
SigningType string `json:"signingType,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
func (sd *SubscriptionDetailsRequest) ToMessage() *messages.UpsertSubscriptionDetails {
return &messages.UpsertSubscriptionDetails{
Id: sd.Id,
OfferingCode: sd.OfferingCode,
SigningType: sd.SigningType,
Data: &anypb.Any{Value: sd.Data},
}
}
func (s *PoolServer) SubscriptionDetailsHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
data := &SubscriptionDetailsRequest{}
err := json.NewDecoder(r.Body).Decode(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return err
}
reply, err := s.ApplyLocal(r.Context(), cartId, data.ToMessage())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return err
}
s.WriteResult(w, reply)
return nil
}
func (s *PoolServer) RemoveVoucherHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
idStr := r.PathValue("voucherId")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return err
}
reply, err := s.ApplyLocal(r.Context(), cartId, &messages.RemoveVoucher{Id: uint32(id)})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return err
}
s.WriteResult(w, reply)
return nil
}
// SetRecoveryContactRequest is the wire shape clients send to
// PUT /cart/recovery-contact. It mirrors proto SetRecoveryContact with
// JSON-friendly types; sending an empty JSON object clears all contact info.
type SetRecoveryContactRequest struct {
Email string `json:"email,omitempty"`
PushTokens []cart.PushToken `json:"pushTokens,omitempty"`
}
// toMessage builds the proto message from the JSON request body.
func (s *SetRecoveryContactRequest) toMessage() *messages.SetRecoveryContact {
out := &messages.SetRecoveryContact{Email: s.Email}
if len(s.PushTokens) > 0 {
out.PushTokens = make([]*messages.PushToken, 0, len(s.PushTokens))
for _, t := range s.PushTokens {
out.PushTokens = append(out.PushTokens, &messages.PushToken{
Platform: t.Platform,
Token: t.Token,
})
}
}
return out
}
func (s *PoolServer) SetRecoveryContactHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
if r.Method != http.MethodPut {
w.WriteHeader(http.StatusMethodNotAllowed)
return nil
}
req := SetRecoveryContactRequest{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return err
}
reply, err := s.ApplyLocal(r.Context(), cartId, req.toMessage())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return err
}
return s.WriteResult(w, reply)
}
func (s *PoolServer) SetUserIdHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
setUserId := messages.SetUserId{}
err := json.NewDecoder(r.Body).Decode(&setUserId)
if err != nil {
return err
}
reply, err := s.ApplyLocal(r.Context(), cartId, &setUserId)
if err != nil {
return err
}
return s.WriteResult(w, reply)
}
func (s *PoolServer) LineItemMarkingHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
itemIdStr := r.PathValue("itemId")
itemId, err := strconv.ParseInt(itemIdStr, 10, 64)
if err != nil {
return err
}
lineItemMarking := messages.LineItemMarking{Id: uint32(itemId)}
err = json.NewDecoder(r.Body).Decode(&lineItemMarking)
if err != nil {
return err
}
reply, err := s.ApplyLocal(r.Context(), cartId, &lineItemMarking)
if err != nil {
return err
}
return s.WriteResult(w, reply)
}
func (s *PoolServer) RemoveLineItemMarkingHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
itemIdStr := r.PathValue("itemId")
itemId, err := strconv.ParseInt(itemIdStr, 10, 64)
if err != nil {
return err
}
removeLineItemMarking := messages.RemoveLineItemMarking{Id: uint32(itemId)}
reply, err := s.ApplyLocal(r.Context(), cartId, &removeLineItemMarking)
if err != nil {
return err
}
return s.WriteResult(w, reply)
}
type SetCustomFieldsRequest struct {
CustomFields map[string]string `json:"customFields"`
}
func (s *PoolServer) SetCustomFieldsHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
itemId, err := strconv.ParseInt(r.PathValue("itemId"), 10, 64)
if err != nil {
return err
}
req := SetCustomFieldsRequest{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return err
}
reply, err := s.ApplyLocal(r.Context(), cartId, &messages.SetLineItemCustomFields{
Id: uint32(itemId),
CustomFields: req.CustomFields,
})
if err != nil {
return err
}
return s.WriteResult(w, reply)
}
func (s *PoolServer) InternalApplyMutationHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return nil
}
data, err := io.ReadAll(r.Body)
if err != nil {
return err
}
mutation := &messages.Mutation{}
err = proto.Unmarshal(data, mutation)
if err != nil {
return err
}
reply, err := s.ApplyLocal(r.Context(), cartId, mutation)
if err != nil {
return err
}
return s.WriteResult(w, reply)
}
func (s *PoolServer) GetAnywhere(ctx context.Context, cartId cart.CartId) (*cart.CartGrain, error) {
id := uint64(cartId)
if host, isOnOtherHost := s.OwnerHost(id); isOnOtherHost {
return host.Get(ctx, id)
}
return s.Get(ctx, id)
}
func (s *PoolServer) ApplyAnywhere(ctx context.Context, cartId cart.CartId, msgs ...proto.Message) error {
id := uint64(cartId)
if host, isOnOtherHost := s.OwnerHost(id); isOnOtherHost {
_, err := host.Apply(ctx, id, msgs...)
return err
}
_, err := s.Apply(ctx, id, msgs...)
return err
}
func (s *PoolServer) Serve(mux *http.ServeMux) {
// mux.HandleFunc("OPTIONS /cart", func(w http.ResponseWriter, r *http.Request) {
// w.Header().Set("Access-Control-Allow-Origin", "*")
// w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE")
// w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
// w.WriteHeader(http.StatusOK)
// })
handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
attr := attribute.String("http.route", pattern)
mux.HandleFunc(pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
span := trace.SpanFromContext(r.Context())
span.SetName(pattern)
span.SetAttributes(attr)
labeler, _ := otelhttp.LabelerFromContext(r.Context())
labeler.Add(attr)
handlerFunc(w, r)
}))
}
handleFunc("GET /cart", CookieCartIdHandler(s.ProxyHandler(s.GetCartHandler)))
handleFunc("GET /cart/add/{sku}", CookieCartIdHandler(s.ProxyHandler(s.AddSkuToCartHandler)))
handleFunc("POST /cart/add", CookieCartIdHandler(s.ProxyHandler(s.AddMultipleItemHandler)))
handleFunc("POST /cart", CookieCartIdHandler(s.ProxyHandler(s.AddSkuRequestHandler)))
handleFunc("POST /cart/set", CookieCartIdHandler(s.ProxyHandler(s.SetCartItemsHandler)))
handleFunc("DELETE /cart/{itemId}", CookieCartIdHandler(s.ProxyHandler(s.DeleteItemHandler)))
handleFunc("PUT /cart", CookieCartIdHandler(s.ProxyHandler(s.QuantityChangeHandler)))
handleFunc("DELETE /cart", CookieCartIdHandler(s.ProxyHandler(s.RemoveCartCookie)))
handleFunc("PUT /cart/voucher", CookieCartIdHandler(s.ProxyHandler(s.AddVoucherHandler)))
handleFunc("PUT /cart/subscription-details", CookieCartIdHandler(s.ProxyHandler(s.SubscriptionDetailsHandler)))
handleFunc("DELETE /cart/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
handleFunc("PUT /cart/user", CookieCartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
// PUT only — SetRecoveryContact is PUT/replace semantics. We deliberately
// do NOT allow POST here to avoid conflicts with any future POST→create
// semantics on /cart paths.
handleFunc("PUT /cart/recovery-contact", CookieCartIdHandler(s.ProxyHandler(s.SetRecoveryContactHandler)))
handleFunc("PUT /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
handleFunc("DELETE /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
handleFunc("PUT /cart/item/{itemId}/custom-fields", CookieCartIdHandler(s.ProxyHandler(s.SetCustomFieldsHandler)))
//mux.HandleFunc("GET /cart/checkout", CookieCartIdHandler(s.ProxyHandler(s.HandleCheckout)))
//mux.HandleFunc("GET /cart/confirmation/{orderId}", CookieCartIdHandler(s.ProxyHandler(s.HandleConfirmation)))
handleFunc("GET /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.GetCartHandler)))
handleFunc("POST /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.AddSkuRequestHandler)))
handleFunc("DELETE /cart/byid/{id}/{itemId}", CartIdHandler(s.ProxyHandler(s.DeleteItemHandler)))
handleFunc("PUT /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.QuantityChangeHandler)))
handleFunc("PUT /cart/byid/{id}/voucher", CookieCartIdHandler(s.ProxyHandler(s.AddVoucherHandler)))
handleFunc("DELETE /cart/byid/{id}/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
handleFunc("PUT /cart/byid/{id}/user", CartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
handleFunc("PUT /cart/byid/{id}/recovery-contact", CartIdHandler(s.ProxyHandler(s.SetRecoveryContactHandler)))
handleFunc("PUT /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
handleFunc("DELETE /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
handleFunc("PUT /cart/byid/{id}/item/{itemId}/custom-fields", CartIdHandler(s.ProxyHandler(s.SetCustomFieldsHandler)))
}