fixes
This commit is contained in:
@@ -101,17 +101,12 @@ verify_proto:
|
||||
fi
|
||||
@echo "$(GREEN)Proto layout OK (no root-level *.pb.go files).$(RESET)"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
run:
|
||||
REDIS_ADDRESS=10.10.3.18:6379 \
|
||||
REDIS_PASSWORD=slaskredis \
|
||||
CART_DEBUG_PORT=8091 \
|
||||
CART_PORT=8090 \
|
||||
go run ./cmd/cart/
|
||||
|
||||
tidy:
|
||||
@echo "$(YELLOW)Running go mod tidy...$(RESET)"
|
||||
|
||||
+93
-63
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
@@ -47,6 +46,23 @@ var amqpUrl = os.Getenv("AMQP_URL")
|
||||
var redisAddress = os.Getenv("REDIS_ADDRESS")
|
||||
var redisPassword = os.Getenv("REDIS_PASSWORD")
|
||||
|
||||
// getEnv returns the value of the environment variable named by key, or def if unset/empty.
|
||||
func getEnv(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// normalizeListenAddr accepts either a bare port ("8080") or a full listen
|
||||
// address (":8080", "0.0.0.0:8080") and returns a value usable by http.Server.Addr.
|
||||
func normalizeListenAddr(v string) string {
|
||||
if strings.Contains(v, ":") {
|
||||
return v
|
||||
}
|
||||
return ":" + v
|
||||
}
|
||||
|
||||
func getCountryFromHost(host string) string {
|
||||
if strings.Contains(strings.ToLower(host), "-no") {
|
||||
return "no"
|
||||
@@ -83,6 +99,14 @@ func matchesSkuAndLocation(update inventory.InventoryResult, item cart.CartItem)
|
||||
|
||||
func main() {
|
||||
|
||||
// cartPort is the bare HTTP port. It drives both the local listener and the
|
||||
// port used to proxy to peer pods, so a cluster must run all pods on the
|
||||
// same CART_PORT.
|
||||
cartPort := getEnv("CART_PORT", "8080")
|
||||
if i := strings.LastIndex(cartPort, ":"); i >= 0 {
|
||||
cartPort = cartPort[i+1:]
|
||||
}
|
||||
|
||||
controlPlaneConfig := actor.DefaultServerConfig()
|
||||
|
||||
promotionData, err := promotions.LoadStateFile("data/promotions.json")
|
||||
@@ -92,25 +116,25 @@ func main() {
|
||||
|
||||
log.Printf("loaded %d promotions", len(promotionData.State.Promotions))
|
||||
|
||||
inventoryPubSub := actor.NewPubSub[inventory.InventoryChange]()
|
||||
//inventoryPubSub := actor.NewPubSub[inventory.InventoryChange]()
|
||||
|
||||
// promotionService := promotions.NewPromotionService(nil)
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddress,
|
||||
Password: redisPassword,
|
||||
DB: 0,
|
||||
})
|
||||
inventoryService, err := inventory.NewRedisInventoryService(rdb)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating inventory service: %v\n", err)
|
||||
}
|
||||
// rdb := redis.NewClient(&redis.Options{
|
||||
// Addr: redisAddress,
|
||||
// Password: redisPassword,
|
||||
// DB: 0,
|
||||
// })
|
||||
// inventoryService, err := inventory.NewRedisInventoryService(rdb)
|
||||
// if err != nil {
|
||||
// log.Fatalf("Error creating inventory service: %v\n", err)
|
||||
// }
|
||||
|
||||
inventoryReservationService, err := inventory.NewRedisCartReservationService(rdb)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating inventory reservation service: %v\n", err)
|
||||
}
|
||||
// inventoryReservationService, err := inventory.NewRedisCartReservationService(rdb)
|
||||
// if err != nil {
|
||||
// log.Fatalf("Error creating inventory reservation service: %v\n", err)
|
||||
// }
|
||||
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(inventoryReservationService))
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
reg.RegisterProcessor(
|
||||
actor.NewMutationProcessor(func(ctx context.Context, g *cart.CartGrain) error {
|
||||
_, span := tracer.Start(ctx, "Totals and promotions")
|
||||
@@ -140,46 +164,46 @@ func main() {
|
||||
ret := cart.NewCartGrain(id, time.Now())
|
||||
// Set baseline lastChange at spawn; replay may update it to last event timestamp.
|
||||
|
||||
inventoryPubSub.Subscribe(ret.HandleInventoryChange)
|
||||
// inventoryPubSub.Subscribe(ret.HandleInventoryChange)
|
||||
err := diskStorage.LoadEvents(ctx, id, ret)
|
||||
if err == nil && inventoryService != nil {
|
||||
refs := make([]*inventory.InventoryReference, 0)
|
||||
for _, item := range ret.Items {
|
||||
refs = append(refs, &inventory.InventoryReference{
|
||||
SKU: inventory.SKU(item.Sku),
|
||||
LocationID: getLocationId(item),
|
||||
})
|
||||
}
|
||||
_, span := tracer.Start(ctx, "update inventory")
|
||||
defer span.End()
|
||||
res, err := inventoryService.GetInventoryBatch(ctx, refs...)
|
||||
if err != nil {
|
||||
log.Printf("unable to update inventory %v", err)
|
||||
} else {
|
||||
for _, update := range res {
|
||||
for _, item := range ret.Items {
|
||||
if matchesSkuAndLocation(update, *item) && update.Quantity != uint32(item.Stock) {
|
||||
// maybe apply an update to give visibility to the cart
|
||||
item.Stock = uint16(update.Quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// if err == nil && inventoryService != nil {
|
||||
// refs := make([]*inventory.InventoryReference, 0)
|
||||
// for _, item := range ret.Items {
|
||||
// refs = append(refs, &inventory.InventoryReference{
|
||||
// SKU: inventory.SKU(item.Sku),
|
||||
// LocationID: getLocationId(item),
|
||||
// })
|
||||
// }
|
||||
// _, span := tracer.Start(ctx, "update inventory")
|
||||
// defer span.End()
|
||||
// res, err := inventoryService.GetInventoryBatch(ctx, refs...)
|
||||
// if err != nil {
|
||||
// log.Printf("unable to update inventory %v", err)
|
||||
// } else {
|
||||
// for _, update := range res {
|
||||
// for _, item := range ret.Items {
|
||||
// if matchesSkuAndLocation(update, *item) && update.Quantity != uint32(item.Stock) {
|
||||
// // maybe apply an update to give visibility to the cart
|
||||
// item.Stock = uint16(update.Quantity)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return ret, err
|
||||
},
|
||||
Destroy: func(grain actor.Grain[cart.CartGrain]) error {
|
||||
cart, err := grain.GetCurrentState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inventoryPubSub.Unsubscribe(cart.HandleInventoryChange)
|
||||
// cart, err := grain.GetCurrentState()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//inventoryPubSub.Unsubscribe(cart.HandleInventoryChange)
|
||||
|
||||
return nil
|
||||
},
|
||||
SpawnHost: func(host string) (actor.Host[cart.CartGrain], error) {
|
||||
return proxy.NewRemoteHost[cart.CartGrain](host)
|
||||
return proxy.NewRemoteHost[cart.CartGrain](host, cartPort)
|
||||
},
|
||||
TTL: 5 * time.Minute,
|
||||
PoolSize: 2 * 65535,
|
||||
@@ -191,7 +215,7 @@ func main() {
|
||||
log.Fatalf("Error creating cart pool: %v\n", err)
|
||||
}
|
||||
|
||||
syncedServer := NewPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), inventoryService, inventoryReservationService)
|
||||
syncedServer := NewPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp)) //inventoryService, inventoryReservationService)
|
||||
|
||||
app := &App{
|
||||
pool: pool,
|
||||
@@ -262,10 +286,16 @@ func main() {
|
||||
|
||||
mux.HandleFunc("/openapi.json", ServeEmbeddedOpenAPI)
|
||||
|
||||
httpAddr := normalizeListenAddr(cartPort)
|
||||
debugAddr := normalizeListenAddr(getEnv("CART_DEBUG_PORT", "8081"))
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":8080",
|
||||
Addr: httpAddr,
|
||||
BaseContext: func(net.Listener) context.Context { return ctx },
|
||||
ReadTimeout: 10 * time.Second,
|
||||
// Close idle keep-alive connections so a load test with many short-lived
|
||||
// clients doesn't pin file handles open indefinitely.
|
||||
IdleTimeout: 60 * time.Second,
|
||||
WriteTimeout: 20 * time.Second,
|
||||
Handler: otelhttp.NewHandler(mux, "/"),
|
||||
}
|
||||
@@ -284,23 +314,23 @@ func main() {
|
||||
srvErr <- srv.ListenAndServe()
|
||||
}()
|
||||
|
||||
listener := inventory.NewInventoryChangeListener(rdb, context.Background(), func(changes []inventory.InventoryChange) {
|
||||
for _, change := range changes {
|
||||
log.Printf("inventory change: %v", change)
|
||||
inventoryPubSub.Publish(change)
|
||||
}
|
||||
})
|
||||
// listener := inventory.NewInventoryChangeListener(rdb, context.Background(), func(changes []inventory.InventoryChange) {
|
||||
// for _, change := range changes {
|
||||
// log.Printf("inventory change: %v", change)
|
||||
// inventoryPubSub.Publish(change)
|
||||
// }
|
||||
// })
|
||||
|
||||
go func() {
|
||||
err := listener.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to start inventory listener: %v", err)
|
||||
}
|
||||
}()
|
||||
// go func() {
|
||||
// err := listener.Start()
|
||||
// if err != nil {
|
||||
// log.Fatalf("Unable to start inventory listener: %v", err)
|
||||
// }
|
||||
// }()
|
||||
|
||||
log.Print("Server started at port 8080")
|
||||
log.Printf("Server started at %s (debug %s)", httpAddr, debugAddr)
|
||||
|
||||
go http.ListenAndServe(":8081", debugMux)
|
||||
go http.ListenAndServe(debugAddr, debugMux)
|
||||
|
||||
select {
|
||||
case err = <-srvErr:
|
||||
|
||||
+81
-4
@@ -358,6 +358,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cart/item/{itemId}/custom-fields": {
|
||||
"put": {
|
||||
"summary": "Set/merge custom input fields on a line item",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "itemId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "integer", "format": "int64", "minimum": 0 },
|
||||
"description": "Internal cart line item identifier."
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/SetCustomFieldsRequest" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Custom fields updated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CartGrain" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Invalid body or id" },
|
||||
"500": { "description": "Server error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cart/item/{itemId}/marking": {
|
||||
"put": {
|
||||
"summary": "Set marking for line item",
|
||||
@@ -955,10 +989,14 @@
|
||||
},
|
||||
"CartItem": {
|
||||
"type": "object",
|
||||
"description": "Cart line item. Beyond the typed properties below, arbitrary dynamic product data is flattened onto the object as additional top-level keys (see additionalProperties).",
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"itemId": { "type": "integer" },
|
||||
"parentId": { "type": "integer" },
|
||||
"parentId": {
|
||||
"type": "integer",
|
||||
"description": "Line-item id (the `id` field) of the parent item, set when this line is a child/sub-article"
|
||||
},
|
||||
"sku": { "type": "string" },
|
||||
"price": { "$ref": "#/components/schemas/Price" },
|
||||
"totalPrice": { "$ref": "#/components/schemas/Price" },
|
||||
@@ -975,11 +1013,19 @@
|
||||
"meta": { "$ref": "#/components/schemas/ItemMeta" },
|
||||
"saleStatus": { "type": "string" },
|
||||
"marking": { "$ref": "#/components/schemas/Marking" },
|
||||
"customFields": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" },
|
||||
"description": "User-supplied custom input fields for this line"
|
||||
},
|
||||
"subscriptionDetailsId": { "type": "string" },
|
||||
"orderReference": { "type": "string" },
|
||||
"isSubscribed": { "type": "boolean" }
|
||||
},
|
||||
"required": ["id", "sku", "price", "qty"]
|
||||
"required": ["id", "sku", "price", "qty"],
|
||||
"additionalProperties": {
|
||||
"description": "Dynamic product data carried through verbatim (e.g. glas, hangning, materialkular). Value can be any JSON type."
|
||||
}
|
||||
},
|
||||
"CartDelivery": {
|
||||
"type": "object",
|
||||
@@ -1021,7 +1067,17 @@
|
||||
"type": "string",
|
||||
"description": "Two-letter country code (inferred if omitted)"
|
||||
},
|
||||
"storeId": { "type": "string", "nullable": true }
|
||||
"storeId": { "type": "string", "nullable": true },
|
||||
"children": {
|
||||
"type": "array",
|
||||
"description": "Sub-articles (accessories, services, insurance, ...) added as separate cart lines whose parentId points to this item's line, and priced relative to this parent item.",
|
||||
"items": { "$ref": "#/components/schemas/Item" }
|
||||
},
|
||||
"customFields": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" },
|
||||
"description": "Optional user-supplied input fields for this line"
|
||||
}
|
||||
},
|
||||
"required": ["sku"]
|
||||
},
|
||||
@@ -1042,7 +1098,17 @@
|
||||
"properties": {
|
||||
"sku": { "type": "string" },
|
||||
"quantity": { "type": "integer", "minimum": 1 },
|
||||
"storeId": { "type": "string", "nullable": true }
|
||||
"storeId": { "type": "string", "nullable": true },
|
||||
"children": {
|
||||
"type": "array",
|
||||
"description": "Sub-articles (accessories, services, insurance, ...). Each is added as a separate cart line whose parentId points to this item's line, and is priced relative to this parent item.",
|
||||
"items": { "$ref": "#/components/schemas/Item" }
|
||||
},
|
||||
"customFields": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" },
|
||||
"description": "Optional user-supplied input fields for this line"
|
||||
}
|
||||
},
|
||||
"required": ["sku", "quantity"]
|
||||
},
|
||||
@@ -1121,6 +1187,17 @@
|
||||
},
|
||||
"required": ["type", "text"]
|
||||
},
|
||||
"SetCustomFieldsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"customFields": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" },
|
||||
"description": "Fields to upsert on the line; keys not present are left untouched"
|
||||
}
|
||||
},
|
||||
"required": ["customFields"]
|
||||
},
|
||||
"Notice": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+174
-29
@@ -15,7 +15,6 @@ import (
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
|
||||
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"go.opentelemetry.io/contrib/bridges/otelslog"
|
||||
@@ -43,16 +42,12 @@ var (
|
||||
type PoolServer struct {
|
||||
actor.GrainPool[cart.CartGrain]
|
||||
pod_name string
|
||||
inventoryService inventory.InventoryService
|
||||
reservationService inventory.CartReservationService
|
||||
}
|
||||
|
||||
func NewPoolServer(pool actor.GrainPool[cart.CartGrain], pod_name string, inventoryService inventory.InventoryService, inventoryReservationService inventory.CartReservationService) *PoolServer {
|
||||
func NewPoolServer(pool actor.GrainPool[cart.CartGrain], pod_name string) *PoolServer {
|
||||
srv := &PoolServer{
|
||||
GrainPool: pool,
|
||||
pod_name: pod_name,
|
||||
inventoryService: inventoryService,
|
||||
reservationService: inventoryReservationService,
|
||||
}
|
||||
|
||||
return srv
|
||||
@@ -134,6 +129,12 @@ 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 {
|
||||
@@ -141,25 +142,116 @@ type SetCartItems struct {
|
||||
Items []Item `json:"items"`
|
||||
}
|
||||
|
||||
func getMultipleAddMessages(ctx context.Context, items []Item, country string) []proto.Message {
|
||||
// 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.
|
||||
func buildItemGroups(ctx context.Context, items []Item, country string) []itemGroup {
|
||||
groups := make([]itemGroup, len(items))
|
||||
wg := sync.WaitGroup{}
|
||||
mu := sync.Mutex{}
|
||||
msgs := make([]proto.Message, 0, len(items))
|
||||
for _, itm := range items {
|
||||
wg.Go(
|
||||
func() {
|
||||
msg, err := GetItemAddMessage(ctx, itm.Sku, itm.Quantity, country, itm.StoreId)
|
||||
for i, itm := range items {
|
||||
wg.Go(func() {
|
||||
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
|
||||
}
|
||||
mu.Lock()
|
||||
msgs = append(msgs, msg)
|
||||
mu.Unlock()
|
||||
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() {
|
||||
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
|
||||
}
|
||||
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 msgs
|
||||
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 {
|
||||
@@ -169,14 +261,22 @@ func (s *PoolServer) SetCartItemsHandler(w http.ResponseWriter, r *http.Request,
|
||||
return err
|
||||
}
|
||||
|
||||
msgs := make([]proto.Message, 0, len(setCartItems.Items)+1)
|
||||
msgs = append(msgs, &messages.ClearCartRequest{})
|
||||
msgs = append(msgs, getMultipleAddMessages(r.Context(), setCartItems.Items, setCartItems.Country)...)
|
||||
|
||||
reply, err := s.ApplyLocal(r.Context(), id, msgs...)
|
||||
if _, err := s.ApplyLocal(r.Context(), id, &messages.ClearCartRequest{}); err != nil {
|
||||
return err
|
||||
}
|
||||
groups := buildItemGroups(r.Context(), setCartItems.Items, setCartItems.Country)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -187,12 +287,18 @@ func (s *PoolServer) AddMultipleItemHandler(w http.ResponseWriter, r *http.Reque
|
||||
return err
|
||||
}
|
||||
|
||||
msgs := getMultipleAddMessages(r.Context(), setCartItems.Items, setCartItems.Country)
|
||||
|
||||
reply, err := s.ApplyLocal(r.Context(), id, msgs...)
|
||||
groups := buildItemGroups(r.Context(), setCartItems.Items, setCartItems.Country)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -201,6 +307,10 @@ type AddRequest struct {
|
||||
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 {
|
||||
@@ -209,16 +319,26 @@ func (s *PoolServer) AddSkuRequestHandler(w http.ResponseWriter, r *http.Request
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := GetItemAddMessage(r.Context(), addRequest.Sku, int(addRequest.Quantity), addRequest.Country, addRequest.StoreId)
|
||||
|
||||
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)
|
||||
reply, err := s.applyItemGroups(r.Context(), id, groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reply, err := s.ApplyLocal(r.Context(), id, msg)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -436,6 +556,29 @@ func (s *PoolServer) RemoveLineItemMarkingHandler(w http.ResponseWriter, r *http
|
||||
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)
|
||||
@@ -513,6 +656,7 @@ func (s *PoolServer) Serve(mux *http.ServeMux) {
|
||||
|
||||
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)))
|
||||
@@ -527,4 +671,5 @@ func (s *PoolServer) Serve(mux *http.ServeMux) {
|
||||
handleFunc("PUT /cart/byid/{id}/user", CartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
|
||||
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)))
|
||||
}
|
||||
|
||||
+169
-100
@@ -4,34 +4,77 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
|
||||
"github.com/matst80/slask-finder/pkg/index"
|
||||
)
|
||||
|
||||
// TODO make this configurable
|
||||
func getBaseUrl(country string) string {
|
||||
// if country == "se" {
|
||||
// return "http://s10n-se:8080"
|
||||
// }
|
||||
if country == "no" {
|
||||
return "http://s10n-no.s10n:8080"
|
||||
}
|
||||
if country == "se" {
|
||||
return "http://s10n-se.s10n:8080"
|
||||
}
|
||||
return "http://s10n-se.s10n:8080"
|
||||
// consumedKeys are product-document keys that are mapped to typed AddItem
|
||||
// fields. They are stripped from the dynamic remainder so Extra only holds
|
||||
// genuinely dynamic data.
|
||||
var consumedKeys = []string{
|
||||
"id", "sku", "title", "img", "price", "vat",
|
||||
"discount", "inStock", "supplierId", "supplierName", "deleted",
|
||||
}
|
||||
|
||||
func FetchItem(ctx context.Context, sku string, country string) (*index.DataItem, error) {
|
||||
// getBaseUrl returns the product service base url. Overridable via
|
||||
// PRODUCT_BASE_URL (the country argument is currently unused but kept for when
|
||||
// per-market routing returns).
|
||||
func getBaseUrl(country string) string {
|
||||
return getEnv("PRODUCT_BASE_URL", "http://localhost:8082")
|
||||
}
|
||||
|
||||
// ProductItem is the flat product document served by GET /api/get/{sku}.
|
||||
// Only the fields the cart currently maps mechanically are decoded; the rest
|
||||
// of the (dynamic) document is intentionally ignored until the cart item model
|
||||
// is reworked to carry arbitrary data.
|
||||
type ProductItem struct {
|
||||
Id uint64 `json:"id"`
|
||||
Sku string `json:"sku"`
|
||||
Title string `json:"title"`
|
||||
Img string `json:"img"`
|
||||
Price float64 `json:"price"`
|
||||
Vat int `json:"vat"`
|
||||
Discount int `json:"discount"` // percent off the original price
|
||||
InStock int32 `json:"inStock"`
|
||||
SupplierId int `json:"supplierId"`
|
||||
SupplierName string `json:"supplierName"`
|
||||
Deleted bool `json:"deleted"`
|
||||
|
||||
// Extra is the rest of the product document (everything not mapped to a
|
||||
// typed field above), preserved verbatim and surfaced on the cart item.
|
||||
Extra map[string]json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// numberField reads a numeric dynamic property from the product document
|
||||
// (width, height, ...). Like JS Number(), it accepts a JSON number or a numeric
|
||||
// string. Returns false when the key is absent or not numeric.
|
||||
func (p *ProductItem) numberField(key string) (float64, bool) {
|
||||
raw, ok := p.Extra[key]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
var f float64
|
||||
if err := json.Unmarshal(raw, &f); err == nil {
|
||||
return f, true
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func FetchItem(ctx context.Context, sku string, country string) (*ProductItem, error) {
|
||||
baseUrl := getBaseUrl(country)
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/by-sku/%s", baseUrl, sku), nil)
|
||||
innerCtx, span := tracer.Start(ctx, fmt.Sprintf("fetching data for %s", sku))
|
||||
defer span.End()
|
||||
req = req.WithContext(innerCtx)
|
||||
req, err := http.NewRequestWithContext(innerCtx, http.MethodGet, fmt.Sprintf("%s/api/get/%s", baseUrl, sku), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,111 +83,137 @@ func FetchItem(ctx context.Context, sku string, country string) (*index.DataItem
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
var item index.DataItem
|
||||
err = json.NewDecoder(res.Body).Decode(&item)
|
||||
return &item, err
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("product service returned %d for sku %s", res.StatusCode, sku)
|
||||
}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var item ProductItem
|
||||
if err := json.Unmarshal(body, &item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Capture everything not mapped to a typed field as dynamic data.
|
||||
all := map[string]json.RawMessage{}
|
||||
if err := json.Unmarshal(body, &all); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, k := range consumedKeys {
|
||||
delete(all, k)
|
||||
}
|
||||
if len(all) > 0 {
|
||||
item.Extra = all
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func GetItemAddMessage(ctx context.Context, sku string, qty int, country string, storeId *string) (*messages.AddItem, error) {
|
||||
msg, _, err := BuildItemMessage(ctx, sku, qty, country, storeId, nil)
|
||||
return msg, err
|
||||
}
|
||||
|
||||
// BuildItemMessage fetches a product and builds its AddItem mutation, returning
|
||||
// the fetched product too so it can serve as the parent for child items. When
|
||||
// parent is non-nil the line is priced as a child of that parent.
|
||||
func BuildItemMessage(ctx context.Context, sku string, qty int, country string, storeId *string, parent *ProductItem) (*messages.AddItem, *ProductItem, error) {
|
||||
item, err := FetchItem(ctx, sku, country)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
return ToItemAddMessage(item, storeId, qty, country)
|
||||
}
|
||||
|
||||
func ToItemAddMessage(item *index.DataItem, storeId *string, qty int, country string) (*messages.AddItem, error) {
|
||||
orgPrice, _ := getInt(item.GetNumberFieldValue(5)) // getInt(item.Fields[5])
|
||||
|
||||
price, err := getInt(item.GetNumberFieldValue(4)) //Fields[4]
|
||||
msg, err := ToItemAddMessage(item, parent, storeId, qty, country)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
return msg, item, nil
|
||||
}
|
||||
stk := item.GetStock()
|
||||
stock := cart.StockStatus(0)
|
||||
|
||||
// Glass is billed at a minimum surface even when the real pane is smaller, so a
|
||||
// small window doesn't get a near-zero per-m² price.
|
||||
const minGlassArea = 0.4 // m²
|
||||
|
||||
// areaFromItem derives the main article's *visible glass* surface in m² from a
|
||||
// resolved configurator selection. The catalog width/height are facet codes;
|
||||
// the outer (karmytter) size is `code × 100 − margin` mm and the visible glass
|
||||
// is that minus the frame border per axis, e.g. width 4, widthMargin 20,
|
||||
// borderWidth 194 → 400 − 20 − 194 = 186 mm. Area = glassW(mm) × glassH(mm) /
|
||||
// 1e6, clamped up to the minGlassArea billing floor. Returns 0 when a dimension
|
||||
// is missing/non-numeric (non-window PDP, or no product resolved yet).
|
||||
func areaFromItem(item *ProductItem) float64 {
|
||||
if item == nil {
|
||||
return 0
|
||||
}
|
||||
w, okW := item.numberField("width")
|
||||
h, okH := item.numberField("height")
|
||||
if !okW || !okH || w <= 0 || h <= 0 {
|
||||
return 0
|
||||
}
|
||||
glassW := w * 100 // - (item.numberField("widthMargin") + item.numberField("borderWidth"))
|
||||
glassH := h * 100 // - (item.numberField("heightMargin") + item.numberField("borderHeight"))
|
||||
if glassW <= 0 || glassH <= 0 {
|
||||
return 0
|
||||
}
|
||||
return math.Max((glassW*glassH)/1_000_000, minGlassArea)
|
||||
}
|
||||
|
||||
// accessoryPrice computes a child line's unit price (inc vat, minor units): the
|
||||
// child's own per-m² price scaled by the parent window's billed glass area.
|
||||
// When the parent has no usable dimensions the area is 0, so the price is 0.
|
||||
func accessoryPrice(parent *ProductItem, child *ProductItem) int64 {
|
||||
area := areaFromItem(parent)
|
||||
return int64(child.Price*100*area + 0.5)
|
||||
}
|
||||
|
||||
// orgPriceFromDiscount reconstructs the pre-discount price from a percentage.
|
||||
// Returns 0 when there is no usable discount, in which case OrgPrice is omitted.
|
||||
func orgPriceFromDiscount(price int64, discountPercent int) int64 {
|
||||
if discountPercent <= 0 || discountPercent >= 100 {
|
||||
return 0
|
||||
}
|
||||
return int64(float64(price)/(1-float64(discountPercent)/100) + 0.5)
|
||||
}
|
||||
|
||||
func ToItemAddMessage(item *ProductItem, parent *ProductItem, storeId *string, qty int, country string) (*messages.AddItem, error) {
|
||||
// Central stock only: store-specific stock is no longer in the payload, so a
|
||||
// storeId request currently resolves to zero stock.
|
||||
var stock int32
|
||||
if storeId == nil {
|
||||
centralStock, ok := stk[country]
|
||||
if ok {
|
||||
if !item.Buyable {
|
||||
return nil, fmt.Errorf("item not available")
|
||||
stock = item.InStock
|
||||
}
|
||||
|
||||
if centralStock == 0 && item.SaleStatus == "TBD" {
|
||||
return nil, fmt.Errorf("no items available")
|
||||
}
|
||||
stock = cart.StockStatus(centralStock)
|
||||
// Top-level items price from their own product; children are priced from the
|
||||
// parent product via the accessory-price hook.
|
||||
price := int64(item.Price * 100)
|
||||
if parent != nil {
|
||||
price = accessoryPrice(parent, item)
|
||||
}
|
||||
|
||||
} else {
|
||||
if !item.BuyableInStore {
|
||||
return nil, fmt.Errorf("item not available in store")
|
||||
}
|
||||
storeStock, ok := stk[*storeId]
|
||||
if ok {
|
||||
stock = cart.StockStatus(storeStock)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
articleType, _ := item.GetStringFieldValue(1) //.Fields[1].(string)
|
||||
outletGrade, ok := item.GetStringFieldValue(20) //.Fields[20].(string)
|
||||
var outlet *string
|
||||
if ok {
|
||||
outlet = &outletGrade
|
||||
}
|
||||
sellerId, _ := item.GetStringFieldValue(24) // .Fields[24].(string)
|
||||
sellerName, _ := item.GetStringFieldValue(9) // .Fields[9].(string)
|
||||
|
||||
brand, _ := item.GetStringFieldValue(2) //.Fields[2].(string)
|
||||
category, _ := item.GetStringFieldValue(10) //.Fields[10].(string)
|
||||
category2, _ := item.GetStringFieldValue(11) //.Fields[11].(string)
|
||||
category3, _ := item.GetStringFieldValue(12) //.Fields[12].(string)
|
||||
category4, _ := item.GetStringFieldValue(13) //Fields[13].(string)
|
||||
category5, _ := item.GetStringFieldValue(14) //.Fields[14].(string)
|
||||
|
||||
cgm, _ := item.GetStringFieldValue(35) // Customer Group Membership
|
||||
|
||||
return &messages.AddItem{
|
||||
msg := &messages.AddItem{
|
||||
ItemId: uint32(item.Id),
|
||||
Quantity: int32(qty),
|
||||
Price: int64(price),
|
||||
OrgPrice: int64(orgPrice),
|
||||
Sku: item.GetSku(),
|
||||
Price: price,
|
||||
OrgPrice: orgPriceFromDiscount(price, item.Discount),
|
||||
Sku: item.Sku,
|
||||
Name: item.Title,
|
||||
Image: item.Img,
|
||||
Stock: int32(stock),
|
||||
Brand: brand,
|
||||
Category: category,
|
||||
Category2: category2,
|
||||
Category3: category3,
|
||||
Category4: category4,
|
||||
Category5: category5,
|
||||
Tax: getTax(articleType),
|
||||
SellerId: sellerId,
|
||||
SellerName: sellerName,
|
||||
ArticleType: articleType,
|
||||
Disclaimer: item.Disclaimer,
|
||||
Stock: stock,
|
||||
Tax: int32(item.Vat * 100),
|
||||
SellerId: strconv.Itoa(item.SupplierId),
|
||||
SellerName: item.SupplierName,
|
||||
Country: country,
|
||||
Outlet: outlet,
|
||||
StoreId: storeId,
|
||||
SaleStatus: item.SaleStatus,
|
||||
Cgm: cgm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getTax(articleType string) int32 {
|
||||
switch articleType {
|
||||
case "ZDIE":
|
||||
return 600
|
||||
default:
|
||||
return 2500
|
||||
if len(item.Extra) > 0 {
|
||||
extra, err := json.Marshal(item.Extra)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal extra product data: %w", err)
|
||||
}
|
||||
msg.ExtraJson = extra
|
||||
}
|
||||
|
||||
func getInt(data float64, ok bool) (int, error) {
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("invalid type")
|
||||
}
|
||||
return int(data), nil
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
//go:build integration
|
||||
|
||||
// Integration tests for the product fetcher. These hit a live product service
|
||||
// and are excluded from the default build/test run. Run them with:
|
||||
//
|
||||
// go test -tags=integration ./cmd/cart/
|
||||
//
|
||||
// Point at a different service with PRODUCT_BASE_URL (default http://localhost:8082).
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/go-cart-actor/pkg/proxy"
|
||||
)
|
||||
|
||||
// newTestPoolServer builds a real, disk-backed PoolServer for exercising the
|
||||
// HTTP handlers end-to-end (no clustering peers).
|
||||
func newTestPoolServer(t *testing.T) *PoolServer {
|
||||
t.Helper()
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
reg.RegisterProcessor(actor.NewMutationProcessor(func(_ context.Context, g *cart.CartGrain) error {
|
||||
g.UpdateTotals()
|
||||
g.Version++
|
||||
return nil
|
||||
}))
|
||||
dir := t.TempDir()
|
||||
disk := actor.NewDiskStorage[cart.CartGrain](dir, reg)
|
||||
pool, err := actor.NewSimpleGrainPool(actor.GrainPoolConfig[cart.CartGrain]{
|
||||
MutationRegistry: reg,
|
||||
Storage: disk,
|
||||
Spawn: func(ctx context.Context, id uint64) (actor.Grain[cart.CartGrain], error) {
|
||||
g := cart.NewCartGrain(id, time.Now())
|
||||
err := disk.LoadEvents(ctx, id, g)
|
||||
return g, err
|
||||
},
|
||||
Destroy: func(actor.Grain[cart.CartGrain]) error { return nil },
|
||||
SpawnHost: func(host string) (actor.Host[cart.CartGrain], error) {
|
||||
return proxy.NewRemoteHost[cart.CartGrain](host, "8080")
|
||||
},
|
||||
TTL: 5 * time.Minute,
|
||||
PoolSize: 1000,
|
||||
Hostname: "",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("new pool: %v", err)
|
||||
}
|
||||
return NewPoolServer(pool, "test")
|
||||
}
|
||||
|
||||
// exampleId is the catalog item the fetcher rework was validated against.
|
||||
const exampleId = "8163"
|
||||
|
||||
// requireService skips the test when the product service is not reachable so
|
||||
// the suite degrades gracefully in environments without it.
|
||||
func requireService(t *testing.T) {
|
||||
t.Helper()
|
||||
base := getBaseUrl("se")
|
||||
req, err := http.NewRequest(http.MethodGet, base+"/api/get/"+exampleId, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build probe request: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
res, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) || errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Skipf("product service %s not reachable: %v", base, err)
|
||||
}
|
||||
t.Fatalf("probe product service: %v", err)
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
func TestFetchItem_Example8163(t *testing.T) {
|
||||
requireService(t)
|
||||
|
||||
ctx := context.Background()
|
||||
item, err := FetchItem(ctx, exampleId, "se")
|
||||
if err != nil {
|
||||
t.Fatalf("FetchItem(%s): %v", exampleId, err)
|
||||
}
|
||||
|
||||
if item.Id != 8163 {
|
||||
t.Errorf("Id = %d, want 8163", item.Id)
|
||||
}
|
||||
if item.Sku == "" {
|
||||
t.Error("Sku is empty")
|
||||
}
|
||||
if item.Price <= 0 {
|
||||
t.Errorf("Price = %v, want > 0", item.Price)
|
||||
}
|
||||
t.Logf("fetched %d sku=%s price=%v vat=%d discount=%d inStock=%d supplier=%q",
|
||||
item.Id, item.Sku, item.Price, item.Vat, item.Discount, item.InStock, item.SupplierName)
|
||||
}
|
||||
|
||||
func TestToItemAddMessage_Example8163(t *testing.T) {
|
||||
requireService(t)
|
||||
|
||||
ctx := context.Background()
|
||||
item, err := FetchItem(ctx, exampleId, "se")
|
||||
if err != nil {
|
||||
t.Fatalf("FetchItem(%s): %v", exampleId, err)
|
||||
}
|
||||
|
||||
// Central stock (no storeId, no parent).
|
||||
msg, err := ToItemAddMessage(item, nil, nil, 2, "se")
|
||||
if err != nil {
|
||||
t.Fatalf("ToItemAddMessage: %v", err)
|
||||
}
|
||||
|
||||
if msg.ItemId != uint32(item.Id) {
|
||||
t.Errorf("ItemId = %d, want %d", msg.ItemId, item.Id)
|
||||
}
|
||||
if msg.Quantity != 2 {
|
||||
t.Errorf("Quantity = %d, want 2", msg.Quantity)
|
||||
}
|
||||
if want := int64(item.Price * 100); msg.Price != want {
|
||||
t.Errorf("Price = %d, want %d (price*100)", msg.Price, want)
|
||||
}
|
||||
if msg.Sku != item.Sku {
|
||||
t.Errorf("Sku = %q, want %q", msg.Sku, item.Sku)
|
||||
}
|
||||
if msg.Name == "" {
|
||||
t.Error("Name is empty")
|
||||
}
|
||||
if want := int32(item.Vat * 100); msg.Tax != want {
|
||||
t.Errorf("Tax = %d, want vat*100 = %d", msg.Tax, want)
|
||||
}
|
||||
if msg.Stock != item.InStock {
|
||||
t.Errorf("Stock = %d, want central inStock %d", msg.Stock, item.InStock)
|
||||
}
|
||||
if msg.SellerName != item.SupplierName {
|
||||
t.Errorf("SellerName = %q, want %q", msg.SellerName, item.SupplierName)
|
||||
}
|
||||
if want := strconv.Itoa(item.SupplierId); msg.SellerId != want {
|
||||
t.Errorf("SellerId = %q, want %q", msg.SellerId, want)
|
||||
}
|
||||
|
||||
// OrgPrice is reconstructed from the discount percent.
|
||||
if item.Discount > 0 && item.Discount < 100 {
|
||||
if msg.OrgPrice <= msg.Price {
|
||||
t.Errorf("OrgPrice = %d, want > Price %d (discount %d%%)", msg.OrgPrice, msg.Price, item.Discount)
|
||||
}
|
||||
} else if msg.OrgPrice != 0 {
|
||||
t.Errorf("OrgPrice = %d, want 0 (no usable discount)", msg.OrgPrice)
|
||||
}
|
||||
|
||||
// Store stock currently resolves to zero (store-level stock not in payload).
|
||||
storeId := "1234"
|
||||
storeMsg, err := ToItemAddMessage(item, nil, &storeId, 1, "se")
|
||||
if err != nil {
|
||||
t.Fatalf("ToItemAddMessage(store): %v", err)
|
||||
}
|
||||
if storeMsg.Stock != 0 {
|
||||
t.Errorf("store Stock = %d, want 0", storeMsg.Stock)
|
||||
}
|
||||
if storeMsg.StoreId == nil || *storeMsg.StoreId != storeId {
|
||||
t.Errorf("StoreId = %v, want %q", storeMsg.StoreId, storeId)
|
||||
}
|
||||
|
||||
if len(msg.ExtraJson) == 0 {
|
||||
t.Error("expected ExtraJson to carry dynamic product data")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDynamicData_RoundTripsToCartItem exercises the full path: fetch the
|
||||
// product, build the AddItem mutation, apply it through the cart mutation
|
||||
// registry, then render the cart exactly as the API does — asserting that a
|
||||
// dynamic product key ("glas" for item 8163) is flattened onto the item.
|
||||
func TestDynamicData_RoundTripsToCartItem(t *testing.T) {
|
||||
requireService(t)
|
||||
|
||||
ctx := context.Background()
|
||||
msg, err := GetItemAddMessage(ctx, exampleId, 1, "se", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetItemAddMessage: %v", err)
|
||||
}
|
||||
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
grain := cart.NewCartGrain(1, time.Now())
|
||||
if _, err := reg.Apply(ctx, grain, msg); err != nil {
|
||||
t.Fatalf("apply AddItem: %v", err)
|
||||
}
|
||||
|
||||
// Marshal the grain the way the HTTP handlers do (WriteResult -> json).
|
||||
b, err := json.Marshal(grain)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal grain: %v", err)
|
||||
}
|
||||
|
||||
var rendered struct {
|
||||
Items []map[string]json.RawMessage `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &rendered); err != nil {
|
||||
t.Fatalf("unmarshal grain: %v", err)
|
||||
}
|
||||
if len(rendered.Items) != 1 {
|
||||
t.Fatalf("items = %d, want 1", len(rendered.Items))
|
||||
}
|
||||
|
||||
item := rendered.Items[0]
|
||||
// Typed field present.
|
||||
if _, ok := item["sku"]; !ok {
|
||||
t.Error("rendered item missing typed key \"sku\"")
|
||||
}
|
||||
// Dynamic key flattened onto the item and returned over the API.
|
||||
glas, ok := item["glas"]
|
||||
if !ok {
|
||||
t.Fatalf("rendered item missing dynamic key \"glas\"; got keys %v", keysOf(item))
|
||||
}
|
||||
if string(glas) != `"V"` {
|
||||
t.Errorf("glas = %s, want \"V\"", glas)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetCartItems_ParentWithChildren reproduces the reported scenario through
|
||||
// the real SetCartItemsHandler: a parent SKU with three distinct children.
|
||||
func TestSetCartItems_ParentWithChildren(t *testing.T) {
|
||||
requireService(t)
|
||||
|
||||
s := newTestPoolServer(t)
|
||||
body := `{"sku":"146620","quantity":1,"children":[{"sku":"170852","quantity":1},{"sku":"123075","quantity":1},{"sku":"123076","quantity":1}]}`
|
||||
// The handler decodes SetCartItems{country, items:[Item]} — the reported
|
||||
// payload is a single Item, so wrap it in the items array.
|
||||
payload := `{"country":"se","items":[` + body + `]}`
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/cart/set", bytes.NewBufferString(payload))
|
||||
if err := s.SetCartItemsHandler(rec, req, cart.CartId(42)); err != nil {
|
||||
t.Fatalf("SetCartItemsHandler: %v", err)
|
||||
}
|
||||
|
||||
type lineT struct {
|
||||
Id uint32 `json:"id"`
|
||||
ItemId uint32 `json:"itemId"`
|
||||
ParentId *uint32 `json:"parentId"`
|
||||
Sku string `json:"sku"`
|
||||
}
|
||||
// Handler returns a MutationResult {result:{items}, mutations}. Fall back to
|
||||
// top-level items for the empty-cart path.
|
||||
var resp struct {
|
||||
Result struct {
|
||||
Items []lineT `json:"items"`
|
||||
} `json:"result"`
|
||||
Items []lineT `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v\nbody: %s", err, rec.Body.String())
|
||||
}
|
||||
grain := struct{ Items []lineT }{Items: resp.Result.Items}
|
||||
if len(grain.Items) == 0 {
|
||||
grain.Items = resp.Items
|
||||
}
|
||||
|
||||
t.Logf("result has %d items:", len(grain.Items))
|
||||
for _, it := range grain.Items {
|
||||
t.Logf(" line %d itemId=%d sku=%s parentId=%v", it.Id, it.ItemId, it.Sku, it.ParentId)
|
||||
}
|
||||
if len(grain.Items) != 4 {
|
||||
t.Fatalf("items = %d, want 4 (parent + 3 children)", len(grain.Items))
|
||||
}
|
||||
children := 0
|
||||
for _, it := range grain.Items {
|
||||
if it.ParentId != nil {
|
||||
children++
|
||||
}
|
||||
}
|
||||
if children != 3 {
|
||||
t.Errorf("children = %d, want 3", children)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddSkuRequest_SingleItemWithChildren posts the bare single-item body (no
|
||||
// items wrapper) to the single-add handler and asserts children are added.
|
||||
func TestAddSkuRequest_SingleItemWithChildren(t *testing.T) {
|
||||
requireService(t)
|
||||
|
||||
s := newTestPoolServer(t)
|
||||
payload := `{"sku":"146620","quantity":1,"country":"se","children":[{"sku":"170852","quantity":1},{"sku":"123075","quantity":1},{"sku":"123076","quantity":1}]}`
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/cart", bytes.NewBufferString(payload))
|
||||
if err := s.AddSkuRequestHandler(rec, req, cart.CartId(7)); err != nil {
|
||||
t.Fatalf("AddSkuRequestHandler: %v", err)
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result struct {
|
||||
Items []struct {
|
||||
ParentId *uint32 `json:"parentId"`
|
||||
} `json:"items"`
|
||||
} `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v\nbody: %s", err, rec.Body.String())
|
||||
}
|
||||
if len(resp.Result.Items) != 4 {
|
||||
t.Fatalf("items = %d, want 4 (parent + 3 children)", len(resp.Result.Items))
|
||||
}
|
||||
}
|
||||
|
||||
func keysOf(m map[string]json.RawMessage) []string {
|
||||
ks := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
return ks
|
||||
}
|
||||
|
||||
// TestChildren_ParentLinkage exercises the nested-children build + ordered apply:
|
||||
// buildItemGroups fetches the parent product and prices the child against it,
|
||||
// then parentLineId resolves the parent's line id and the child line links to it.
|
||||
//
|
||||
// Structural test: it reuses 8163 as both parent and child (child pinned to a
|
||||
// store so it does not merge with the parent line). Real children would be
|
||||
// distinct service/insurance/accessory SKUs.
|
||||
func TestChildren_ParentLinkage(t *testing.T) {
|
||||
requireService(t)
|
||||
|
||||
ctx := context.Background()
|
||||
childStore := "1"
|
||||
items := []Item{{
|
||||
Sku: exampleId,
|
||||
Quantity: 1,
|
||||
Children: []Item{{Sku: exampleId, Quantity: 1, StoreId: &childStore}},
|
||||
}}
|
||||
|
||||
groups := buildItemGroups(ctx, items, "se")
|
||||
if len(groups) != 1 {
|
||||
t.Fatalf("groups = %d, want 1", len(groups))
|
||||
}
|
||||
if groups[0].parent == nil {
|
||||
t.Fatal("parent message not built")
|
||||
}
|
||||
if len(groups[0].children) != 1 {
|
||||
t.Fatalf("children = %d, want 1", len(groups[0].children))
|
||||
}
|
||||
|
||||
// Apply through the real mutation registry to verify the linkage the handler
|
||||
// performs (applyItemGroups does the same with pool.ApplyLocal).
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
grain := cart.NewCartGrain(1, time.Now())
|
||||
|
||||
if _, err := reg.Apply(ctx, grain, groups[0].parent); err != nil {
|
||||
t.Fatalf("apply parent: %v", err)
|
||||
}
|
||||
pid, ok := parentLineId(grain, groups[0].parent)
|
||||
if !ok {
|
||||
t.Fatal("parentLineId did not resolve the parent line")
|
||||
}
|
||||
|
||||
child := groups[0].children[0]
|
||||
child.ParentId = &pid
|
||||
if _, err := reg.Apply(ctx, grain, child); err != nil {
|
||||
t.Fatalf("apply child: %v", err)
|
||||
}
|
||||
|
||||
if len(grain.Items) != 2 {
|
||||
t.Fatalf("cart items = %d, want 2 (parent + child)", len(grain.Items))
|
||||
}
|
||||
|
||||
var parentLine, childLine *cart.CartItem
|
||||
for _, it := range grain.Items {
|
||||
if it.ParentId == nil {
|
||||
parentLine = it
|
||||
} else {
|
||||
childLine = it
|
||||
}
|
||||
}
|
||||
if parentLine == nil || childLine == nil {
|
||||
t.Fatalf("expected one parent and one child line, got %+v", grain.Items)
|
||||
}
|
||||
if *childLine.ParentId != parentLine.Id {
|
||||
t.Errorf("child.ParentId = %d, want parent line id %d", *childLine.ParentId, parentLine.Id)
|
||||
}
|
||||
t.Logf("parent line %d, child line %d -> parent %d", parentLine.Id, childLine.Id, *childLine.ParentId)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func makeProduct(price float64, extra map[string]any) *ProductItem {
|
||||
m := map[string]json.RawMessage{}
|
||||
for k, v := range extra {
|
||||
b, _ := json.Marshal(v)
|
||||
m[k] = b
|
||||
}
|
||||
return &ProductItem{Price: price, Extra: m}
|
||||
}
|
||||
|
||||
func TestAreaFromItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
extra map[string]any
|
||||
want float64
|
||||
}{
|
||||
{"floors small window to min", map[string]any{"width": 5, "height": 6}, 0.4}, // 500*600/1e6 = 0.30 -> floor 0.4
|
||||
{"normal window", map[string]any{"width": 10, "height": 10}, 1.0}, // 1000*1000/1e6 = 1.0
|
||||
{"numeric strings", map[string]any{"width": "10", "height": "10"}, 1.0}, // JS Number() parity
|
||||
{"missing height", map[string]any{"width": 10}, 0},
|
||||
{"zero width", map[string]any{"width": 0, "height": 10}, 0},
|
||||
{"non-numeric", map[string]any{"width": "abc", "height": 10}, 0},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := areaFromItem(makeProduct(0, tc.extra))
|
||||
if got != tc.want {
|
||||
t.Errorf("areaFromItem = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if got := areaFromItem(nil); got != 0 {
|
||||
t.Errorf("areaFromItem(nil) = %v, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessoryPrice(t *testing.T) {
|
||||
// area 1.0 m^2, child 100.00 -> 100.00 * 100 * 1.0 = 10000 minor units.
|
||||
parent := makeProduct(0, map[string]any{"width": 10, "height": 10})
|
||||
if got := accessoryPrice(parent, makeProduct(100, nil)); got != 10000 {
|
||||
t.Errorf("accessoryPrice = %d, want 10000", got)
|
||||
}
|
||||
|
||||
// area floored to 0.4, child 9952.78 -> round(9952.78 * 100 * 0.4) = 398111.
|
||||
small := makeProduct(0, map[string]any{"width": 5, "height": 6})
|
||||
if got := accessoryPrice(small, makeProduct(9952.78, nil)); got != 398111 {
|
||||
t.Errorf("accessoryPrice = %d, want 398111", got)
|
||||
}
|
||||
|
||||
// parent without dimensions -> area 0 -> price 0.
|
||||
if got := accessoryPrice(makeProduct(0, nil), makeProduct(500, nil)); got != 0 {
|
||||
t.Errorf("accessoryPrice (no dims) = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{"type":"AddItem","timestamp":"2026-06-14T12:06:33.026264929+02:00","mutation":{"item_id":147528,"quantity":1,"price":18559,"orgPrice":23199,"sku":"A0161291","name":"SF vridfönster 2380x680mm 2-luft, insida trä utsida aluminium, 3-glas ","image":"/A0161291.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJib3JkZXJIZWlnaHQiOjE5NCwiYm9yZGVyV2lkdGgiOjE5NCwiYnVsa3kiOjEsImNhdGVnb3J5IjowLCJkZWxldGUiOmZhbHNlLCJkZWxpdmVyeVN0cmluZyI6Ik5vcm1hbHQgNC02IGFyYmV0c3ZlY2tvciIsImRlbGl2ZXJ5V2VlayI6MTAsImRlc2NyaXB0aW9uIjoiS2FybXl0dGVybcOldHQgYnJlZGQgeCBow7ZqZC4iLCJkaXZpZGVyU2l6ZSI6MTY0LCJnbGFzc01hcmdpblBvc3QiOjcxLCJncm91cCI6IjEzNyIsImhlaWdodCI6NywiaGVpZ2h0TWFyZ2luIjoyMCwiaXNBY2Nlc3NvcnkiOjAsImx1ZnQiOiIyIiwibHVmdF90aXRsZSI6IjItTHVmdCAodHbDpSDDtnBwbmluZ3NiYXJhIGLDpWdhciBpIHNhbW1hIGthcm0pIiwibW9kZWwiOiIxMzciLCJtb2RlbERlc2NyaXB0aW9uIjoiU1AgQmFsYW5zIGbDtm5zdGVyIHZyaWQiLCJwYWdlSWQiOjg4NDM2LCJwb3N0U2l6ZSI6NzEsInByZXZpb3VzRGlzY291bnQiOjMwLCJzcHJvanNTaXplIjozMSwic3RvcmxlayI6IjI0MDciLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCIsIndpZHRoIjoyNCwid2lkdGhNYXJnaW4iOjIwfQ=="}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T12:06:41.00641672+02:00","mutation":{"Id":1,"quantity":2}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T12:06:50.402587934+02:00","mutation":{"Id":1,"quantity":1}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T12:08:25.714006052+02:00","mutation":{"Id":1,"quantity":2}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T12:11:38.722916072+02:00","mutation":{"Id":1,"quantity":1}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T12:14:12.485362998+02:00","mutation":{"Id":1,"quantity":2}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T12:14:13.259209602+02:00","mutation":{"Id":1,"quantity":1}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T12:14:19.560413353+02:00","mutation":{"item_id":146107,"quantity":1,"price":1812900,"orgPrice":2266125,"sku":"A0158995","name":"SF vridfönster 1680x1380mm 1-luft, insida trä utsida aluminium, 3-glas ","image":"/A0158995.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJib3JkZXJIZWlnaHQiOjE5NCwiYm9yZGVyV2lkdGgiOjE5NCwiYnVsa3kiOjEsImNhdGVnb3J5IjowLCJkZWxldGUiOmZhbHNlLCJkZWxpdmVyeVN0cmluZyI6Ik5vcm1hbHQgNC02IGFyYmV0c3ZlY2tvciIsImRlbGl2ZXJ5V2VlayI6MTAsImRlc2NyaXB0aW9uIjoiS2FybXl0dGVybcOldHQgYnJlZGQgeCBow7ZqZC4iLCJkaXZpZGVyU2l6ZSI6MTY0LCJnbGFzc01hcmdpblBvc3QiOjcxLCJncm91cCI6IjEzNyIsImhlaWdodCI6MTQsImhlaWdodE1hcmdpbiI6MjAsImlzQWNjZXNzb3J5IjowLCJsdWZ0IjoiMSIsImx1ZnRfdGl0bGUiOiIxLUx1ZnQgKGVuIMO2cHBuaW5nc2JhciBiw6VnZSBpIHNhbW1hIGthcm0pIiwibW9kZWwiOiIxMzciLCJtb2RlbERlc2NyaXB0aW9uIjoiU1AgQmFsYW5zIGbDtm5zdGVyIHZyaWQiLCJwYWdlSWQiOjg4NDM2LCJwb3N0U2l6ZSI6NzEsInByZXZpb3VzRGlzY291bnQiOjMwLCJzcHJvanNTaXplIjozMSwic3RvcmxlayI6IjE3MTQiLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCIsIndpZHRoIjoxNywid2lkdGhNYXJnaW4iOjIwfQ=="}}
|
||||
{"type":"RemoveItem","timestamp":"2026-06-14T12:16:35.832642257+02:00","mutation":{"Id":1}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T12:47:41.110158952+02:00","mutation":{"item_id":144047,"quantity":1,"price":690193,"orgPrice":862741,"sku":"A0162590","name":"SF vridfönster 480x580mm 1-luft, insida trä utsida aluminium, 3-glas ","image":"/A0162590.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJib3JkZXJIZWlnaHQiOjE5NCwiYm9yZGVyV2lkdGgiOjE5NCwiYnVsa3kiOjEsImNhdGVnb3J5IjowLCJkZWxldGUiOmZhbHNlLCJkZWxpdmVyeVN0cmluZyI6Ik5vcm1hbHQgNC02IGFyYmV0c3ZlY2tvciIsImRlbGl2ZXJ5V2VlayI6MTAsImRlc2NyaXB0aW9uIjoiS2FybXl0dGVybcOldHQgYnJlZGQgeCBow7ZqZC4iLCJkaXZpZGVyU2l6ZSI6MTY0LCJnbGFzc01hcmdpblBvc3QiOjcxLCJncm91cCI6IjEzNyIsImhlaWdodCI6NiwiaGVpZ2h0TWFyZ2luIjoyMCwiaXNBY2Nlc3NvcnkiOjAsImx1ZnQiOiIxIiwibHVmdF90aXRsZSI6IjEtTHVmdCAoZW4gw7ZwcG5pbmdzYmFyIGLDpWdlIGkgc2FtbWEga2FybSkiLCJtb2RlbCI6IjEzNyIsIm1vZGVsRGVzY3JpcHRpb24iOiJTUCBCYWxhbnMgZsO2bnN0ZXIgdnJpZCIsInBhZ2VJZCI6ODg0MzYsInBvc3RTaXplIjo3MSwicHJldmlvdXNEaXNjb3VudCI6MzAsInNwcm9qc1NpemUiOjMxLCJzdG9ybGVrIjoiMDUwNiIsInRheG9ub215IjpbXSwidW5pdCI6InN0Iiwid2lkdGgiOjUsIndpZHRoTWFyZ2luIjoyMH0="}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T12:49:14.15670005+02:00","mutation":{"item_id":144047,"quantity":1,"price":690193,"orgPrice":862741,"sku":"A0162590","name":"SF vridfönster 480x580mm 1-luft, insida trä utsida aluminium, 3-glas ","image":"/A0162590.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJib3JkZXJIZWlnaHQiOjE5NCwiYm9yZGVyV2lkdGgiOjE5NCwiYnVsa3kiOjEsImNhdGVnb3J5IjowLCJkZWxldGUiOmZhbHNlLCJkZWxpdmVyeVN0cmluZyI6Ik5vcm1hbHQgNC02IGFyYmV0c3ZlY2tvciIsImRlbGl2ZXJ5V2VlayI6MTAsImRlc2NyaXB0aW9uIjoiS2FybXl0dGVybcOldHQgYnJlZGQgeCBow7ZqZC4iLCJkaXZpZGVyU2l6ZSI6MTY0LCJnbGFzc01hcmdpblBvc3QiOjcxLCJncm91cCI6IjEzNyIsImhlaWdodCI6NiwiaGVpZ2h0TWFyZ2luIjoyMCwiaXNBY2Nlc3NvcnkiOjAsImx1ZnQiOiIxIiwibHVmdF90aXRsZSI6IjEtTHVmdCAoZW4gw7ZwcG5pbmdzYmFyIGLDpWdlIGkgc2FtbWEga2FybSkiLCJtb2RlbCI6IjEzNyIsIm1vZGVsRGVzY3JpcHRpb24iOiJTUCBCYWxhbnMgZsO2bnN0ZXIgdnJpZCIsInBhZ2VJZCI6ODg0MzYsInBvc3RTaXplIjo3MSwicHJldmlvdXNEaXNjb3VudCI6MzAsInNwcm9qc1NpemUiOjMxLCJzdG9ybGVrIjoiMDUwNiIsInRheG9ub215IjpbXSwidW5pdCI6InN0Iiwid2lkdGgiOjUsIndpZHRoTWFyZ2luIjoyMH0="}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T12:49:19.389741453+02:00","mutation":{"Id":3,"quantity":1}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T12:49:19.68037225+02:00","mutation":{"Id":3}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T12:49:20.250569359+02:00","mutation":{"Id":2}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T12:49:22.994875367+02:00","mutation":{"item_id":144047,"quantity":1,"price":690193,"orgPrice":862741,"sku":"A0162590","name":"SF vridfönster 480x580mm 1-luft, insida trä utsida aluminium, 3-glas ","image":"/A0162590.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJib3JkZXJIZWlnaHQiOjE5NCwiYm9yZGVyV2lkdGgiOjE5NCwiYnVsa3kiOjEsImNhdGVnb3J5IjowLCJkZWxldGUiOmZhbHNlLCJkZWxpdmVyeVN0cmluZyI6Ik5vcm1hbHQgNC02IGFyYmV0c3ZlY2tvciIsImRlbGl2ZXJ5V2VlayI6MTAsImRlc2NyaXB0aW9uIjoiS2FybXl0dGVybcOldHQgYnJlZGQgeCBow7ZqZC4iLCJkaXZpZGVyU2l6ZSI6MTY0LCJnbGFzc01hcmdpblBvc3QiOjcxLCJncm91cCI6IjEzNyIsImhlaWdodCI6NiwiaGVpZ2h0TWFyZ2luIjoyMCwiaXNBY2Nlc3NvcnkiOjAsImx1ZnQiOiIxIiwibHVmdF90aXRsZSI6IjEtTHVmdCAoZW4gw7ZwcG5pbmdzYmFyIGLDpWdlIGkgc2FtbWEga2FybSkiLCJtb2RlbCI6IjEzNyIsIm1vZGVsRGVzY3JpcHRpb24iOiJTUCBCYWxhbnMgZsO2bnN0ZXIgdnJpZCIsInBhZ2VJZCI6ODg0MzYsInBvc3RTaXplIjo3MSwicHJldmlvdXNEaXNjb3VudCI6MzAsInNwcm9qc1NpemUiOjMxLCJzdG9ybGVrIjoiMDUwNiIsInRheG9ub215IjpbXSwidW5pdCI6InN0Iiwid2lkdGgiOjUsIndpZHRoTWFyZ2luIjoyMH0="}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T13:02:45.511064864+02:00","mutation":{"item_id":146248,"quantity":1,"price":1842370,"orgPrice":2302963,"sku":"A0159038","name":"SF vridfönster 1780x780mm 2-luft, insida trä utsida aluminium, 3-glas ","image":"/A0159038.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJib3JkZXJIZWlnaHQiOjE5NCwiYm9yZGVyV2lkdGgiOjE5NCwiYnVsa3kiOjEsImNhdGVnb3J5IjowLCJkZWxldGUiOmZhbHNlLCJkZWxpdmVyeVN0cmluZyI6Ik5vcm1hbHQgNC02IGFyYmV0c3ZlY2tvciIsImRlbGl2ZXJ5V2VlayI6MTAsImRlc2NyaXB0aW9uIjoiS2FybXl0dGVybcOldHQgYnJlZGQgeCBow7ZqZC4iLCJkaXZpZGVyU2l6ZSI6MTY0LCJnbGFzc01hcmdpblBvc3QiOjcxLCJncm91cCI6IjEzNyIsImhlaWdodCI6OCwiaGVpZ2h0TWFyZ2luIjoyMCwiaXNBY2Nlc3NvcnkiOjAsImx1ZnQiOiIyIiwibHVmdF90aXRsZSI6IjItTHVmdCAodHbDpSDDtnBwbmluZ3NiYXJhIGLDpWdhciBpIHNhbW1hIGthcm0pIiwibW9kZWwiOiIxMzciLCJtb2RlbERlc2NyaXB0aW9uIjoiU1AgQmFsYW5zIGbDtm5zdGVyIHZyaWQiLCJwYWdlSWQiOjg4NDM2LCJwb3N0U2l6ZSI6NzEsInByZXZpb3VzRGlzY291bnQiOjMwLCJzcHJvanNTaXplIjozMSwic3RvcmxlayI6IjE4MDgiLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCIsIndpZHRoIjoxOCwid2lkdGhNYXJnaW4iOjIwfQ=="}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T13:02:48.965993343+02:00","mutation":{"Id":5}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T13:02:49.533960677+02:00","mutation":{"Id":4}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T13:03:02.543932484+02:00","mutation":{"item_id":146620,"quantity":1,"price":3043620,"orgPrice":3804525,"sku":"A0164975","name":"SF vridfönster 1880x1180mm 3-luft, insida trä utsida aluminium, 3-glas ","image":"/A0164975.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJib3JkZXJIZWlnaHQiOjE5NCwiYm9yZGVyV2lkdGgiOjE5NCwiYnVsa3kiOjEsImNhdGVnb3J5IjowLCJkZWxldGUiOmZhbHNlLCJkZWxpdmVyeVN0cmluZyI6Ik5vcm1hbHQgNC02IGFyYmV0c3ZlY2tvciIsImRlbGl2ZXJ5V2VlayI6MTAsImRlc2NyaXB0aW9uIjoiS2FybXl0dGVybcOldHQgYnJlZGQgeCBow7ZqZC4iLCJkaXZpZGVyU2l6ZSI6MTY0LCJnbGFzc01hcmdpblBvc3QiOjcxLCJncm91cCI6IjEzNyIsImhlaWdodCI6MTIsImhlaWdodE1hcmdpbiI6MjAsImlzQWNjZXNzb3J5IjowLCJsdWZ0IjoiMyIsImx1ZnRfdGl0bGUiOiIzLUx1ZnQgKHRyZSDDtnBwbmluZ3NiYXJhIGLDpWdhciBpIHNhbW1hIGthcm0pIiwibW9kZWwiOiIxMzciLCJtb2RlbERlc2NyaXB0aW9uIjoiU1AgQmFsYW5zIGbDtm5zdGVyIHZyaWQiLCJwYWdlSWQiOjg4NDM2LCJwb3N0U2l6ZSI6NzEsInByZXZpb3VzRGlzY291bnQiOjMwLCJzcHJvanNTaXplIjozMSwic3RvcmxlayI6IjE5MTIiLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCIsIndpZHRoIjoxOSwid2lkdGhNYXJnaW4iOjIwfQ=="}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T13:06:48.388062792+02:00","mutation":{"Id":6}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T13:07:01.83789281+02:00","mutation":{"item_id":146387,"quantity":1,"price":1767450,"orgPrice":2524929,"sku":"A0164933","name":"SF vridfönster 1780x1180mm 1-luft, insida trä utsida aluminium, 3-glas ","image":"/A0164933.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJib3JkZXJIZWlnaHQiOjE5NCwiYm9yZGVyV2lkdGgiOjE5NCwiYnVsa3kiOjEsImNhdGVnb3J5IjowLCJkZWxldGUiOmZhbHNlLCJkZWxpdmVyeVN0cmluZyI6Ik5vcm1hbHQgNC02IGFyYmV0c3ZlY2tvciIsImRlbGl2ZXJ5V2VlayI6MTAsImRlc2NyaXB0aW9uIjoiS2FybXl0dGVybcOldHQgYnJlZGQgeCBow7ZqZC4iLCJkaXZpZGVyU2l6ZSI6MTY0LCJnbGFzc01hcmdpblBvc3QiOjcxLCJncm91cCI6IjEzNyIsImhlaWdodCI6MTIsImhlaWdodE1hcmdpbiI6MjAsImlzQWNjZXNzb3J5IjowLCJsdWZ0IjoiMSIsImx1ZnRfdGl0bGUiOiIxLUx1ZnQgKGVuIMO2cHBuaW5nc2JhciBiw6VnZSBpIHNhbW1hIGthcm0pIiwibW9kZWwiOiIxMzciLCJtb2RlbERlc2NyaXB0aW9uIjoiU1AgQmFsYW5zIGbDtm5zdGVyIHZyaWQiLCJwYWdlSWQiOjg4NDM2LCJwb3N0U2l6ZSI6NzEsInByZXZpb3VzRGlzY291bnQiOjMwLCJzcHJvanNTaXplIjozMSwic3RvcmxlayI6IjE4MTIiLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCIsIndpZHRoIjoxOCwid2lkdGhNYXJnaW4iOjIwfQ=="}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T13:07:01.837946101+02:00","mutation":{"item_id":170852,"quantity":1,"sku":"A0190103","name":"Rundad profil på karm \u0026 båge, SP utseende","image":"/UserFiles/Svenska_Fonster/Diverse/Genomskarning_rund_profil.jpg","tax":2500,"sellerId":"7","sellerName":"Kaski","parentId":7,"extra_json":"eyJidWxreSI6MCwiY2F0ZWdvcnkiOjE3NCwiZGVsZXRlIjpmYWxzZSwiZGVsaXZlcnlTdHJpbmciOiJOb3JtYWx0IDQtNiBhcmJldHN2ZWNrb3IiLCJkZWxpdmVyeVdlZWsiOjEwLCJkZXNjcmlwdGlvbiI6IiIsImhlaWdodE1hcmdpbiI6MTAsImlzQWNjZXNzb3J5Ijp0cnVlLCJtb2RlbCI6IllEIiwibW9kZWxEZXNjcmlwdGlvbiI6Ikthc2tpIFl0dGVyZMO2cnJhciB0aGVybW8iLCJwYWdlSWQiOjAsInByZXZpb3VzRGlzY291bnQiOjIwLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCIsIndpZHRoTWFyZ2luIjoxMH0="}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T13:07:01.837975176+02:00","mutation":{"item_id":123075,"quantity":1,"price":995278,"sku":"A0125821","name":"Ställkostnad egen kulör Ncs/Ral nr (ej lasyr) / leverans","image":"/UserFiles/Fargkarta.jpg","tax":2500,"sellerId":"0","parentId":7,"extra_json":"eyJhY2Nlc3NvcnlNYXRjaCI6IiIsImJ1bGt5IjowLCJjYXRlZ29yeSI6NjUsImRlbGV0ZSI6ZmFsc2UsImRlc2NyaXB0aW9uIjoiIiwiZ3JvdXAiOiIiLCJpc0FjY2Vzc29yeSI6dHJ1ZSwibW9kZWwiOiIiLCJwYWdlSWQiOjAsInRheG9ub215IjpbXSwidW5pdCI6InN0In0="}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T13:07:01.837991607+02:00","mutation":{"item_id":123076,"quantity":1,"price":2420510,"sku":"A0125831","name":"Ställkostnad egen kulör utsida NCS S / leverans (väljs på en produkt)","image":"/UserFiles/Fargkarta.jpg","tax":2500,"sellerId":"0","parentId":7,"extra_json":"eyJhY2Nlc3NvcnlNYXRjaCI6IiIsImJ1bGt5IjowLCJjYXRlZ29yeSI6NjYsImRlbGV0ZSI6ZmFsc2UsImRlc2NyaXB0aW9uIjoiIiwiZ3JvdXAiOiIiLCJpc0FjY2Vzc29yeSI6dHJ1ZSwibW9kZWwiOiIiLCJwYWdlSWQiOjAsInRheG9ub215IjpbXSwidW5pdCI6InN0In0="}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T13:28:50.254122718+02:00","mutation":{"Id":7}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T15:15:53.256709127+02:00","mutation":{"item_id":148722,"quantity":1,"price":2531850,"orgPrice":3164813,"sku":"A0166079","name":"SF vridfönster 1980x1580mm 1-luft, insida trä utsida aluminium, 2-glas pga storleken","image":"/A0166079.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJhY2Nlc3NvcnlNYXRjaCI6IiIsImJvcmRlckhlaWdodCI6MTk0LCJib3JkZXJXaWR0aCI6MTk0LCJidWxreSI6MSwiY2F0ZWdvcnkiOjAsImRlbGV0ZSI6ZmFsc2UsImRlbGl2ZXJ5U3RyaW5nIjoiTm9ybWFsdCA0LTYgYXJiZXRzdmVja29yIiwiZGVsaXZlcnlXZWVrIjoxMCwiZGVzY3JpcHRpb24iOiJLYXJteXR0ZXJtw6V0dCBicmVkZCB4IGjDtmpkLiIsImRpdmlkZXJTaXplIjoxNjQsImdsYXNzTWFyZ2luUG9zdCI6NzEsImdyb3VwIjoiMTM3IiwiaGVpZ2h0IjoxNiwiaGVpZ2h0TWFyZ2luIjoyMCwiaXNBY2Nlc3NvcnkiOjAsImx1ZnQiOiIxIiwibHVmdF90aXRsZSI6IjEtTHVmdCAoZW4gw7ZwcG5pbmdzYmFyIGLDpWdlIGkgc2FtbWEga2FybSkiLCJtb2RlbCI6IjEzNyIsIm1vZGVsRGVzY3JpcHRpb24iOiJTUCBCYWxhbnMgZsO2bnN0ZXIgdnJpZCIsInBhZ2VJZCI6ODg0MzYsInBvc3RTaXplIjo3MSwicHJldmlvdXNEaXNjb3VudCI6MzAsInNwcm9qc1NpemUiOjMxLCJzdG9ybGVrIjoiMjAxNiIsInRheG9ub215IjpbXSwidW5pdCI6InN0Iiwid2lkdGgiOjIwLCJ3aWR0aE1hcmdpbiI6MjB9"}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T15:15:53.256762929+02:00","mutation":{"item_id":170852,"quantity":1,"sku":"A0190103","name":"Rundad profil på karm \u0026 båge, SP utseende","image":"/UserFiles/Svenska_Fonster/Diverse/Genomskarning_rund_profil.jpg","tax":2500,"sellerId":"7","sellerName":"Kaski","parentId":11,"extra_json":"eyJidWxreSI6MCwiY2F0ZWdvcnkiOjE3NCwiZGVsZXRlIjpmYWxzZSwiZGVsaXZlcnlTdHJpbmciOiJOb3JtYWx0IDQtNiBhcmJldHN2ZWNrb3IiLCJkZWxpdmVyeVdlZWsiOjEwLCJkZXNjcmlwdGlvbiI6IiIsImhlaWdodE1hcmdpbiI6MTAsImlzQWNjZXNzb3J5Ijp0cnVlLCJtb2RlbCI6IllEIiwibW9kZWxEZXNjcmlwdGlvbiI6Ikthc2tpIFl0dGVyZMO2cnJhciB0aGVybW8iLCJwYWdlSWQiOjAsInByZXZpb3VzRGlzY291bnQiOjIwLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCIsIndpZHRoTWFyZ2luIjoxMH0="}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T15:15:53.256793727+02:00","mutation":{"item_id":97103,"quantity":1,"price":4139744,"sku":"A0086072","name":"Brand klassat glas EI30 (välj även förankring)","image":"/UserFiles/Svenska_Fonster/Glas/Brand_Ei30.jpg","tax":2500,"sellerId":"0","parentId":11,"extra_json":"eyJhY2Nlc3NvcnlNYXRjaCI6IiIsImJ1bGt5IjowLCJjYXRlZ29yeSI6MjAsImRlbGV0ZSI6ZmFsc2UsImRlc2NyaXB0aW9uIjoiRXR0IGdsYXMgc29tIGtsYXJhciBrcmF2ZW4gRUkzMCIsImdyb3VwIjoiIiwiaXNBY2Nlc3NvcnkiOnRydWUsIm1vZGVsIjoiIiwicGFnZUlkIjowLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCJ9"}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T15:16:03.078662993+02:00","mutation":{"Id":8,"quantity":1}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T15:16:03.54547595+02:00","mutation":{"Id":8}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T15:16:04.240661903+02:00","mutation":{"Id":9}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T15:16:05.581768231+02:00","mutation":{"Id":10}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T15:16:09.40673458+02:00","mutation":{"Id":11}}
|
||||
{"type":"ChangeQuantity","timestamp":"2026-06-14T15:16:11.41069986+02:00","mutation":{"Id":12}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T15:24:17.09761136+02:00","mutation":{"item_id":148722,"quantity":1,"price":2531850,"orgPrice":3164813,"sku":"A0166079","name":"SF vridfönster 1980x1580mm 1-luft, insida trä utsida aluminium, 2-glas pga storleken","image":"/A0166079.wimg","tax":2500,"sellerId":"1","sellerName":"Svenska Fönster","extra_json":"eyJhY2Nlc3NvcnlNYXRjaCI6IiIsImJvcmRlckhlaWdodCI6MTk0LCJib3JkZXJXaWR0aCI6MTk0LCJidWxreSI6MSwiY2F0ZWdvcnkiOjAsImRlbGV0ZSI6ZmFsc2UsImRlbGl2ZXJ5U3RyaW5nIjoiTm9ybWFsdCA0LTYgYXJiZXRzdmVja29yIiwiZGVsaXZlcnlXZWVrIjoxMCwiZGVzY3JpcHRpb24iOiJLYXJteXR0ZXJtw6V0dCBicmVkZCB4IGjDtmpkLiIsImRpdmlkZXJTaXplIjoxNjQsImdsYXNzTWFyZ2luUG9zdCI6NzEsImdyb3VwIjoiMTM3IiwiaGVpZ2h0IjoxNiwiaGVpZ2h0TWFyZ2luIjoyMCwiaXNBY2Nlc3NvcnkiOjAsImx1ZnQiOiIxIiwibHVmdF90aXRsZSI6IjEtTHVmdCAoZW4gw7ZwcG5pbmdzYmFyIGLDpWdlIGkgc2FtbWEga2FybSkiLCJtb2RlbCI6IjEzNyIsIm1vZGVsRGVzY3JpcHRpb24iOiJTUCBCYWxhbnMgZsO2bnN0ZXIgdnJpZCIsInBhZ2VJZCI6ODg0MzYsInBvc3RTaXplIjo3MSwicHJldmlvdXNEaXNjb3VudCI6MzAsInNwcm9qc1NpemUiOjMxLCJzdG9ybGVrIjoiMjAxNiIsInRheG9ub215IjpbXSwidW5pdCI6InN0Iiwid2lkdGgiOjIwLCJ3aWR0aE1hcmdpbiI6MjB9"}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T15:24:17.097670772+02:00","mutation":{"item_id":170852,"quantity":1,"sku":"A0190103","name":"Rundad profil på karm \u0026 båge, SP utseende","image":"/UserFiles/Svenska_Fonster/Diverse/Genomskarning_rund_profil.jpg","tax":2500,"sellerId":"7","sellerName":"Kaski","parentId":13,"extra_json":"eyJidWxreSI6MCwiY2F0ZWdvcnkiOjE3NCwiZGVsZXRlIjpmYWxzZSwiZGVsaXZlcnlTdHJpbmciOiJOb3JtYWx0IDQtNiBhcmJldHN2ZWNrb3IiLCJkZWxpdmVyeVdlZWsiOjEwLCJkZXNjcmlwdGlvbiI6IiIsImhlaWdodE1hcmdpbiI6MTAsImlzQWNjZXNzb3J5Ijp0cnVlLCJtb2RlbCI6IllEIiwibW9kZWxEZXNjcmlwdGlvbiI6Ikthc2tpIFl0dGVyZMO2cnJhciB0aGVybW8iLCJwYWdlSWQiOjAsInByZXZpb3VzRGlzY291bnQiOjIwLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCIsIndpZHRoTWFyZ2luIjoxMH0="}}
|
||||
{"type":"AddItem","timestamp":"2026-06-14T15:24:17.097710397+02:00","mutation":{"item_id":97103,"quantity":1,"price":4139744,"sku":"A0086072","name":"Brand klassat glas EI30 (välj även förankring)","image":"/UserFiles/Svenska_Fonster/Glas/Brand_Ei30.jpg","tax":2500,"sellerId":"0","parentId":13,"extra_json":"eyJhY2Nlc3NvcnlNYXRjaCI6IiIsImJ1bGt5IjowLCJjYXRlZ29yeSI6MjAsImRlbGV0ZSI6ZmFsc2UsImRlc2NyaXB0aW9uIjoiRXR0IGdsYXMgc29tIGtsYXJhciBrcmF2ZW4gRUkzMCIsImdyb3VwIjoiIiwiaXNBY2Nlc3NvcnkiOnRydWUsIm1vZGVsIjoiIiwicGFnZUlkIjowLCJ0YXhvbm9teSI6W10sInVuaXQiOiJzdCJ9"}}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,6 +1,6 @@
|
||||
module git.k6n.net/go-cart-actor
|
||||
|
||||
go 1.25.4
|
||||
go 1.26.3
|
||||
|
||||
require (
|
||||
github.com/adyen/adyen-go-api-library/v21 v21.1.0
|
||||
@@ -8,35 +8,34 @@ require (
|
||||
github.com/matst80/go-redis-inventory v0.0.0-20251126173508-51b30de2d86e
|
||||
github.com/matst80/slask-finder v0.0.0-20251125182907-9e57f193127a
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/redis/go-redis/v9 v9.17.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
|
||||
go.opentelemetry.io/otel/log v0.14.0
|
||||
go.opentelemetry.io/otel/metric v1.38.0
|
||||
go.opentelemetry.io/otel/sdk v1.38.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
github.com/rabbitmq/amqp091-go v1.11.0
|
||||
github.com/redis/go-redis/v9 v9.20.0
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
|
||||
go.opentelemetry.io/otel v1.44.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
|
||||
go.opentelemetry.io/otel/log v0.20.0
|
||||
go.opentelemetry.io/otel/metric v1.44.0
|
||||
go.opentelemetry.io/otel/sdk v1.44.0
|
||||
go.opentelemetry.io/otel/sdk/log v0.20.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0
|
||||
go.opentelemetry.io/otel/trace v1.44.0
|
||||
google.golang.org/grpc v1.81.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
k8s.io/api v0.34.2
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/client-go v0.34.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.18.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dprotaso/go-yit v0.0.0-20251117151522-da16f3077589 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
@@ -62,7 +61,7 @@ require (
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
@@ -77,8 +76,8 @@ require (
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/common v0.68.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/speakeasy-api/jsonpath v0.6.2 // indirect
|
||||
github.com/speakeasy-api/openapi-overlay v0.10.3 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
@@ -86,22 +85,23 @@ require (
|
||||
github.com/woodsbury/decimal128 v1.4.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/term v0.43.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
@@ -116,3 +116,5 @@ require (
|
||||
)
|
||||
|
||||
tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
|
||||
|
||||
replace github.com/matst80/slask-finder => /home/mats/github.com/matst80/slask-finder
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.18.2 h1:oPq3Cgx//iDuJQVp6xSInAKW34J9CEwE5GmLI2z+Eic=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.18.2/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
|
||||
github.com/adyen/adyen-go-api-library/v21 v21.1.0 h1:QIKtn99yoBdt2R4PhuMdmY/DTm6Ex5HYd0cB7Sh3y6Y=
|
||||
github.com/adyen/adyen-go-api-library/v21 v21.1.0/go.mod h1:qsAGYetm761eDAz+f2OQoY4qC+tKNhZOHil1b4FO5zE=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
@@ -20,8 +20,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
||||
github.com/dprotaso/go-yit v0.0.0-20251117151522-da16f3077589 h1:VJ/jVUWr+r4MQA7U/cscbbXRuwh1PfPCUUItYAjlKN4=
|
||||
github.com/dprotaso/go-yit v0.0.0-20251117151522-da16f3077589/go.mod h1:IeI20psFPeg2n1jxwbkYCmkpYsXsJqB7qmoqCIlX80s=
|
||||
@@ -95,8 +93,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
@@ -108,6 +106,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -121,8 +121,6 @@ github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8
|
||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/matst80/go-redis-inventory v0.0.0-20251126173508-51b30de2d86e h1:Z7A73W6jsxFuFKWvB1efQmTjs0s7+x2B7IBM2ukkI6Y=
|
||||
github.com/matst80/go-redis-inventory v0.0.0-20251126173508-51b30de2d86e/go.mod h1:9P52UwIlLWLZvObfO29aKTWUCA9Gm62IuPJ/qv4Xvs0=
|
||||
github.com/matst80/slask-finder v0.0.0-20251125182907-9e57f193127a h1:EfUO5BNDK3a563zQlwJYTNNv46aJFT9gbSItAwZOZ/Y=
|
||||
github.com/matst80/slask-finder v0.0.0-20251125182907-9e57f193127a/go.mod h1:VIPNkIvU0dZKwbSuv75zZcB93MXISm2UyiIPly/ucXQ=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -161,14 +159,14 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM=
|
||||
github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/prometheus/common v0.68.0 h1:8rQJvQmYltsR2L7h8Zw0Iyj8WYNNmpwikoQTZXwfVeA=
|
||||
github.com/prometheus/common v0.68.0/go.mod h1:4soH+U8yJSROk7OJ//hmTiWKsxapv6zRGgTt3keN8gQ=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rabbitmq/amqp091-go v1.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8=
|
||||
github.com/rabbitmq/amqp091-go v1.11.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0=
|
||||
github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
@@ -199,42 +197,48 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
|
||||
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0 h1:5RgvxieNq9tS3ewrV1vnODvbHPfKUIJcYtF9Cvz+6aQ=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0/go.mod h1:iTBIdNwx/xmUhfgJs6+84S4dIK059811cO1eUBjKcHY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0 h1:rydZ9sxbcFdm/oWrVyfLTjHIygMgv0bEeMd+3B/BvoM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0/go.mod h1:earQ25dooT0Hhspq59DZ8YCC50jWfOlFEeWoxy/P444=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
|
||||
go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
|
||||
go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY=
|
||||
go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
||||
@@ -244,57 +248,57 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package actor
|
||||
|
||||
import "sync"
|
||||
|
||||
// keyedMutex provides one logical mutex per key (grain id). It is the
|
||||
// mechanism that enforces the core actor guarantee: a single grain only ever
|
||||
// processes one message (spawn / mutation / read) at a time, while distinct
|
||||
// grains run fully in parallel.
|
||||
//
|
||||
// Locks are reference counted and removed once no caller holds or waits on
|
||||
// them, so memory stays proportional to in-flight grains rather than the total
|
||||
// number of grains ever touched.
|
||||
type keyedMutex struct {
|
||||
mu sync.Mutex
|
||||
locks map[uint64]*keyedMutexEntry
|
||||
}
|
||||
|
||||
type keyedMutexEntry struct {
|
||||
mu sync.Mutex
|
||||
refs int
|
||||
}
|
||||
|
||||
func newKeyedMutex() *keyedMutex {
|
||||
return &keyedMutex{locks: make(map[uint64]*keyedMutexEntry)}
|
||||
}
|
||||
|
||||
// lock acquires the mutex for id and returns an unlock function. The returned
|
||||
// function must be called exactly once.
|
||||
func (k *keyedMutex) lock(id uint64) func() {
|
||||
k.mu.Lock()
|
||||
entry, ok := k.locks[id]
|
||||
if !ok {
|
||||
entry = &keyedMutexEntry{}
|
||||
k.locks[id] = entry
|
||||
}
|
||||
entry.refs++
|
||||
k.mu.Unlock()
|
||||
|
||||
entry.mu.Lock()
|
||||
|
||||
return func() {
|
||||
entry.mu.Unlock()
|
||||
k.mu.Lock()
|
||||
entry.refs--
|
||||
if entry.refs == 0 {
|
||||
delete(k.locks, id)
|
||||
}
|
||||
k.mu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,10 @@ type SimpleGrainPool[V any] struct {
|
||||
ttl time.Duration
|
||||
poolSize int
|
||||
|
||||
// grainLocks serializes spawn + mutation + read per grain id so that each
|
||||
// grain processes a single message at a time (the actor guarantee).
|
||||
grainLocks *keyedMutex
|
||||
|
||||
// Cluster coordination --------------------------------------------------
|
||||
hostname string
|
||||
remoteMu sync.RWMutex
|
||||
@@ -59,6 +63,7 @@ func NewSimpleGrainPool[V any](config GrainPoolConfig[V]) (*SimpleGrainPool[V],
|
||||
hostname: config.Hostname,
|
||||
remoteOwners: make(map[uint64]Host[V]),
|
||||
remoteHosts: make(map[string]Host[V]),
|
||||
grainLocks: newKeyedMutex(),
|
||||
}
|
||||
|
||||
p.purgeTicker = time.NewTicker(time.Minute)
|
||||
@@ -187,7 +192,17 @@ func (p *SimpleGrainPool[V]) AddRemote(host string) (Host[V], error) {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Re-check under the write lock: spawnHost opened a real connection above
|
||||
// without holding the lock, so a concurrent AddRemote for the same host may
|
||||
// have already registered one. Keep the existing entry and close ours to
|
||||
// avoid leaking the gRPC connection / HTTP transport (and its file handles).
|
||||
p.remoteMu.Lock()
|
||||
if existing, found := p.remoteHosts[host]; found {
|
||||
p.remoteMu.Unlock()
|
||||
go remote.Close()
|
||||
return existing, nil
|
||||
}
|
||||
p.remoteHosts[host] = remote
|
||||
p.remoteMu.Unlock()
|
||||
// connectedRemotes.Set(float64(p.RemoteCount()))
|
||||
@@ -221,7 +236,6 @@ func (p *SimpleGrainPool[V]) RemoveHost(host string) {
|
||||
remote, exists := p.remoteHosts[host]
|
||||
|
||||
if exists {
|
||||
go remote.Close()
|
||||
delete(p.remoteHosts, host)
|
||||
}
|
||||
count := 0
|
||||
@@ -234,6 +248,7 @@ func (p *SimpleGrainPool[V]) RemoveHost(host string) {
|
||||
log.Printf("Removing host %s, grains: %d", host, count)
|
||||
p.remoteMu.Unlock()
|
||||
|
||||
// Close once, outside the lock.
|
||||
if exists {
|
||||
remote.Close()
|
||||
}
|
||||
@@ -275,7 +290,9 @@ func (p *SimpleGrainPool[V]) pingLoop(remote Host[V]) {
|
||||
if !remote.Ping() {
|
||||
if !remote.IsHealthy() {
|
||||
log.Printf("Remote %s unhealthy, removing", remote.Name())
|
||||
p.Close()
|
||||
// Remove only this host. Previously this called p.Close(),
|
||||
// which tore down every remote connection and stopped the
|
||||
// purge ticker for the whole pool.
|
||||
p.RemoveHost(remote.Name())
|
||||
return
|
||||
}
|
||||
@@ -397,6 +414,12 @@ func (p *SimpleGrainPool[V]) getOrClaimGrain(ctx context.Context, id uint64) (Gr
|
||||
|
||||
// Apply applies a mutation to a grain.
|
||||
func (p *SimpleGrainPool[V]) Apply(ctx context.Context, id uint64, mutation ...proto.Message) (*MutationResult[V], error) {
|
||||
// Serialize all access to this grain: spawn, mutation handlers and the
|
||||
// final state read happen atomically with respect to other callers of the
|
||||
// same id. Different ids never contend.
|
||||
unlock := p.grainLocks.lock(id)
|
||||
defer unlock()
|
||||
|
||||
grain, err := p.getOrClaimGrain(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -429,6 +452,9 @@ func (p *SimpleGrainPool[V]) Apply(ctx context.Context, id uint64, mutation ...p
|
||||
|
||||
// Get returns the current state of a grain.
|
||||
func (p *SimpleGrainPool[V]) Get(ctx context.Context, id uint64) (*V, error) {
|
||||
unlock := p.grainLocks.lock(id)
|
||||
defer unlock()
|
||||
|
||||
grain, err := p.getOrClaimGrain(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -47,10 +47,19 @@ type CartItem struct {
|
||||
Meta *ItemMeta `json:"meta,omitempty"`
|
||||
SaleStatus string `json:"saleStatus"`
|
||||
Marking *Marking `json:"marking,omitempty"`
|
||||
// CustomFields holds optional user-supplied input fields for this line
|
||||
// (engraving text, configurator notes, ...), keyed by field name.
|
||||
CustomFields map[string]string `json:"customFields,omitempty"`
|
||||
SubscriptionDetailsId string `json:"subscriptionDetailsId,omitempty"`
|
||||
OrderReference string `json:"orderReference,omitempty"`
|
||||
IsSubscribed bool `json:"isSubscribed,omitempty"`
|
||||
ReservationEndTime *time.Time `json:"reservationEndTime,omitempty"`
|
||||
|
||||
// Extra holds arbitrary dynamic product data. Its keys are flattened onto
|
||||
// the item object in JSON (see cart_item_json.go), so they are returned
|
||||
// over the API alongside the typed fields. Typed fields win on key
|
||||
// collisions.
|
||||
Extra map[string]json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
type CartNotification struct {
|
||||
|
||||
@@ -79,6 +79,7 @@ func NewCartMultationRegistry(context *CartMutationContext) actor.MutationRegist
|
||||
actor.NewMutation(SetUserId),
|
||||
actor.NewMutation(LineItemMarking),
|
||||
actor.NewMutation(RemoveLineItemMarking),
|
||||
actor.NewMutation(SetLineItemCustomFields),
|
||||
actor.NewMutation(SubscriptionAdded),
|
||||
// actor.NewMutation(SubscriptionRemoved),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// cartItemAlias mirrors CartItem but has no custom (Un)MarshalJSON, so it is
|
||||
// used to (de)serialize the typed fields without recursing.
|
||||
type cartItemAlias CartItem
|
||||
|
||||
// knownItemKeys is the set of top-level JSON object keys owned by typed
|
||||
// CartItem fields. Any other key in an item object is dynamic and kept in
|
||||
// CartItem.Extra.
|
||||
var knownItemKeys = buildKnownItemKeys()
|
||||
|
||||
func buildKnownItemKeys() map[string]struct{} {
|
||||
keys := make(map[string]struct{})
|
||||
t := reflect.TypeOf(CartItem{})
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",")
|
||||
if name == "" {
|
||||
name = t.Field(i).Name // untagged fields serialize under their Go name
|
||||
}
|
||||
if name == "-" {
|
||||
continue // e.g. Extra itself
|
||||
}
|
||||
keys[name] = struct{}{}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// MarshalJSON emits the typed fields and flattens Extra onto the same object.
|
||||
// Typed fields take precedence on key collisions.
|
||||
func (c CartItem) MarshalJSON() ([]byte, error) {
|
||||
core, err := json.Marshal(cartItemAlias(c))
|
||||
if err != nil || len(c.Extra) == 0 {
|
||||
return core, err
|
||||
}
|
||||
merged := make(map[string]json.RawMessage, len(c.Extra)+8)
|
||||
if err := json.Unmarshal(core, &merged); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range c.Extra {
|
||||
if _, taken := merged[k]; taken {
|
||||
continue
|
||||
}
|
||||
merged[k] = v
|
||||
}
|
||||
return json.Marshal(merged)
|
||||
}
|
||||
|
||||
// UnmarshalJSON reads the typed fields and collects every other key into Extra.
|
||||
func (c *CartItem) UnmarshalJSON(data []byte) error {
|
||||
var alias cartItemAlias
|
||||
if err := json.Unmarshal(data, &alias); err != nil {
|
||||
return err
|
||||
}
|
||||
*c = CartItem(alias)
|
||||
|
||||
all := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal(data, &all); err != nil {
|
||||
return err
|
||||
}
|
||||
for k := range all {
|
||||
if _, known := knownItemKeys[k]; known {
|
||||
delete(all, k)
|
||||
}
|
||||
}
|
||||
if len(all) > 0 {
|
||||
c.Extra = all
|
||||
} else {
|
||||
c.Extra = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCartItem_FlattensDynamicKeys(t *testing.T) {
|
||||
item := CartItem{
|
||||
Id: 7,
|
||||
Sku: "YD10050921H11406",
|
||||
Quantity: 2,
|
||||
Extra: map[string]json.RawMessage{
|
||||
"glas": json.RawMessage(`"V"`),
|
||||
"hangning": json.RawMessage(`"H"`),
|
||||
"deliveryWeek": json.RawMessage(`10`),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&item)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
var flat map[string]json.RawMessage
|
||||
if err := json.Unmarshal(b, &flat); err != nil {
|
||||
t.Fatalf("unmarshal to map: %v", err)
|
||||
}
|
||||
|
||||
// Typed field present at top level.
|
||||
if _, ok := flat["sku"]; !ok {
|
||||
t.Error("expected typed key \"sku\" at top level")
|
||||
}
|
||||
// Dynamic keys flattened to top level (not nested under \"extra\").
|
||||
for _, k := range []string{"glas", "hangning", "deliveryWeek"} {
|
||||
if _, ok := flat[k]; !ok {
|
||||
t.Errorf("expected dynamic key %q flattened to top level", k)
|
||||
}
|
||||
}
|
||||
if string(flat["glas"]) != `"V"` {
|
||||
t.Errorf("glas = %s, want \"V\"", flat["glas"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCartItem_RoundTrip(t *testing.T) {
|
||||
original := CartItem{
|
||||
Id: 3,
|
||||
Sku: "ABC",
|
||||
Quantity: 1,
|
||||
Extra: map[string]json.RawMessage{
|
||||
"color": json.RawMessage(`"red"`),
|
||||
"weight": json.RawMessage(`12.5`),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&original)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
var got CartItem
|
||||
if err := json.Unmarshal(b, &got); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if got.Sku != original.Sku || got.Id != original.Id || got.Quantity != original.Quantity {
|
||||
t.Errorf("typed fields not preserved: got %+v", got)
|
||||
}
|
||||
if len(got.Extra) != 2 {
|
||||
t.Fatalf("Extra len = %d, want 2 (%v)", len(got.Extra), got.Extra)
|
||||
}
|
||||
if string(got.Extra["color"]) != `"red"` {
|
||||
t.Errorf("color = %s, want \"red\"", got.Extra["color"])
|
||||
}
|
||||
// A typed key must never leak into Extra.
|
||||
if _, leaked := got.Extra["sku"]; leaked {
|
||||
t.Error("typed key \"sku\" leaked into Extra")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCartItem_NoExtraIsCompact(t *testing.T) {
|
||||
item := CartItem{Id: 1, Sku: "X", Quantity: 1}
|
||||
b, err := json.Marshal(&item)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
var flat map[string]json.RawMessage
|
||||
if err := json.Unmarshal(b, &flat); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if _, ok := flat["sku"]; !ok {
|
||||
t.Error("expected \"sku\" in output")
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -11,6 +12,20 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// decodeExtra unpacks the dynamic product data carried as raw JSON. Invalid
|
||||
// payloads are logged and dropped rather than failing the mutation.
|
||||
func decodeExtra(b []byte) map[string]json.RawMessage {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
m := map[string]json.RawMessage{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
log.Printf("AddItem: invalid extra_json: %v", err)
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// mutation_add_item.go
|
||||
//
|
||||
// Registers the AddItem cart mutation in the generic mutation registry.
|
||||
@@ -18,10 +33,13 @@ import (
|
||||
//
|
||||
// Behavior:
|
||||
// - Validates quantity > 0
|
||||
// - If an item with same SKU exists -> increases quantity
|
||||
// - If an item with the same item id (ItemId) exists -> increases quantity
|
||||
// - Else creates a new CartItem with computed tax amounts
|
||||
// - Totals recalculated automatically via WithTotals()
|
||||
//
|
||||
// Item identity is the catalog item id (ItemId), not the SKU: the product
|
||||
// service is looked up by id and the returned SKU is reference-only.
|
||||
//
|
||||
// NOTE: Any future field additions in messages.AddItem that affect pricing / tax
|
||||
// must keep this handler in sync.
|
||||
var ErrPaymentInProgress = errors.New("payment in progress")
|
||||
@@ -35,9 +53,10 @@ func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) er
|
||||
return fmt.Errorf("AddItem: invalid quantity %d", m.Quantity)
|
||||
}
|
||||
|
||||
// Merge with any existing item having same SKU and matching StoreId (including both nil).
|
||||
// Merge with any existing item having the same item id and matching StoreId
|
||||
// (including both nil). Identity is the id; SKU is reference-only.
|
||||
for _, existing := range g.Items {
|
||||
if existing.Sku != m.Sku {
|
||||
if existing.ItemId != m.ItemId {
|
||||
continue
|
||||
}
|
||||
sameStore := (existing.StoreId == nil && m.StoreId == nil) ||
|
||||
@@ -61,6 +80,14 @@ func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) er
|
||||
if existing.StoreId == nil && m.StoreId != nil {
|
||||
existing.StoreId = m.StoreId
|
||||
}
|
||||
// Refresh dynamic product data with the latest payload.
|
||||
if extra := decodeExtra(m.ExtraJson); extra != nil {
|
||||
existing.Extra = extra
|
||||
}
|
||||
// Replace custom fields when provided on the re-add.
|
||||
if len(m.CustomFields) > 0 {
|
||||
existing.CustomFields = m.CustomFields
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -114,6 +141,9 @@ func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) er
|
||||
ArticleType: m.ArticleType,
|
||||
|
||||
StoreId: m.StoreId,
|
||||
|
||||
Extra: decodeExtra(m.ExtraJson),
|
||||
CustomFields: m.CustomFields,
|
||||
}
|
||||
|
||||
if needsReservation && c.UseReservations(cartItem) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
// Item identity is the catalog item id, not the (reference-only) SKU: two adds
|
||||
// with the same ItemId but different SKU strings must merge into one line.
|
||||
func TestAddItem_MergesByItemIdNotSku(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 144047, Sku: "A0162590", Quantity: 1, Price: 1000}); err != nil {
|
||||
t.Fatalf("first add: %v", err)
|
||||
}
|
||||
// Same id, different (reference) sku string -> should merge, not create a line.
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 144047, Sku: "STALE-REF", Quantity: 2, Price: 1000}); err != nil {
|
||||
t.Fatalf("second add: %v", err)
|
||||
}
|
||||
|
||||
if len(g.Items) != 1 {
|
||||
t.Fatalf("items = %d, want 1 (merged by id)", len(g.Items))
|
||||
}
|
||||
if g.Items[0].Quantity != 3 {
|
||||
t.Errorf("quantity = %d, want 3", g.Items[0].Quantity)
|
||||
}
|
||||
|
||||
// A different id is a distinct line.
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 170852, Sku: "A0190103", Quantity: 1, Price: 500}); err != nil {
|
||||
t.Fatalf("third add: %v", err)
|
||||
}
|
||||
if len(g.Items) != 2 {
|
||||
t.Fatalf("items = %d, want 2", len(g.Items))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
func TestAddItem_StoresCustomFields(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{
|
||||
ItemId: 100, Sku: "P", Quantity: 1, Price: 1000,
|
||||
CustomFields: map[string]string{"engraving": "Happy Birthday", "color": "blue"},
|
||||
})
|
||||
|
||||
if len(g.Items) != 1 {
|
||||
t.Fatalf("items = %d, want 1", len(g.Items))
|
||||
}
|
||||
cf := g.Items[0].CustomFields
|
||||
if cf["engraving"] != "Happy Birthday" || cf["color"] != "blue" {
|
||||
t.Fatalf("custom fields = %v, want engraving+color", cf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLineItemCustomFields_Merges(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{
|
||||
ItemId: 100, Sku: "P", Quantity: 1, Price: 1000,
|
||||
CustomFields: map[string]string{"engraving": "v1"},
|
||||
})
|
||||
line := g.Items[0].Id
|
||||
|
||||
// Upsert: overwrite "engraving", add "note", leave others alone.
|
||||
mustApply(t, reg, g, &cart_messages.SetLineItemCustomFields{
|
||||
Id: line,
|
||||
CustomFields: map[string]string{"engraving": "v2", "note": "gift wrap"},
|
||||
})
|
||||
|
||||
cf := g.Items[0].CustomFields
|
||||
if cf["engraving"] != "v2" {
|
||||
t.Errorf("engraving = %q, want v2", cf["engraving"])
|
||||
}
|
||||
if cf["note"] != "gift wrap" {
|
||||
t.Errorf("note = %q, want 'gift wrap'", cf["note"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLineItemCustomFields_UnknownItem(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
// The handler error is reported per-mutation in the ApplyResult (the
|
||||
// registry only returns a top-level error for unregistered mutations).
|
||||
results, _ := reg.Apply(context.Background(), g, &cart_messages.SetLineItemCustomFields{
|
||||
Id: 999, CustomFields: map[string]string{"x": "y"},
|
||||
})
|
||||
if len(results) != 1 || results[0].Error == nil {
|
||||
t.Errorf("expected per-mutation error for unknown item id, got %+v", results)
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,17 @@ import (
|
||||
//
|
||||
// Behavior:
|
||||
// - Removes the cart line whose local cart line Id == payload.Id
|
||||
// - Cascades: also removes any line whose ParentId points at a removed line
|
||||
// (transitively), so removing a parent removes its child sub-articles
|
||||
// - If no such line exists returns an error
|
||||
// - Recalculates cart totals (WithTotals)
|
||||
// - Releases reservations for every removed line and recalculates totals
|
||||
//
|
||||
// Notes:
|
||||
// - This removes only the line item; any deliveries referencing the removed
|
||||
// - This removes only the line items; any deliveries referencing a removed
|
||||
// item are NOT automatically adjusted (mirrors prior logic). If future
|
||||
// semantics require pruning delivery.item_ids you can extend this handler.
|
||||
// - If multiple lines somehow shared the same Id (should not happen), only
|
||||
// the first match would be removed—data integrity relies on unique line Ids.
|
||||
// - If multiple lines somehow shared the same Id (should not happen), all
|
||||
// matches are removed—data integrity relies on unique line Ids.
|
||||
|
||||
func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) error {
|
||||
if m == nil {
|
||||
@@ -32,26 +34,46 @@ func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) e
|
||||
|
||||
targetID := uint32(m.Id)
|
||||
|
||||
index := -1
|
||||
for i, it := range g.Items {
|
||||
found := false
|
||||
for _, it := range g.Items {
|
||||
if it.Id == targetID {
|
||||
index = i
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
if !found {
|
||||
return fmt.Errorf("RemoveItem: item id %d not found", m.Id)
|
||||
}
|
||||
|
||||
item := g.Items[index]
|
||||
if item.ReservationEndTime != nil && item.ReservationEndTime.After(time.Now()) {
|
||||
err := c.ReleaseItem(context.Background(), g.Id, item.Sku, item.StoreId)
|
||||
if err != nil {
|
||||
log.Printf("unable to release item reservation")
|
||||
// Collect the target and, transitively, any children pointing at a removed
|
||||
// line. Loops until no further descendants are found (handles nesting).
|
||||
remove := map[uint32]bool{targetID: true}
|
||||
for {
|
||||
grew := false
|
||||
for _, it := range g.Items {
|
||||
if it.ParentId != nil && remove[*it.ParentId] && !remove[it.Id] {
|
||||
remove[it.Id] = true
|
||||
grew = true
|
||||
}
|
||||
}
|
||||
if !grew {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
g.Items = append(g.Items[:index], g.Items[index+1:]...)
|
||||
kept := g.Items[:0]
|
||||
for _, it := range g.Items {
|
||||
if remove[it.Id] {
|
||||
if it.ReservationEndTime != nil && it.ReservationEndTime.After(time.Now()) {
|
||||
if err := c.ReleaseItem(context.Background(), g.Id, it.Sku, it.StoreId); err != nil {
|
||||
log.Printf("unable to release reservation for item %d: %v", it.Id, err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
kept = append(kept, it)
|
||||
}
|
||||
g.Items = kept
|
||||
g.UpdateTotals()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// Removing a parent line cascades to its child sub-articles, while unrelated
|
||||
// lines are left untouched.
|
||||
func TestRemoveItem_CascadesToChildren(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
ctx := context.Background()
|
||||
|
||||
// Parent (line 1) + two children (lines 2,3) + an unrelated item (line 4).
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 100, Sku: "P", Quantity: 1, Price: 1000})
|
||||
parentLine := g.Items[0].Id
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 101, Sku: "C1", Quantity: 1, Price: 100, ParentId: &parentLine})
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 102, Sku: "C2", Quantity: 1, Price: 200, ParentId: &parentLine})
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 200, Sku: "OTHER", Quantity: 1, Price: 300})
|
||||
|
||||
if len(g.Items) != 4 {
|
||||
t.Fatalf("setup: items = %d, want 4", len(g.Items))
|
||||
}
|
||||
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.RemoveItem{Id: parentLine}); err != nil {
|
||||
t.Fatalf("remove parent: %v", err)
|
||||
}
|
||||
|
||||
if len(g.Items) != 1 {
|
||||
t.Fatalf("after remove: items = %d, want 1 (only the unrelated line)", len(g.Items))
|
||||
}
|
||||
if g.Items[0].Sku != "OTHER" {
|
||||
t.Errorf("remaining item sku = %q, want OTHER", g.Items[0].Sku)
|
||||
}
|
||||
}
|
||||
|
||||
// Removing a child leaves the parent and siblings intact.
|
||||
func TestRemoveItem_ChildDoesNotRemoveParent(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
ctx := context.Background()
|
||||
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 100, Sku: "P", Quantity: 1, Price: 1000})
|
||||
parentLine := g.Items[0].Id
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 101, Sku: "C1", Quantity: 1, Price: 100, ParentId: &parentLine})
|
||||
childLine := g.Items[1].Id
|
||||
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.RemoveItem{Id: childLine}); err != nil {
|
||||
t.Fatalf("remove child: %v", err)
|
||||
}
|
||||
if len(g.Items) != 1 {
|
||||
t.Fatalf("items = %d, want 1 (parent remains)", len(g.Items))
|
||||
}
|
||||
if g.Items[0].Id != parentLine {
|
||||
t.Errorf("remaining line = %d, want parent %d", g.Items[0].Id, parentLine)
|
||||
}
|
||||
}
|
||||
|
||||
func mustApply(t *testing.T, reg actor.MutationRegistry, g *CartGrain, m proto.Message) {
|
||||
t.Helper()
|
||||
if _, err := reg.Apply(context.Background(), g, m); err != nil {
|
||||
t.Fatalf("apply %T: %v", m, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
// SetLineItemCustomFields sets/merges user-supplied custom input fields on an
|
||||
// existing cart line. It is the dict equivalent of LineItemMarking: keys in the
|
||||
// request are upserted; existing keys not present in the request are left
|
||||
// untouched. (Send an empty value to clear a single field at the API layer if
|
||||
// desired.)
|
||||
func SetLineItemCustomFields(grain *CartGrain, req *messages.SetLineItemCustomFields) error {
|
||||
for _, item := range grain.Items {
|
||||
if item.Id != req.Id {
|
||||
continue
|
||||
}
|
||||
if len(req.CustomFields) == 0 {
|
||||
return nil
|
||||
}
|
||||
if item.CustomFields == nil {
|
||||
item.CustomFields = make(map[string]string, len(req.CustomFields))
|
||||
}
|
||||
for k, v := range req.CustomFields {
|
||||
item.CustomFields[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.Id)
|
||||
}
|
||||
+42
-2
@@ -69,7 +69,15 @@ func (m *MockResponseWriter) WriteHeader(statusCode int) {
|
||||
m.StatusCode = statusCode
|
||||
}
|
||||
|
||||
func NewRemoteHost[V any](host string) (*RemoteHost[V], error) {
|
||||
// NewRemoteHost connects to a peer node. httpPort overrides the peer's HTTP
|
||||
// port used for request proxying (defaults to "8080"); the gRPC control port
|
||||
// is always 1337.
|
||||
func NewRemoteHost[V any](host string, httpPort ...string) (*RemoteHost[V], error) {
|
||||
|
||||
port := "8080"
|
||||
if len(httpPort) > 0 && httpPort[0] != "" {
|
||||
port = httpPort[0]
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:1337", host)
|
||||
|
||||
@@ -85,6 +93,10 @@ func NewRemoteHost[V any](host string) (*RemoteHost[V], error) {
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
// Bound concurrently-open connections to this peer so a load spike
|
||||
// can't exhaust file handles. Excess requests wait for a free conn
|
||||
// rather than opening unbounded sockets.
|
||||
MaxConnsPerHost: 256,
|
||||
DisableKeepAlives: false,
|
||||
IdleConnTimeout: 120 * time.Second,
|
||||
}
|
||||
@@ -92,7 +104,7 @@ func NewRemoteHost[V any](host string) (*RemoteHost[V], error) {
|
||||
|
||||
return &RemoteHost[V]{
|
||||
host: host,
|
||||
httpBase: fmt.Sprintf("http://%s:8080", host),
|
||||
httpBase: fmt.Sprintf("http://%s:%s", host, port),
|
||||
conn: conn,
|
||||
transport: transport,
|
||||
client: client,
|
||||
@@ -101,6 +113,25 @@ func NewRemoteHost[V any](host string) (*RemoteHost[V], error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// hopByHopHeaders are connection-specific headers that must not be forwarded
|
||||
// by a proxy (RFC 7230 §6.1).
|
||||
var hopByHopHeaders = map[string]struct{}{
|
||||
"Connection": {},
|
||||
"Proxy-Connection": {},
|
||||
"Keep-Alive": {},
|
||||
"Proxy-Authenticate": {},
|
||||
"Proxy-Authorization": {},
|
||||
"Te": {},
|
||||
"Trailer": {},
|
||||
"Transfer-Encoding": {},
|
||||
"Upgrade": {},
|
||||
}
|
||||
|
||||
func isHopByHopHeader(key string) bool {
|
||||
_, ok := hopByHopHeaders[http.CanonicalHeaderKey(key)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (h *RemoteHost[V]) Name() string {
|
||||
return h.host
|
||||
}
|
||||
@@ -277,6 +308,12 @@ func (h *RemoteHost[V]) Proxy(id uint64, w http.ResponseWriter, r *http.Request,
|
||||
req.Header.Set("X-Forwarded-Host", r.Host)
|
||||
|
||||
for k, v := range r.Header {
|
||||
if isHopByHopHeader(k) {
|
||||
// Don't forward connection-management headers (notably an inbound
|
||||
// "Connection: close" would disable keep-alive on the proxied leg
|
||||
// and churn outbound connections / file handles).
|
||||
continue
|
||||
}
|
||||
for _, vv := range v {
|
||||
req.Header.Add(k, vv)
|
||||
}
|
||||
@@ -290,6 +327,9 @@ func (h *RemoteHost[V]) Proxy(id uint64, w http.ResponseWriter, r *http.Request,
|
||||
defer res.Body.Close()
|
||||
span.SetAttributes(attribute.Int("status_code", res.StatusCode))
|
||||
for k, v := range res.Header {
|
||||
if isHopByHopHeader(k) {
|
||||
continue
|
||||
}
|
||||
for _, vv := range v {
|
||||
w.Header().Add(k, vv)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ message AddItem {
|
||||
optional uint32 parentId = 23;
|
||||
string cgm = 25;
|
||||
optional google.protobuf.Timestamp reservationEndTime = 26;
|
||||
// extra_json carries arbitrary product data (the flat product document minus
|
||||
// the fields mapped above) as raw JSON. It is stored losslessly and surfaced
|
||||
// as flattened keys on the cart item.
|
||||
bytes extra_json = 27;
|
||||
// custom_fields holds optional user-supplied input fields for this line
|
||||
// (e.g. engraving text, configurator notes), keyed by field name.
|
||||
map<string, string> custom_fields = 28;
|
||||
}
|
||||
|
||||
message RemoveItem { uint32 Id = 1; }
|
||||
@@ -57,6 +64,13 @@ message RemoveLineItemMarking {
|
||||
uint32 id = 1;
|
||||
}
|
||||
|
||||
// SetLineItemCustomFields sets/merges user-supplied custom input fields on an
|
||||
// existing cart line (the dict equivalent of a line item marking).
|
||||
message SetLineItemCustomFields {
|
||||
uint32 id = 1;
|
||||
map<string, string> custom_fields = 2;
|
||||
}
|
||||
|
||||
message SubscriptionAdded {
|
||||
uint32 itemId = 1;
|
||||
string detailsId = 3;
|
||||
@@ -89,6 +103,7 @@ message Mutation {
|
||||
LineItemMarking line_item_marking = 6;
|
||||
RemoveLineItemMarking remove_line_item_marking = 7;
|
||||
SubscriptionAdded subscription_added = 8;
|
||||
SetLineItemCustomFields set_line_item_custom_fields = 9;
|
||||
AddVoucher add_voucher = 20;
|
||||
RemoveVoucher remove_voucher = 21;
|
||||
UpsertSubscriptionDetails upsert_subscription_details = 22;
|
||||
|
||||
+162
-51
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc v6.33.1
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// source: cart.proto
|
||||
|
||||
package cart_messages
|
||||
@@ -87,6 +87,13 @@ type AddItem struct {
|
||||
ParentId *uint32 `protobuf:"varint,23,opt,name=parentId,proto3,oneof" json:"parentId,omitempty"`
|
||||
Cgm string `protobuf:"bytes,25,opt,name=cgm,proto3" json:"cgm,omitempty"`
|
||||
ReservationEndTime *timestamppb.Timestamp `protobuf:"bytes,26,opt,name=reservationEndTime,proto3,oneof" json:"reservationEndTime,omitempty"`
|
||||
// extra_json carries arbitrary product data (the flat product document minus
|
||||
// the fields mapped above) as raw JSON. It is stored losslessly and surfaced
|
||||
// as flattened keys on the cart item.
|
||||
ExtraJson []byte `protobuf:"bytes,27,opt,name=extra_json,json=extraJson,proto3" json:"extra_json,omitempty"`
|
||||
// custom_fields holds optional user-supplied input fields for this line
|
||||
// (e.g. engraving text, configurator notes), keyed by field name.
|
||||
CustomFields map[string]string `protobuf:"bytes,28,rep,name=custom_fields,json=customFields,proto3" json:"custom_fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -303,6 +310,20 @@ func (x *AddItem) GetReservationEndTime() *timestamppb.Timestamp {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AddItem) GetExtraJson() []byte {
|
||||
if x != nil {
|
||||
return x.ExtraJson
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AddItem) GetCustomFields() map[string]string {
|
||||
if x != nil {
|
||||
return x.CustomFields
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RemoveItem struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint32 `protobuf:"varint,1,opt,name=Id,proto3" json:"Id,omitempty"`
|
||||
@@ -547,6 +568,60 @@ func (x *RemoveLineItemMarking) GetId() uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// SetLineItemCustomFields sets/merges user-supplied custom input fields on an
|
||||
// existing cart line (the dict equivalent of a line item marking).
|
||||
type SetLineItemCustomFields struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
CustomFields map[string]string `protobuf:"bytes,2,rep,name=custom_fields,json=customFields,proto3" json:"custom_fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetLineItemCustomFields) Reset() {
|
||||
*x = SetLineItemCustomFields{}
|
||||
mi := &file_cart_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetLineItemCustomFields) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetLineItemCustomFields) ProtoMessage() {}
|
||||
|
||||
func (x *SetLineItemCustomFields) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_cart_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetLineItemCustomFields.ProtoReflect.Descriptor instead.
|
||||
func (*SetLineItemCustomFields) Descriptor() ([]byte, []int) {
|
||||
return file_cart_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *SetLineItemCustomFields) GetId() uint32 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SetLineItemCustomFields) GetCustomFields() map[string]string {
|
||||
if x != nil {
|
||||
return x.CustomFields
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SubscriptionAdded struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ItemId uint32 `protobuf:"varint,1,opt,name=itemId,proto3" json:"itemId,omitempty"`
|
||||
@@ -558,7 +633,7 @@ type SubscriptionAdded struct {
|
||||
|
||||
func (x *SubscriptionAdded) Reset() {
|
||||
*x = SubscriptionAdded{}
|
||||
mi := &file_cart_proto_msgTypes[7]
|
||||
mi := &file_cart_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -570,7 +645,7 @@ func (x *SubscriptionAdded) String() string {
|
||||
func (*SubscriptionAdded) ProtoMessage() {}
|
||||
|
||||
func (x *SubscriptionAdded) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_cart_proto_msgTypes[7]
|
||||
mi := &file_cart_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -583,7 +658,7 @@ func (x *SubscriptionAdded) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use SubscriptionAdded.ProtoReflect.Descriptor instead.
|
||||
func (*SubscriptionAdded) Descriptor() ([]byte, []int) {
|
||||
return file_cart_proto_rawDescGZIP(), []int{7}
|
||||
return file_cart_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *SubscriptionAdded) GetItemId() uint32 {
|
||||
@@ -619,7 +694,7 @@ type AddVoucher struct {
|
||||
|
||||
func (x *AddVoucher) Reset() {
|
||||
*x = AddVoucher{}
|
||||
mi := &file_cart_proto_msgTypes[8]
|
||||
mi := &file_cart_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -631,7 +706,7 @@ func (x *AddVoucher) String() string {
|
||||
func (*AddVoucher) ProtoMessage() {}
|
||||
|
||||
func (x *AddVoucher) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_cart_proto_msgTypes[8]
|
||||
mi := &file_cart_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -644,7 +719,7 @@ func (x *AddVoucher) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use AddVoucher.ProtoReflect.Descriptor instead.
|
||||
func (*AddVoucher) Descriptor() ([]byte, []int) {
|
||||
return file_cart_proto_rawDescGZIP(), []int{8}
|
||||
return file_cart_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *AddVoucher) GetCode() string {
|
||||
@@ -684,7 +759,7 @@ type RemoveVoucher struct {
|
||||
|
||||
func (x *RemoveVoucher) Reset() {
|
||||
*x = RemoveVoucher{}
|
||||
mi := &file_cart_proto_msgTypes[9]
|
||||
mi := &file_cart_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -696,7 +771,7 @@ func (x *RemoveVoucher) String() string {
|
||||
func (*RemoveVoucher) ProtoMessage() {}
|
||||
|
||||
func (x *RemoveVoucher) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_cart_proto_msgTypes[9]
|
||||
mi := &file_cart_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -709,7 +784,7 @@ func (x *RemoveVoucher) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RemoveVoucher.ProtoReflect.Descriptor instead.
|
||||
func (*RemoveVoucher) Descriptor() ([]byte, []int) {
|
||||
return file_cart_proto_rawDescGZIP(), []int{9}
|
||||
return file_cart_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *RemoveVoucher) GetId() uint32 {
|
||||
@@ -731,7 +806,7 @@ type UpsertSubscriptionDetails struct {
|
||||
|
||||
func (x *UpsertSubscriptionDetails) Reset() {
|
||||
*x = UpsertSubscriptionDetails{}
|
||||
mi := &file_cart_proto_msgTypes[10]
|
||||
mi := &file_cart_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -743,7 +818,7 @@ func (x *UpsertSubscriptionDetails) String() string {
|
||||
func (*UpsertSubscriptionDetails) ProtoMessage() {}
|
||||
|
||||
func (x *UpsertSubscriptionDetails) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_cart_proto_msgTypes[10]
|
||||
mi := &file_cart_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -756,7 +831,7 @@ func (x *UpsertSubscriptionDetails) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use UpsertSubscriptionDetails.ProtoReflect.Descriptor instead.
|
||||
func (*UpsertSubscriptionDetails) Descriptor() ([]byte, []int) {
|
||||
return file_cart_proto_rawDescGZIP(), []int{10}
|
||||
return file_cart_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *UpsertSubscriptionDetails) GetId() string {
|
||||
@@ -799,6 +874,7 @@ type Mutation struct {
|
||||
// *Mutation_LineItemMarking
|
||||
// *Mutation_RemoveLineItemMarking
|
||||
// *Mutation_SubscriptionAdded
|
||||
// *Mutation_SetLineItemCustomFields
|
||||
// *Mutation_AddVoucher
|
||||
// *Mutation_RemoveVoucher
|
||||
// *Mutation_UpsertSubscriptionDetails
|
||||
@@ -809,7 +885,7 @@ type Mutation struct {
|
||||
|
||||
func (x *Mutation) Reset() {
|
||||
*x = Mutation{}
|
||||
mi := &file_cart_proto_msgTypes[11]
|
||||
mi := &file_cart_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -821,7 +897,7 @@ func (x *Mutation) String() string {
|
||||
func (*Mutation) ProtoMessage() {}
|
||||
|
||||
func (x *Mutation) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_cart_proto_msgTypes[11]
|
||||
mi := &file_cart_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -834,7 +910,7 @@ func (x *Mutation) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Mutation.ProtoReflect.Descriptor instead.
|
||||
func (*Mutation) Descriptor() ([]byte, []int) {
|
||||
return file_cart_proto_rawDescGZIP(), []int{11}
|
||||
return file_cart_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *Mutation) GetType() isMutation_Type {
|
||||
@@ -916,6 +992,15 @@ func (x *Mutation) GetSubscriptionAdded() *SubscriptionAdded {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Mutation) GetSetLineItemCustomFields() *SetLineItemCustomFields {
|
||||
if x != nil {
|
||||
if x, ok := x.Type.(*Mutation_SetLineItemCustomFields); ok {
|
||||
return x.SetLineItemCustomFields
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Mutation) GetAddVoucher() *AddVoucher {
|
||||
if x != nil {
|
||||
if x, ok := x.Type.(*Mutation_AddVoucher); ok {
|
||||
@@ -979,6 +1064,10 @@ type Mutation_SubscriptionAdded struct {
|
||||
SubscriptionAdded *SubscriptionAdded `protobuf:"bytes,8,opt,name=subscription_added,json=subscriptionAdded,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Mutation_SetLineItemCustomFields struct {
|
||||
SetLineItemCustomFields *SetLineItemCustomFields `protobuf:"bytes,9,opt,name=set_line_item_custom_fields,json=setLineItemCustomFields,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Mutation_AddVoucher struct {
|
||||
AddVoucher *AddVoucher `protobuf:"bytes,20,opt,name=add_voucher,json=addVoucher,proto3,oneof"`
|
||||
}
|
||||
@@ -1007,6 +1096,8 @@ func (*Mutation_RemoveLineItemMarking) isMutation_Type() {}
|
||||
|
||||
func (*Mutation_SubscriptionAdded) isMutation_Type() {}
|
||||
|
||||
func (*Mutation_SetLineItemCustomFields) isMutation_Type() {}
|
||||
|
||||
func (*Mutation_AddVoucher) isMutation_Type() {}
|
||||
|
||||
func (*Mutation_RemoveVoucher) isMutation_Type() {}
|
||||
@@ -1019,7 +1110,7 @@ const file_cart_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"cart.proto\x12\rcart_messages\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x12\n" +
|
||||
"\x10ClearCartRequest\"\xb1\x06\n" +
|
||||
"\x10ClearCartRequest\"\xe0\a\n" +
|
||||
"\aAddItem\x12\x17\n" +
|
||||
"\aitem_id\x18\x01 \x01(\rR\x06itemId\x12\x1a\n" +
|
||||
"\bquantity\x18\x02 \x01(\x05R\bquantity\x12\x14\n" +
|
||||
@@ -1053,7 +1144,13 @@ const file_cart_proto_rawDesc = "" +
|
||||
"\astoreId\x18\x16 \x01(\tH\x01R\astoreId\x88\x01\x01\x12\x1f\n" +
|
||||
"\bparentId\x18\x17 \x01(\rH\x02R\bparentId\x88\x01\x01\x12\x10\n" +
|
||||
"\x03cgm\x18\x19 \x01(\tR\x03cgm\x12O\n" +
|
||||
"\x12reservationEndTime\x18\x1a \x01(\v2\x1a.google.protobuf.TimestampH\x03R\x12reservationEndTime\x88\x01\x01B\t\n" +
|
||||
"\x12reservationEndTime\x18\x1a \x01(\v2\x1a.google.protobuf.TimestampH\x03R\x12reservationEndTime\x88\x01\x01\x12\x1d\n" +
|
||||
"\n" +
|
||||
"extra_json\x18\x1b \x01(\fR\textraJson\x12M\n" +
|
||||
"\rcustom_fields\x18\x1c \x03(\v2(.cart_messages.AddItem.CustomFieldsEntryR\fcustomFields\x1a?\n" +
|
||||
"\x11CustomFieldsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\t\n" +
|
||||
"\a_outletB\n" +
|
||||
"\n" +
|
||||
"\b_storeIdB\v\n" +
|
||||
@@ -1072,7 +1169,13 @@ const file_cart_proto_rawDesc = "" +
|
||||
"\x04type\x18\x02 \x01(\rR\x04type\x12\x18\n" +
|
||||
"\amarking\x18\x03 \x01(\tR\amarking\"'\n" +
|
||||
"\x15RemoveLineItemMarking\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\rR\x02id\"q\n" +
|
||||
"\x02id\x18\x01 \x01(\rR\x02id\"\xc9\x01\n" +
|
||||
"\x17SetLineItemCustomFields\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\rR\x02id\x12]\n" +
|
||||
"\rcustom_fields\x18\x02 \x03(\v28.cart_messages.SetLineItemCustomFields.CustomFieldsEntryR\fcustomFields\x1a?\n" +
|
||||
"\x11CustomFieldsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"q\n" +
|
||||
"\x11SubscriptionAdded\x12\x16\n" +
|
||||
"\x06itemId\x18\x01 \x01(\rR\x06itemId\x12\x1c\n" +
|
||||
"\tdetailsId\x18\x03 \x01(\tR\tdetailsId\x12&\n" +
|
||||
@@ -1090,7 +1193,7 @@ const file_cart_proto_rawDesc = "" +
|
||||
"\fofferingCode\x18\x02 \x01(\tR\fofferingCode\x12 \n" +
|
||||
"\vsigningType\x18\x03 \x01(\tR\vsigningType\x12(\n" +
|
||||
"\x04data\x18\x04 \x01(\v2\x14.google.protobuf.AnyR\x04dataB\x05\n" +
|
||||
"\x03_id\"\xc0\x06\n" +
|
||||
"\x03_id\"\xa8\a\n" +
|
||||
"\bMutation\x12@\n" +
|
||||
"\n" +
|
||||
"clear_cart\x18\x01 \x01(\v2\x1f.cart_messages.ClearCartRequestH\x00R\tclearCart\x123\n" +
|
||||
@@ -1101,7 +1204,8 @@ const file_cart_proto_rawDesc = "" +
|
||||
"\vset_user_id\x18\x05 \x01(\v2\x18.cart_messages.SetUserIdH\x00R\tsetUserId\x12L\n" +
|
||||
"\x11line_item_marking\x18\x06 \x01(\v2\x1e.cart_messages.LineItemMarkingH\x00R\x0flineItemMarking\x12_\n" +
|
||||
"\x18remove_line_item_marking\x18\a \x01(\v2$.cart_messages.RemoveLineItemMarkingH\x00R\x15removeLineItemMarking\x12Q\n" +
|
||||
"\x12subscription_added\x18\b \x01(\v2 .cart_messages.SubscriptionAddedH\x00R\x11subscriptionAdded\x12<\n" +
|
||||
"\x12subscription_added\x18\b \x01(\v2 .cart_messages.SubscriptionAddedH\x00R\x11subscriptionAdded\x12f\n" +
|
||||
"\x1bset_line_item_custom_fields\x18\t \x01(\v2&.cart_messages.SetLineItemCustomFieldsH\x00R\x17setLineItemCustomFields\x12<\n" +
|
||||
"\vadd_voucher\x18\x14 \x01(\v2\x19.cart_messages.AddVoucherH\x00R\n" +
|
||||
"addVoucher\x12E\n" +
|
||||
"\x0eremove_voucher\x18\x15 \x01(\v2\x1c.cart_messages.RemoveVoucherH\x00R\rremoveVoucher\x12j\n" +
|
||||
@@ -1120,7 +1224,7 @@ func file_cart_proto_rawDescGZIP() []byte {
|
||||
return file_cart_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_cart_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_cart_proto_msgTypes = make([]protoimpl.MessageInfo, 15)
|
||||
var file_cart_proto_goTypes = []any{
|
||||
(*ClearCartRequest)(nil), // 0: cart_messages.ClearCartRequest
|
||||
(*AddItem)(nil), // 1: cart_messages.AddItem
|
||||
@@ -1129,33 +1233,39 @@ var file_cart_proto_goTypes = []any{
|
||||
(*SetUserId)(nil), // 4: cart_messages.SetUserId
|
||||
(*LineItemMarking)(nil), // 5: cart_messages.LineItemMarking
|
||||
(*RemoveLineItemMarking)(nil), // 6: cart_messages.RemoveLineItemMarking
|
||||
(*SubscriptionAdded)(nil), // 7: cart_messages.SubscriptionAdded
|
||||
(*AddVoucher)(nil), // 8: cart_messages.AddVoucher
|
||||
(*RemoveVoucher)(nil), // 9: cart_messages.RemoveVoucher
|
||||
(*UpsertSubscriptionDetails)(nil), // 10: cart_messages.UpsertSubscriptionDetails
|
||||
(*Mutation)(nil), // 11: cart_messages.Mutation
|
||||
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 13: google.protobuf.Any
|
||||
(*SetLineItemCustomFields)(nil), // 7: cart_messages.SetLineItemCustomFields
|
||||
(*SubscriptionAdded)(nil), // 8: cart_messages.SubscriptionAdded
|
||||
(*AddVoucher)(nil), // 9: cart_messages.AddVoucher
|
||||
(*RemoveVoucher)(nil), // 10: cart_messages.RemoveVoucher
|
||||
(*UpsertSubscriptionDetails)(nil), // 11: cart_messages.UpsertSubscriptionDetails
|
||||
(*Mutation)(nil), // 12: cart_messages.Mutation
|
||||
nil, // 13: cart_messages.AddItem.CustomFieldsEntry
|
||||
nil, // 14: cart_messages.SetLineItemCustomFields.CustomFieldsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 16: google.protobuf.Any
|
||||
}
|
||||
var file_cart_proto_depIdxs = []int32{
|
||||
12, // 0: cart_messages.AddItem.reservationEndTime:type_name -> google.protobuf.Timestamp
|
||||
13, // 1: cart_messages.UpsertSubscriptionDetails.data:type_name -> google.protobuf.Any
|
||||
0, // 2: cart_messages.Mutation.clear_cart:type_name -> cart_messages.ClearCartRequest
|
||||
1, // 3: cart_messages.Mutation.add_item:type_name -> cart_messages.AddItem
|
||||
2, // 4: cart_messages.Mutation.remove_item:type_name -> cart_messages.RemoveItem
|
||||
3, // 5: cart_messages.Mutation.change_quantity:type_name -> cart_messages.ChangeQuantity
|
||||
4, // 6: cart_messages.Mutation.set_user_id:type_name -> cart_messages.SetUserId
|
||||
5, // 7: cart_messages.Mutation.line_item_marking:type_name -> cart_messages.LineItemMarking
|
||||
6, // 8: cart_messages.Mutation.remove_line_item_marking:type_name -> cart_messages.RemoveLineItemMarking
|
||||
7, // 9: cart_messages.Mutation.subscription_added:type_name -> cart_messages.SubscriptionAdded
|
||||
8, // 10: cart_messages.Mutation.add_voucher:type_name -> cart_messages.AddVoucher
|
||||
9, // 11: cart_messages.Mutation.remove_voucher:type_name -> cart_messages.RemoveVoucher
|
||||
10, // 12: cart_messages.Mutation.upsert_subscription_details:type_name -> cart_messages.UpsertSubscriptionDetails
|
||||
13, // [13:13] is the sub-list for method output_type
|
||||
13, // [13:13] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
15, // 0: cart_messages.AddItem.reservationEndTime:type_name -> google.protobuf.Timestamp
|
||||
13, // 1: cart_messages.AddItem.custom_fields:type_name -> cart_messages.AddItem.CustomFieldsEntry
|
||||
14, // 2: cart_messages.SetLineItemCustomFields.custom_fields:type_name -> cart_messages.SetLineItemCustomFields.CustomFieldsEntry
|
||||
16, // 3: cart_messages.UpsertSubscriptionDetails.data:type_name -> google.protobuf.Any
|
||||
0, // 4: cart_messages.Mutation.clear_cart:type_name -> cart_messages.ClearCartRequest
|
||||
1, // 5: cart_messages.Mutation.add_item:type_name -> cart_messages.AddItem
|
||||
2, // 6: cart_messages.Mutation.remove_item:type_name -> cart_messages.RemoveItem
|
||||
3, // 7: cart_messages.Mutation.change_quantity:type_name -> cart_messages.ChangeQuantity
|
||||
4, // 8: cart_messages.Mutation.set_user_id:type_name -> cart_messages.SetUserId
|
||||
5, // 9: cart_messages.Mutation.line_item_marking:type_name -> cart_messages.LineItemMarking
|
||||
6, // 10: cart_messages.Mutation.remove_line_item_marking:type_name -> cart_messages.RemoveLineItemMarking
|
||||
8, // 11: cart_messages.Mutation.subscription_added:type_name -> cart_messages.SubscriptionAdded
|
||||
7, // 12: cart_messages.Mutation.set_line_item_custom_fields:type_name -> cart_messages.SetLineItemCustomFields
|
||||
9, // 13: cart_messages.Mutation.add_voucher:type_name -> cart_messages.AddVoucher
|
||||
10, // 14: cart_messages.Mutation.remove_voucher:type_name -> cart_messages.RemoveVoucher
|
||||
11, // 15: cart_messages.Mutation.upsert_subscription_details:type_name -> cart_messages.UpsertSubscriptionDetails
|
||||
16, // [16:16] is the sub-list for method output_type
|
||||
16, // [16:16] is the sub-list for method input_type
|
||||
16, // [16:16] is the sub-list for extension type_name
|
||||
16, // [16:16] is the sub-list for extension extendee
|
||||
0, // [0:16] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_cart_proto_init() }
|
||||
@@ -1164,8 +1274,8 @@ func file_cart_proto_init() {
|
||||
return
|
||||
}
|
||||
file_cart_proto_msgTypes[1].OneofWrappers = []any{}
|
||||
file_cart_proto_msgTypes[10].OneofWrappers = []any{}
|
||||
file_cart_proto_msgTypes[11].OneofWrappers = []any{
|
||||
file_cart_proto_msgTypes[11].OneofWrappers = []any{}
|
||||
file_cart_proto_msgTypes[12].OneofWrappers = []any{
|
||||
(*Mutation_ClearCart)(nil),
|
||||
(*Mutation_AddItem)(nil),
|
||||
(*Mutation_RemoveItem)(nil),
|
||||
@@ -1174,6 +1284,7 @@ func file_cart_proto_init() {
|
||||
(*Mutation_LineItemMarking)(nil),
|
||||
(*Mutation_RemoveLineItemMarking)(nil),
|
||||
(*Mutation_SubscriptionAdded)(nil),
|
||||
(*Mutation_SetLineItemCustomFields)(nil),
|
||||
(*Mutation_AddVoucher)(nil),
|
||||
(*Mutation_RemoveVoucher)(nil),
|
||||
(*Mutation_UpsertSubscriptionDetails)(nil),
|
||||
@@ -1184,7 +1295,7 @@ func file_cart_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_cart_proto_rawDesc), len(file_cart_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 12,
|
||||
NumMessages: 15,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc v6.33.1
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// source: checkout.proto
|
||||
|
||||
package checkout_messages
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc v6.33.1
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// source: control_plane.proto
|
||||
|
||||
package control_plane_messages
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v6.33.1
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// source: control_plane.proto
|
||||
|
||||
package control_plane_messages
|
||||
@@ -170,28 +170,28 @@ type ControlPlaneServer interface {
|
||||
type UnimplementedControlPlaneServer struct{}
|
||||
|
||||
func (UnimplementedControlPlaneServer) Ping(context.Context, *Empty) (*PingReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Ping not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) Negotiate(context.Context, *NegotiateRequest) (*NegotiateReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Negotiate not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Negotiate not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) GetLocalActorIds(context.Context, *Empty) (*ActorIdsReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetLocalActorIds not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetLocalActorIds not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) AnnounceOwnership(context.Context, *OwnershipAnnounce) (*OwnerChangeAck, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AnnounceOwnership not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AnnounceOwnership not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) Apply(context.Context, *ApplyRequest) (*ApplyResult, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Apply not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Apply not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) AnnounceExpiry(context.Context, *ExpiryAnnounce) (*OwnerChangeAck, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AnnounceExpiry not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method AnnounceExpiry not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) Closing(context.Context, *ClosingNotice) (*OwnerChangeAck, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Closing not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Closing not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) Get(context.Context, *GetRequest) (*GetReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method Get not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) mustEmbedUnimplementedControlPlaneServer() {}
|
||||
func (UnimplementedControlPlaneServer) testEmbeddedByValue() {}
|
||||
@@ -204,7 +204,7 @@ type UnsafeControlPlaneServer interface {
|
||||
}
|
||||
|
||||
func RegisterControlPlaneServer(s grpc.ServiceRegistrar, srv ControlPlaneServer) {
|
||||
// If the following call pancis, it indicates UnimplementedControlPlaneServer was
|
||||
// If the following call panics, it indicates UnimplementedControlPlaneServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
||||
Reference in New Issue
Block a user