mcp and ucp
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
package ucp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
||||
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// CartServer is the UCP REST adapter over the cart grain pool.
|
||||
type CartServer struct {
|
||||
applier CartApplier
|
||||
newCartId func() (cart.CartId, error)
|
||||
}
|
||||
|
||||
// NewCartServer builds a UCP REST adapter over a cart grain pool.
|
||||
func NewCartServer(applier CartApplier) *CartServer {
|
||||
return &CartServer{
|
||||
applier: applier,
|
||||
newCartId: cart.NewCartId,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UCP Cart endpoints
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// handleCreateCart handles POST /carts.
|
||||
func (s *CartServer) handleCreateCart(w http.ResponseWriter, r *http.Request) {
|
||||
cid, err := s.newCartId()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to generate cart id")
|
||||
return
|
||||
}
|
||||
id := uint64(cid)
|
||||
|
||||
// Seed the cart grain by calling Get once (spawns if new).
|
||||
if _, err := s.applier.Get(r.Context(), id); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create cart: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// If items were supplied, add them.
|
||||
var req CreateCartRequest
|
||||
hasBody := false
|
||||
if r.ContentLength > 0 {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
|
||||
hasBody = true
|
||||
}
|
||||
}
|
||||
if hasBody && len(req.Items) > 0 {
|
||||
groups := buildItemGroups(r.Context(), req.Items, "")
|
||||
if _, err := applyItemGroups(r.Context(), s.applier, id, groups); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to add items: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g, err := s.applier.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to read cart: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, cartGrainToResponse(cid.String(), g))
|
||||
}
|
||||
|
||||
// handleGetCart handles GET /carts/{id}.
|
||||
func (s *CartServer) handleGetCart(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := parseCartID(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid cart id")
|
||||
return
|
||||
}
|
||||
g, err := s.applier.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "cart not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, cartGrainToResponse(r.PathValue("id"), g))
|
||||
}
|
||||
|
||||
// handleUpdateCart handles PUT /carts/{id}.
|
||||
func (s *CartServer) handleUpdateCart(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := parseCartID(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid cart id")
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateCartRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Clear first, then apply.
|
||||
if _, err := s.applier.Apply(r.Context(), id, &messages.ClearCartRequest{}); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to clear cart: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Add items.
|
||||
if len(req.Items) > 0 {
|
||||
groups := buildItemGroups(r.Context(), req.Items, req.Country)
|
||||
if _, err := applyItemGroups(r.Context(), s.applier, id, groups); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to add items: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set user ID.
|
||||
if req.UserId != nil && *req.UserId != "" {
|
||||
if _, err := s.applier.Apply(r.Context(), id, &messages.SetUserId{UserId: *req.UserId}); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to set user: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g, err := s.applier.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to read cart: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, cartGrainToResponse(r.PathValue("id"), g))
|
||||
}
|
||||
|
||||
// handleCancelCart handles POST /carts/{id}/cancel.
|
||||
func (s *CartServer) handleCancelCart(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := parseCartID(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid cart id")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.applier.Apply(r.Context(), id, &messages.ClearCartRequest{}); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to clear cart: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
g, err := s.applier.Get(r.Context(), id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to read cart: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, cartGrainToResponse(r.PathValue("id"), g))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func parseCartID(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
|
||||
}
|
||||
|
||||
func cartGrainToResponse(id string, g *cart.CartGrain) CartResponse {
|
||||
resp := CartResponse{
|
||||
Id: id,
|
||||
Items: make([]CartItem, 0, len(g.Items)),
|
||||
Totals: Totals{Currency: g.Currency},
|
||||
Status: "active",
|
||||
}
|
||||
if g.CheckoutStatus != nil && *g.CheckoutStatus != "" {
|
||||
resp.Status = string(*g.CheckoutStatus)
|
||||
}
|
||||
|
||||
for _, it := range g.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,
|
||||
ItemId: it.Id,
|
||||
Fields: it.CustomFields,
|
||||
}
|
||||
if it.Meta != nil {
|
||||
item.Name = it.Meta.Name
|
||||
item.Image = it.Meta.Image
|
||||
}
|
||||
resp.Items = append(resp.Items, item)
|
||||
}
|
||||
|
||||
if g.TotalPrice != nil {
|
||||
resp.Totals.TotalIncVat = g.TotalPrice.IncVat
|
||||
resp.Totals.TotalExVat = g.TotalPrice.ValueExVat()
|
||||
resp.Totals.TotalVat = g.TotalPrice.TotalVat()
|
||||
}
|
||||
if g.TotalDiscount != nil {
|
||||
resp.Totals.Discount = g.TotalDiscount.IncVat
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Item group building (adapted from cmd/cart/pool-server.go)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type itemGroup struct {
|
||||
parent *messages.AddItem
|
||||
children []*messages.AddItem
|
||||
}
|
||||
|
||||
func buildItemGroups(_ context.Context, items []CartItemInput, _ string) []itemGroup {
|
||||
groups := make([]itemGroup, len(items))
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
for i, itm := range items {
|
||||
wg.Add(1)
|
||||
go func(i int, itm CartItemInput) {
|
||||
defer wg.Done()
|
||||
parentMsg := buildAddItem(itm, 0)
|
||||
mu.Lock()
|
||||
groups[i].parent = parentMsg
|
||||
mu.Unlock()
|
||||
if len(itm.Children) > 0 {
|
||||
children := make([]*messages.AddItem, len(itm.Children))
|
||||
for j, child := range itm.Children {
|
||||
children[j] = buildAddItem(child, parentMsg.ItemId)
|
||||
}
|
||||
mu.Lock()
|
||||
groups[i].children = children
|
||||
mu.Unlock()
|
||||
}
|
||||
}(i, itm)
|
||||
}
|
||||
wg.Wait()
|
||||
return groups
|
||||
}
|
||||
|
||||
func buildAddItem(input CartItemInput, parentItemId uint32) *messages.AddItem {
|
||||
qty := int32(input.Quantity)
|
||||
if qty <= 0 {
|
||||
qty = 1
|
||||
}
|
||||
msg := &messages.AddItem{
|
||||
Sku: input.Sku,
|
||||
Quantity: qty,
|
||||
}
|
||||
if input.StoreId != nil {
|
||||
msg.StoreId = input.StoreId
|
||||
}
|
||||
if input.Sku != "" {
|
||||
// Pass along the item id so findParentLineId can match.
|
||||
msg.ItemId = parentItemId
|
||||
}
|
||||
if len(input.Fields) > 0 {
|
||||
msg.CustomFields = input.Fields
|
||||
}
|
||||
if parentItemId > 0 {
|
||||
msg.ParentId = &parentItemId
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func applyItemGroups(ctx context.Context, applier CartApplier, id uint64, groups []itemGroup) (*actor.MutationResult[cart.CartGrain], error) {
|
||||
var last *actor.MutationResult[cart.CartGrain]
|
||||
for _, g := range groups {
|
||||
if g.parent == nil {
|
||||
continue
|
||||
}
|
||||
res, err := applier.Apply(ctx, id, g.parent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
last = res
|
||||
|
||||
if len(g.children) == 0 {
|
||||
continue
|
||||
}
|
||||
parentLineId, ok := findParentLineId(&res.Result, g.parent)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
childMsgs := make([]proto.Message, len(g.children))
|
||||
for i, c := range g.children {
|
||||
c.ParentId = &parentLineId
|
||||
childMsgs[i] = c
|
||||
}
|
||||
res, err = applier.Apply(ctx, id, childMsgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
last = res
|
||||
}
|
||||
return last, nil
|
||||
}
|
||||
|
||||
func findParentLineId(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
|
||||
}
|
||||
Reference in New Issue
Block a user