fixes
Build and Publish / BuildAndDeployArm64 (push) Failing after 49s
Build and Publish / BuildAndDeployAmd64 (push) Has been cancelled

This commit is contained in:
2026-06-16 13:14:35 +02:00
parent faec360789
commit 60722e3414
29 changed files with 1946 additions and 455 deletions
+6 -11
View File
@@ -101,17 +101,12 @@ verify_proto:
fi fi
@echo "$(GREEN)Proto layout OK (no root-level *.pb.go files).$(RESET)" @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: tidy:
@echo "$(YELLOW)Running go mod tidy...$(RESET)" @echo "$(YELLOW)Running go mod tidy...$(RESET)"
+95 -65
View File
@@ -21,7 +21,6 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/redis/go-redis/v9"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "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 redisAddress = os.Getenv("REDIS_ADDRESS")
var redisPassword = os.Getenv("REDIS_PASSWORD") 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 { func getCountryFromHost(host string) string {
if strings.Contains(strings.ToLower(host), "-no") { if strings.Contains(strings.ToLower(host), "-no") {
return "no" return "no"
@@ -83,6 +99,14 @@ func matchesSkuAndLocation(update inventory.InventoryResult, item cart.CartItem)
func main() { 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() controlPlaneConfig := actor.DefaultServerConfig()
promotionData, err := promotions.LoadStateFile("data/promotions.json") promotionData, err := promotions.LoadStateFile("data/promotions.json")
@@ -92,25 +116,25 @@ func main() {
log.Printf("loaded %d promotions", len(promotionData.State.Promotions)) log.Printf("loaded %d promotions", len(promotionData.State.Promotions))
inventoryPubSub := actor.NewPubSub[inventory.InventoryChange]() //inventoryPubSub := actor.NewPubSub[inventory.InventoryChange]()
// promotionService := promotions.NewPromotionService(nil) // promotionService := promotions.NewPromotionService(nil)
rdb := redis.NewClient(&redis.Options{ // rdb := redis.NewClient(&redis.Options{
Addr: redisAddress, // Addr: redisAddress,
Password: redisPassword, // Password: redisPassword,
DB: 0, // DB: 0,
}) // })
inventoryService, err := inventory.NewRedisInventoryService(rdb) // inventoryService, err := inventory.NewRedisInventoryService(rdb)
if err != nil { // if err != nil {
log.Fatalf("Error creating inventory service: %v\n", err) // log.Fatalf("Error creating inventory service: %v\n", err)
} // }
inventoryReservationService, err := inventory.NewRedisCartReservationService(rdb) // inventoryReservationService, err := inventory.NewRedisCartReservationService(rdb)
if err != nil { // if err != nil {
log.Fatalf("Error creating inventory reservation service: %v\n", err) // log.Fatalf("Error creating inventory reservation service: %v\n", err)
} // }
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(inventoryReservationService)) reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
reg.RegisterProcessor( reg.RegisterProcessor(
actor.NewMutationProcessor(func(ctx context.Context, g *cart.CartGrain) error { actor.NewMutationProcessor(func(ctx context.Context, g *cart.CartGrain) error {
_, span := tracer.Start(ctx, "Totals and promotions") _, span := tracer.Start(ctx, "Totals and promotions")
@@ -140,46 +164,46 @@ func main() {
ret := cart.NewCartGrain(id, time.Now()) ret := cart.NewCartGrain(id, time.Now())
// Set baseline lastChange at spawn; replay may update it to last event timestamp. // 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) err := diskStorage.LoadEvents(ctx, id, ret)
if err == nil && inventoryService != nil { // if err == nil && inventoryService != nil {
refs := make([]*inventory.InventoryReference, 0) // refs := make([]*inventory.InventoryReference, 0)
for _, item := range ret.Items { // for _, item := range ret.Items {
refs = append(refs, &inventory.InventoryReference{ // refs = append(refs, &inventory.InventoryReference{
SKU: inventory.SKU(item.Sku), // SKU: inventory.SKU(item.Sku),
LocationID: getLocationId(item), // LocationID: getLocationId(item),
}) // })
} // }
_, span := tracer.Start(ctx, "update inventory") // _, span := tracer.Start(ctx, "update inventory")
defer span.End() // defer span.End()
res, err := inventoryService.GetInventoryBatch(ctx, refs...) // res, err := inventoryService.GetInventoryBatch(ctx, refs...)
if err != nil { // if err != nil {
log.Printf("unable to update inventory %v", err) // log.Printf("unable to update inventory %v", err)
} else { // } else {
for _, update := range res { // for _, update := range res {
for _, item := range ret.Items { // for _, item := range ret.Items {
if matchesSkuAndLocation(update, *item) && update.Quantity != uint32(item.Stock) { // if matchesSkuAndLocation(update, *item) && update.Quantity != uint32(item.Stock) {
// maybe apply an update to give visibility to the cart // // maybe apply an update to give visibility to the cart
item.Stock = uint16(update.Quantity) // item.Stock = uint16(update.Quantity)
} // }
} // }
} // }
} // }
} // }
return ret, err return ret, err
}, },
Destroy: func(grain actor.Grain[cart.CartGrain]) error { Destroy: func(grain actor.Grain[cart.CartGrain]) error {
cart, err := grain.GetCurrentState() // cart, err := grain.GetCurrentState()
if err != nil { // if err != nil {
return err // return err
} // }
inventoryPubSub.Unsubscribe(cart.HandleInventoryChange) //inventoryPubSub.Unsubscribe(cart.HandleInventoryChange)
return nil return nil
}, },
SpawnHost: func(host string) (actor.Host[cart.CartGrain], error) { 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, TTL: 5 * time.Minute,
PoolSize: 2 * 65535, PoolSize: 2 * 65535,
@@ -191,7 +215,7 @@ func main() {
log.Fatalf("Error creating cart pool: %v\n", err) 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{ app := &App{
pool: pool, pool: pool,
@@ -262,10 +286,16 @@ func main() {
mux.HandleFunc("/openapi.json", ServeEmbeddedOpenAPI) mux.HandleFunc("/openapi.json", ServeEmbeddedOpenAPI)
httpAddr := normalizeListenAddr(cartPort)
debugAddr := normalizeListenAddr(getEnv("CART_DEBUG_PORT", "8081"))
srv := &http.Server{ srv := &http.Server{
Addr: ":8080", Addr: httpAddr,
BaseContext: func(net.Listener) context.Context { return ctx }, BaseContext: func(net.Listener) context.Context { return ctx },
ReadTimeout: 10 * time.Second, 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, WriteTimeout: 20 * time.Second,
Handler: otelhttp.NewHandler(mux, "/"), Handler: otelhttp.NewHandler(mux, "/"),
} }
@@ -284,23 +314,23 @@ func main() {
srvErr <- srv.ListenAndServe() srvErr <- srv.ListenAndServe()
}() }()
listener := inventory.NewInventoryChangeListener(rdb, context.Background(), func(changes []inventory.InventoryChange) { // listener := inventory.NewInventoryChangeListener(rdb, context.Background(), func(changes []inventory.InventoryChange) {
for _, change := range changes { // for _, change := range changes {
log.Printf("inventory change: %v", change) // log.Printf("inventory change: %v", change)
inventoryPubSub.Publish(change) // inventoryPubSub.Publish(change)
} // }
}) // })
go func() { // go func() {
err := listener.Start() // err := listener.Start()
if err != nil { // if err != nil {
log.Fatalf("Unable to start inventory listener: %v", err) // 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 { select {
case err = <-srvErr: case err = <-srvErr:
+81 -4
View File
@@ -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": { "/cart/item/{itemId}/marking": {
"put": { "put": {
"summary": "Set marking for line item", "summary": "Set marking for line item",
@@ -955,10 +989,14 @@
}, },
"CartItem": { "CartItem": {
"type": "object", "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": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"itemId": { "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" }, "sku": { "type": "string" },
"price": { "$ref": "#/components/schemas/Price" }, "price": { "$ref": "#/components/schemas/Price" },
"totalPrice": { "$ref": "#/components/schemas/Price" }, "totalPrice": { "$ref": "#/components/schemas/Price" },
@@ -975,11 +1013,19 @@
"meta": { "$ref": "#/components/schemas/ItemMeta" }, "meta": { "$ref": "#/components/schemas/ItemMeta" },
"saleStatus": { "type": "string" }, "saleStatus": { "type": "string" },
"marking": { "$ref": "#/components/schemas/Marking" }, "marking": { "$ref": "#/components/schemas/Marking" },
"customFields": {
"type": "object",
"additionalProperties": { "type": "string" },
"description": "User-supplied custom input fields for this line"
},
"subscriptionDetailsId": { "type": "string" }, "subscriptionDetailsId": { "type": "string" },
"orderReference": { "type": "string" }, "orderReference": { "type": "string" },
"isSubscribed": { "type": "boolean" } "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": { "CartDelivery": {
"type": "object", "type": "object",
@@ -1021,7 +1067,17 @@
"type": "string", "type": "string",
"description": "Two-letter country code (inferred if omitted)" "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"] "required": ["sku"]
}, },
@@ -1042,7 +1098,17 @@
"properties": { "properties": {
"sku": { "type": "string" }, "sku": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 }, "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"] "required": ["sku", "quantity"]
}, },
@@ -1121,6 +1187,17 @@
}, },
"required": ["type", "text"] "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": { "Notice": {
"type": "object", "type": "object",
"properties": { "properties": {
+183 -38
View File
@@ -15,7 +15,6 @@ import (
messages "git.k6n.net/go-cart-actor/proto/cart" messages "git.k6n.net/go-cart-actor/proto/cart"
"git.k6n.net/go-cart-actor/pkg/voucher" "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"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"go.opentelemetry.io/contrib/bridges/otelslog" "go.opentelemetry.io/contrib/bridges/otelslog"
@@ -42,17 +41,13 @@ var (
type PoolServer struct { type PoolServer struct {
actor.GrainPool[cart.CartGrain] actor.GrainPool[cart.CartGrain]
pod_name string 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{ srv := &PoolServer{
GrainPool: pool, GrainPool: pool,
pod_name: pod_name, pod_name: pod_name,
inventoryService: inventoryService,
reservationService: inventoryReservationService,
} }
return srv return srv
@@ -134,6 +129,12 @@ type Item struct {
Sku string `json:"sku"` Sku string `json:"sku"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
StoreId *string `json:"storeId,omitempty"` 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 { type SetCartItems struct {
@@ -141,25 +142,116 @@ type SetCartItems struct {
Items []Item `json:"items"` 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{} wg := sync.WaitGroup{}
mu := sync.Mutex{} for i, itm := range items {
msgs := make([]proto.Message, 0, len(items)) wg.Go(func() {
for _, itm := range items { parentMsg, parentProduct, err := BuildItemMessage(ctx, itm.Sku, itm.Quantity, country, itm.StoreId, nil)
wg.Go( if err != nil {
func() { log.Printf("error adding item %s: %v", itm.Sku, err)
msg, err := GetItemAddMessage(ctx, itm.Sku, itm.Quantity, country, itm.StoreId) return
if err != nil { }
log.Printf("error adding item %s: %v", itm.Sku, err) parentMsg.CustomFields = itm.CustomFields
return 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)
} }
mu.Lock() }
msgs = append(msgs, msg) })
mu.Unlock()
})
} }
wg.Wait() 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 { 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 return err
} }
msgs := make([]proto.Message, 0, len(setCartItems.Items)+1) if _, err := s.ApplyLocal(r.Context(), id, &messages.ClearCartRequest{}); err != nil {
msgs = append(msgs, &messages.ClearCartRequest{}) return err
msgs = append(msgs, getMultipleAddMessages(r.Context(), setCartItems.Items, setCartItems.Country)...) }
groups := buildItemGroups(r.Context(), setCartItems.Items, setCartItems.Country)
reply, err := s.ApplyLocal(r.Context(), id, msgs...) reply, err := s.applyItemGroups(r.Context(), id, groups)
if err != nil { if err != nil {
return err 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) return s.WriteResult(w, reply)
} }
@@ -187,12 +287,18 @@ func (s *PoolServer) AddMultipleItemHandler(w http.ResponseWriter, r *http.Reque
return err return err
} }
msgs := getMultipleAddMessages(r.Context(), setCartItems.Items, setCartItems.Country) groups := buildItemGroups(r.Context(), setCartItems.Items, setCartItems.Country)
reply, err := s.applyItemGroups(r.Context(), id, groups)
reply, err := s.ApplyLocal(r.Context(), id, msgs...)
if err != nil { if err != nil {
return err 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) return s.WriteResult(w, reply)
} }
@@ -201,6 +307,10 @@ type AddRequest struct {
Quantity int32 `json:"quantity"` Quantity int32 `json:"quantity"`
Country string `json:"country"` Country string `json:"country"`
StoreId *string `json:"storeId"` 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 { 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 { if err != nil {
return err 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 { if err != nil {
return err return err
} }
if reply == nil {
reply, err := s.ApplyLocal(r.Context(), id, msg) grain, err := s.Get(r.Context(), uint64(id))
if err != nil { if err != nil {
return err return err
}
return s.WriteResult(w, grain)
} }
return s.WriteResult(w, reply) return s.WriteResult(w, reply)
} }
@@ -436,6 +556,29 @@ func (s *PoolServer) RemoveLineItemMarkingHandler(w http.ResponseWriter, r *http
return s.WriteResult(w, reply) 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 { func (s *PoolServer) InternalApplyMutationHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed) 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("PUT /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
handleFunc("DELETE /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler))) 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/checkout", CookieCartIdHandler(s.ProxyHandler(s.HandleCheckout)))
//mux.HandleFunc("GET /cart/confirmation/{orderId}", CookieCartIdHandler(s.ProxyHandler(s.HandleConfirmation))) //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}/user", CartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
handleFunc("PUT /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler))) 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("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)))
} }
+180 -111
View File
@@ -4,34 +4,77 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"math"
"net/http" "net/http"
"strconv"
"git.k6n.net/go-cart-actor/pkg/cart"
messages "git.k6n.net/go-cart-actor/proto/cart" messages "git.k6n.net/go-cart-actor/proto/cart"
"github.com/matst80/slask-finder/pkg/index"
) )
// TODO make this configurable // consumedKeys are product-document keys that are mapped to typed AddItem
func getBaseUrl(country string) string { // fields. They are stripped from the dynamic remainder so Extra only holds
// if country == "se" { // genuinely dynamic data.
// return "http://s10n-se:8080" var consumedKeys = []string{
// } "id", "sku", "title", "img", "price", "vat",
if country == "no" { "discount", "inStock", "supplierId", "supplierName", "deleted",
return "http://s10n-no.s10n:8080"
}
if country == "se" {
return "http://s10n-se.s10n:8080"
}
return "http://s10n-se.s10n:8080"
} }
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) 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)) innerCtx, span := tracer.Start(ctx, fmt.Sprintf("fetching data for %s", sku))
defer span.End() 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 { if err != nil {
return nil, err return nil, err
} }
@@ -40,111 +83,137 @@ func FetchItem(ctx context.Context, sku string, country string) (*index.DataItem
return nil, err return nil, err
} }
defer res.Body.Close() defer res.Body.Close()
var item index.DataItem if res.StatusCode != http.StatusOK {
err = json.NewDecoder(res.Body).Decode(&item) return nil, fmt.Errorf("product service returned %d for sku %s", res.StatusCode, sku)
return &item, err }
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) { 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) item, err := FetchItem(ctx, sku, country)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
return ToItemAddMessage(item, storeId, qty, country) msg, err := ToItemAddMessage(item, parent, 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]
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
stk := item.GetStock() return msg, item, nil
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 { if storeId == nil {
centralStock, ok := stk[country] stock = item.InStock
if ok { }
if !item.Buyable {
return nil, fmt.Errorf("item not available")
}
if centralStock == 0 && item.SaleStatus == "TBD" { // Top-level items price from their own product; children are priced from the
return nil, fmt.Errorf("no items available") // parent product via the accessory-price hook.
} price := int64(item.Price * 100)
stock = cart.StockStatus(centralStock) if parent != nil {
price = accessoryPrice(parent, item)
}
msg := &messages.AddItem{
ItemId: uint32(item.Id),
Quantity: int32(qty),
Price: price,
OrgPrice: orgPriceFromDiscount(price, item.Discount),
Sku: item.Sku,
Name: item.Title,
Image: item.Img,
Stock: stock,
Tax: int32(item.Vat * 100),
SellerId: strconv.Itoa(item.SupplierId),
SellerName: item.SupplierName,
Country: country,
StoreId: storeId,
}
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
} 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) return msg, nil
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{
ItemId: uint32(item.Id),
Quantity: int32(qty),
Price: int64(price),
OrgPrice: int64(orgPrice),
Sku: item.GetSku(),
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,
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
}
}
func getInt(data float64, ok bool) (int, error) {
if !ok {
return 0, fmt.Errorf("invalid type")
}
return int(data), 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)
}
+60
View File
@@ -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)
}
}
+37
View File
@@ -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"}}
+1
View File
@@ -0,0 +1 @@
{}
+38 -36
View File
@@ -1,6 +1,6 @@
module git.k6n.net/go-cart-actor module git.k6n.net/go-cart-actor
go 1.25.4 go 1.26.3
require ( require (
github.com/adyen/adyen-go-api-library/v21 v21.1.0 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/go-redis-inventory v0.0.0-20251126173508-51b30de2d86e
github.com/matst80/slask-finder v0.0.0-20251125182907-9e57f193127a github.com/matst80/slask-finder v0.0.0-20251125182907-9e57f193127a
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/rabbitmq/amqp091-go v1.10.0 github.com/rabbitmq/amqp091-go v1.11.0
github.com/redis/go-redis/v9 v9.17.0 github.com/redis/go-redis/v9 v9.20.0
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 go.opentelemetry.io/contrib/bridges/otelslog v0.19.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/log v0.14.0 go.opentelemetry.io/otel/log v0.20.0
go.opentelemetry.io/otel/metric v1.38.0 go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk v1.38.0 go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/log v0.14.0 go.opentelemetry.io/otel/sdk/log v0.20.0
go.opentelemetry.io/otel/sdk/metric v1.38.0 go.opentelemetry.io/otel/sdk/metric v1.44.0
go.opentelemetry.io/otel/trace v1.38.0 go.opentelemetry.io/otel/trace v1.44.0
google.golang.org/grpc v1.77.0 google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.10 google.golang.org/protobuf v1.36.11
k8s.io/api v0.34.2 k8s.io/api v0.34.2
k8s.io/apimachinery v0.34.2 k8s.io/apimachinery v0.34.2
k8s.io/client-go v0.34.2 k8s.io/client-go v0.34.2
) )
require ( 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/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/dprotaso/go-yit v0.0.0-20251117151522-da16f3077589 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // 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/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/schema v1.4.1 // 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/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.9.1 // indirect github.com/mailru/easyjson v0.9.1 // indirect
@@ -77,8 +76,8 @@ require (
github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/common v0.68.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.20.1 // indirect
github.com/speakeasy-api/jsonpath v0.6.2 // indirect github.com/speakeasy-api/jsonpath v0.6.2 // indirect
github.com/speakeasy-api/openapi-overlay v0.10.3 // indirect github.com/speakeasy-api/openapi-overlay v0.10.3 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
@@ -86,22 +85,23 @@ require (
github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // 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/v3 v3.0.4 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
golang.org/x/mod v0.30.0 // indirect golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.45.0 // indirect
golang.org/x/term v0.37.0 // indirect golang.org/x/term v0.43.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.39.0 // indirect golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // 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/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // 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 tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
replace github.com/matst80/slask-finder => /home/mats/github.com/matst80/slask-finder
+80 -76
View File
@@ -1,5 +1,5 @@
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw= github.com/RoaringBitmap/roaring/v2 v2.18.2 h1:oPq3Cgx//iDuJQVp6xSInAKW34J9CEwE5GmLI2z+Eic=
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY= 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 h1:QIKtn99yoBdt2R4PhuMdmY/DTm6Ex5HYd0cB7Sh3y6Y=
github.com/adyen/adyen-go-api-library/v21 v21.1.0/go.mod h1:qsAGYetm761eDAz+f2OQoY4qC+tKNhZOHil1b4FO5zE= 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= 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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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-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 h1:VJ/jVUWr+r4MQA7U/cscbbXRuwh1PfPCUUItYAjlKN4=
github.com/dprotaso/go-yit v0.0.0-20251117151522-da16f3077589/go.mod h1:IeI20psFPeg2n1jxwbkYCmkpYsXsJqB7qmoqCIlX80s= 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/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 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= 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.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= 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/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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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/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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/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 h1:Z7A73W6jsxFuFKWvB1efQmTjs0s7+x2B7IBM2ukkI6Y=
github.com/matst80/go-redis-inventory v0.0.0-20251126173508-51b30de2d86e/go.mod h1:9P52UwIlLWLZvObfO29aKTWUCA9Gm62IuPJ/qv4Xvs0= 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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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_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 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 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.68.0 h1:8rQJvQmYltsR2L7h8Zw0Iyj8WYNNmpwikoQTZXwfVeA=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/common v0.68.0/go.mod h1:4soH+U8yJSROk7OJ//hmTiWKsxapv6zRGgTt3keN8gQ=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rabbitmq/amqp091-go v1.11.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.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0=
github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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= 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/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 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 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.19.0 h1:5RgvxieNq9tS3ewrV1vnODvbHPfKUIJcYtF9Cvz+6aQ=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4= go.opentelemetry.io/contrib/bridges/otelslog v0.19.0/go.mod h1:iTBIdNwx/xmUhfgJs6+84S4dIK059811cO1eUBjKcHY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= 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.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= 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.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= 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.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= 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.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= 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/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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 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-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-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-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-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.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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-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-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-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.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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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-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-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-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.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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-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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0= 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-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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 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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+50
View File
@@ -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()
}
}
+28 -2
View File
@@ -24,6 +24,10 @@ type SimpleGrainPool[V any] struct {
ttl time.Duration ttl time.Duration
poolSize int 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 -------------------------------------------------- // Cluster coordination --------------------------------------------------
hostname string hostname string
remoteMu sync.RWMutex remoteMu sync.RWMutex
@@ -59,6 +63,7 @@ func NewSimpleGrainPool[V any](config GrainPoolConfig[V]) (*SimpleGrainPool[V],
hostname: config.Hostname, hostname: config.Hostname,
remoteOwners: make(map[uint64]Host[V]), remoteOwners: make(map[uint64]Host[V]),
remoteHosts: make(map[string]Host[V]), remoteHosts: make(map[string]Host[V]),
grainLocks: newKeyedMutex(),
} }
p.purgeTicker = time.NewTicker(time.Minute) p.purgeTicker = time.NewTicker(time.Minute)
@@ -187,7 +192,17 @@ func (p *SimpleGrainPool[V]) AddRemote(host string) (Host[V], error) {
return nil, err 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() p.remoteMu.Lock()
if existing, found := p.remoteHosts[host]; found {
p.remoteMu.Unlock()
go remote.Close()
return existing, nil
}
p.remoteHosts[host] = remote p.remoteHosts[host] = remote
p.remoteMu.Unlock() p.remoteMu.Unlock()
// connectedRemotes.Set(float64(p.RemoteCount())) // connectedRemotes.Set(float64(p.RemoteCount()))
@@ -221,7 +236,6 @@ func (p *SimpleGrainPool[V]) RemoveHost(host string) {
remote, exists := p.remoteHosts[host] remote, exists := p.remoteHosts[host]
if exists { if exists {
go remote.Close()
delete(p.remoteHosts, host) delete(p.remoteHosts, host)
} }
count := 0 count := 0
@@ -234,6 +248,7 @@ func (p *SimpleGrainPool[V]) RemoveHost(host string) {
log.Printf("Removing host %s, grains: %d", host, count) log.Printf("Removing host %s, grains: %d", host, count)
p.remoteMu.Unlock() p.remoteMu.Unlock()
// Close once, outside the lock.
if exists { if exists {
remote.Close() remote.Close()
} }
@@ -275,7 +290,9 @@ func (p *SimpleGrainPool[V]) pingLoop(remote Host[V]) {
if !remote.Ping() { if !remote.Ping() {
if !remote.IsHealthy() { if !remote.IsHealthy() {
log.Printf("Remote %s unhealthy, removing", remote.Name()) 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()) p.RemoveHost(remote.Name())
return return
} }
@@ -397,6 +414,12 @@ func (p *SimpleGrainPool[V]) getOrClaimGrain(ctx context.Context, id uint64) (Gr
// Apply applies a mutation to a grain. // Apply applies a mutation to a grain.
func (p *SimpleGrainPool[V]) Apply(ctx context.Context, id uint64, mutation ...proto.Message) (*MutationResult[V], error) { 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) grain, err := p.getOrClaimGrain(ctx, id)
if err != nil { if err != nil {
return nil, err 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. // Get returns the current state of a grain.
func (p *SimpleGrainPool[V]) Get(ctx context.Context, id uint64) (*V, error) { 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) grain, err := p.getOrClaimGrain(ctx, id)
if err != nil { if err != nil {
return nil, err return nil, err
+32 -23
View File
@@ -28,29 +28,38 @@ type ItemMeta struct {
} }
type CartItem struct { type CartItem struct {
Id uint32 `json:"id"` Id uint32 `json:"id"`
ItemId uint32 `json:"itemId,omitempty"` ItemId uint32 `json:"itemId,omitempty"`
ParentId *uint32 `json:"parentId,omitempty"` ParentId *uint32 `json:"parentId,omitempty"`
Sku string `json:"sku"` Sku string `json:"sku"`
Price Price `json:"price"` Price Price `json:"price"`
TotalPrice Price `json:"totalPrice"` TotalPrice Price `json:"totalPrice"`
SellerId string `json:"sellerId,omitempty"` SellerId string `json:"sellerId,omitempty"`
OrgPrice *Price `json:"orgPrice,omitempty"` OrgPrice *Price `json:"orgPrice,omitempty"`
Cgm string `json:"cgm,omitempty"` Cgm string `json:"cgm,omitempty"`
Tax int Tax int
Stock uint16 `json:"stock"` Stock uint16 `json:"stock"`
Quantity uint16 `json:"qty"` Quantity uint16 `json:"qty"`
Discount *Price `json:"discount,omitempty"` Discount *Price `json:"discount,omitempty"`
Disclaimer string `json:"disclaimer,omitempty"` Disclaimer string `json:"disclaimer,omitempty"`
ArticleType string `json:"type,omitempty"` ArticleType string `json:"type,omitempty"`
StoreId *string `json:"storeId,omitempty"` StoreId *string `json:"storeId,omitempty"`
Meta *ItemMeta `json:"meta,omitempty"` Meta *ItemMeta `json:"meta,omitempty"`
SaleStatus string `json:"saleStatus"` SaleStatus string `json:"saleStatus"`
Marking *Marking `json:"marking,omitempty"` Marking *Marking `json:"marking,omitempty"`
SubscriptionDetailsId string `json:"subscriptionDetailsId,omitempty"` // CustomFields holds optional user-supplied input fields for this line
OrderReference string `json:"orderReference,omitempty"` // (engraving text, configurator notes, ...), keyed by field name.
IsSubscribed bool `json:"isSubscribed,omitempty"` CustomFields map[string]string `json:"customFields,omitempty"`
ReservationEndTime *time.Time `json:"reservationEndTime,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 { type CartNotification struct {
+1
View File
@@ -79,6 +79,7 @@ func NewCartMultationRegistry(context *CartMutationContext) actor.MutationRegist
actor.NewMutation(SetUserId), actor.NewMutation(SetUserId),
actor.NewMutation(LineItemMarking), actor.NewMutation(LineItemMarking),
actor.NewMutation(RemoveLineItemMarking), actor.NewMutation(RemoveLineItemMarking),
actor.NewMutation(SetLineItemCustomFields),
actor.NewMutation(SubscriptionAdded), actor.NewMutation(SubscriptionAdded),
// actor.NewMutation(SubscriptionRemoved), // actor.NewMutation(SubscriptionRemoved),
) )
+77
View File
@@ -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
}
+94
View File
@@ -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")
}
}
+33 -3
View File
@@ -2,6 +2,7 @@ package cart
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@@ -11,6 +12,20 @@ import (
"google.golang.org/protobuf/types/known/timestamppb" "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 // mutation_add_item.go
// //
// Registers the AddItem cart mutation in the generic mutation registry. // Registers the AddItem cart mutation in the generic mutation registry.
@@ -18,10 +33,13 @@ import (
// //
// Behavior: // Behavior:
// - Validates quantity > 0 // - 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 // - Else creates a new CartItem with computed tax amounts
// - Totals recalculated automatically via WithTotals() // - 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 // NOTE: Any future field additions in messages.AddItem that affect pricing / tax
// must keep this handler in sync. // must keep this handler in sync.
var ErrPaymentInProgress = errors.New("payment in progress") 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) 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 { for _, existing := range g.Items {
if existing.Sku != m.Sku { if existing.ItemId != m.ItemId {
continue continue
} }
sameStore := (existing.StoreId == nil && m.StoreId == nil) || 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 { if existing.StoreId == nil && m.StoreId != nil {
existing.StoreId = m.StoreId 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 return nil
} }
@@ -114,6 +141,9 @@ func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) er
ArticleType: m.ArticleType, ArticleType: m.ArticleType,
StoreId: m.StoreId, StoreId: m.StoreId,
Extra: decodeExtra(m.ExtraJson),
CustomFields: m.CustomFields,
} }
if needsReservation && c.UseReservations(cartItem) { if needsReservation && c.UseReservations(cartItem) {
+40
View File
@@ -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))
}
}
+65
View File
@@ -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)
}
}
+36 -14
View File
@@ -15,15 +15,17 @@ import (
// //
// Behavior: // Behavior:
// - Removes the cart line whose local cart line Id == payload.Id // - 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 // - If no such line exists returns an error
// - Recalculates cart totals (WithTotals) // - Releases reservations for every removed line and recalculates totals
// //
// Notes: // 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 // item are NOT automatically adjusted (mirrors prior logic). If future
// semantics require pruning delivery.item_ids you can extend this handler. // semantics require pruning delivery.item_ids you can extend this handler.
// - If multiple lines somehow shared the same Id (should not happen), only // - If multiple lines somehow shared the same Id (should not happen), all
// the first match would be removed—data integrity relies on unique line Ids. // matches are removed—data integrity relies on unique line Ids.
func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) error { func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) error {
if m == nil { if m == nil {
@@ -32,26 +34,46 @@ func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) e
targetID := uint32(m.Id) targetID := uint32(m.Id)
index := -1 found := false
for i, it := range g.Items { for _, it := range g.Items {
if it.Id == targetID { if it.Id == targetID {
index = i found = true
break break
} }
} }
if index == -1 { if !found {
return fmt.Errorf("RemoveItem: item id %d not found", m.Id) return fmt.Errorf("RemoveItem: item id %d not found", m.Id)
} }
item := g.Items[index] // Collect the target and, transitively, any children pointing at a removed
if item.ReservationEndTime != nil && item.ReservationEndTime.After(time.Now()) { // line. Loops until no further descendants are found (handles nesting).
err := c.ReleaseItem(context.Background(), g.Id, item.Sku, item.StoreId) remove := map[uint32]bool{targetID: true}
if err != nil { for {
log.Printf("unable to release item reservation") 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() g.UpdateTotals()
return nil return nil
} }
+70
View File
@@ -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)
}
}
+31
View File
@@ -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)
}
+44 -4
View File
@@ -69,7 +69,15 @@ func (m *MockResponseWriter) WriteHeader(statusCode int) {
m.StatusCode = statusCode 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) target := fmt.Sprintf("%s:1337", host)
@@ -85,14 +93,18 @@ func NewRemoteHost[V any](host string) (*RemoteHost[V], error) {
transport := &http.Transport{ transport := &http.Transport{
MaxIdleConns: 100, MaxIdleConns: 100,
MaxIdleConnsPerHost: 100, MaxIdleConnsPerHost: 100,
DisableKeepAlives: false, // Bound concurrently-open connections to this peer so a load spike
IdleConnTimeout: 120 * time.Second, // 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,
} }
client := &http.Client{Transport: transport, Timeout: 10 * time.Second} client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
return &RemoteHost[V]{ return &RemoteHost[V]{
host: host, host: host,
httpBase: fmt.Sprintf("http://%s:8080", host), httpBase: fmt.Sprintf("http://%s:%s", host, port),
conn: conn, conn: conn,
transport: transport, transport: transport,
client: client, client: client,
@@ -101,6 +113,25 @@ func NewRemoteHost[V any](host string) (*RemoteHost[V], error) {
}, nil }, 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 { func (h *RemoteHost[V]) Name() string {
return h.host 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) req.Header.Set("X-Forwarded-Host", r.Host)
for k, v := range r.Header { 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 { for _, vv := range v {
req.Header.Add(k, vv) 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() defer res.Body.Close()
span.SetAttributes(attribute.Int("status_code", res.StatusCode)) span.SetAttributes(attribute.Int("status_code", res.StatusCode))
for k, v := range res.Header { for k, v := range res.Header {
if isHopByHopHeader(k) {
continue
}
for _, vv := range v { for _, vv := range v {
w.Header().Add(k, vv) w.Header().Add(k, vv)
} }
+15
View File
@@ -34,6 +34,13 @@ message AddItem {
optional uint32 parentId = 23; optional uint32 parentId = 23;
string cgm = 25; string cgm = 25;
optional google.protobuf.Timestamp reservationEndTime = 26; 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; } message RemoveItem { uint32 Id = 1; }
@@ -57,6 +64,13 @@ message RemoveLineItemMarking {
uint32 id = 1; 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 { message SubscriptionAdded {
uint32 itemId = 1; uint32 itemId = 1;
string detailsId = 3; string detailsId = 3;
@@ -89,6 +103,7 @@ message Mutation {
LineItemMarking line_item_marking = 6; LineItemMarking line_item_marking = 6;
RemoveLineItemMarking remove_line_item_marking = 7; RemoveLineItemMarking remove_line_item_marking = 7;
SubscriptionAdded subscription_added = 8; SubscriptionAdded subscription_added = 8;
SetLineItemCustomFields set_line_item_custom_fields = 9;
AddVoucher add_voucher = 20; AddVoucher add_voucher = 20;
RemoveVoucher remove_voucher = 21; RemoveVoucher remove_voucher = 21;
UpsertSubscriptionDetails upsert_subscription_details = 22; UpsertSubscriptionDetails upsert_subscription_details = 22;
+164 -53
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.10 // protoc-gen-go v1.36.11
// protoc v6.33.1 // protoc v7.35.0
// source: cart.proto // source: cart.proto
package cart_messages package cart_messages
@@ -87,8 +87,15 @@ type AddItem struct {
ParentId *uint32 `protobuf:"varint,23,opt,name=parentId,proto3,oneof" json:"parentId,omitempty"` 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"` 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"` ReservationEndTime *timestamppb.Timestamp `protobuf:"bytes,26,opt,name=reservationEndTime,proto3,oneof" json:"reservationEndTime,omitempty"`
unknownFields protoimpl.UnknownFields // extra_json carries arbitrary product data (the flat product document minus
sizeCache protoimpl.SizeCache // 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
} }
func (x *AddItem) Reset() { func (x *AddItem) Reset() {
@@ -303,6 +310,20 @@ func (x *AddItem) GetReservationEndTime() *timestamppb.Timestamp {
return nil 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 { type RemoveItem struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id uint32 `protobuf:"varint,1,opt,name=Id,proto3" json:"Id,omitempty"` Id uint32 `protobuf:"varint,1,opt,name=Id,proto3" json:"Id,omitempty"`
@@ -547,6 +568,60 @@ func (x *RemoveLineItemMarking) GetId() uint32 {
return 0 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 { type SubscriptionAdded struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
ItemId uint32 `protobuf:"varint,1,opt,name=itemId,proto3" json:"itemId,omitempty"` ItemId uint32 `protobuf:"varint,1,opt,name=itemId,proto3" json:"itemId,omitempty"`
@@ -558,7 +633,7 @@ type SubscriptionAdded struct {
func (x *SubscriptionAdded) Reset() { func (x *SubscriptionAdded) Reset() {
*x = SubscriptionAdded{} *x = SubscriptionAdded{}
mi := &file_cart_proto_msgTypes[7] mi := &file_cart_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -570,7 +645,7 @@ func (x *SubscriptionAdded) String() string {
func (*SubscriptionAdded) ProtoMessage() {} func (*SubscriptionAdded) ProtoMessage() {}
func (x *SubscriptionAdded) ProtoReflect() protoreflect.Message { func (x *SubscriptionAdded) ProtoReflect() protoreflect.Message {
mi := &file_cart_proto_msgTypes[7] mi := &file_cart_proto_msgTypes[8]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -583,7 +658,7 @@ func (x *SubscriptionAdded) ProtoReflect() protoreflect.Message {
// Deprecated: Use SubscriptionAdded.ProtoReflect.Descriptor instead. // Deprecated: Use SubscriptionAdded.ProtoReflect.Descriptor instead.
func (*SubscriptionAdded) Descriptor() ([]byte, []int) { func (*SubscriptionAdded) Descriptor() ([]byte, []int) {
return file_cart_proto_rawDescGZIP(), []int{7} return file_cart_proto_rawDescGZIP(), []int{8}
} }
func (x *SubscriptionAdded) GetItemId() uint32 { func (x *SubscriptionAdded) GetItemId() uint32 {
@@ -619,7 +694,7 @@ type AddVoucher struct {
func (x *AddVoucher) Reset() { func (x *AddVoucher) Reset() {
*x = AddVoucher{} *x = AddVoucher{}
mi := &file_cart_proto_msgTypes[8] mi := &file_cart_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -631,7 +706,7 @@ func (x *AddVoucher) String() string {
func (*AddVoucher) ProtoMessage() {} func (*AddVoucher) ProtoMessage() {}
func (x *AddVoucher) ProtoReflect() protoreflect.Message { func (x *AddVoucher) ProtoReflect() protoreflect.Message {
mi := &file_cart_proto_msgTypes[8] mi := &file_cart_proto_msgTypes[9]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -644,7 +719,7 @@ func (x *AddVoucher) ProtoReflect() protoreflect.Message {
// Deprecated: Use AddVoucher.ProtoReflect.Descriptor instead. // Deprecated: Use AddVoucher.ProtoReflect.Descriptor instead.
func (*AddVoucher) Descriptor() ([]byte, []int) { func (*AddVoucher) Descriptor() ([]byte, []int) {
return file_cart_proto_rawDescGZIP(), []int{8} return file_cart_proto_rawDescGZIP(), []int{9}
} }
func (x *AddVoucher) GetCode() string { func (x *AddVoucher) GetCode() string {
@@ -684,7 +759,7 @@ type RemoveVoucher struct {
func (x *RemoveVoucher) Reset() { func (x *RemoveVoucher) Reset() {
*x = RemoveVoucher{} *x = RemoveVoucher{}
mi := &file_cart_proto_msgTypes[9] mi := &file_cart_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -696,7 +771,7 @@ func (x *RemoveVoucher) String() string {
func (*RemoveVoucher) ProtoMessage() {} func (*RemoveVoucher) ProtoMessage() {}
func (x *RemoveVoucher) ProtoReflect() protoreflect.Message { func (x *RemoveVoucher) ProtoReflect() protoreflect.Message {
mi := &file_cart_proto_msgTypes[9] mi := &file_cart_proto_msgTypes[10]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -709,7 +784,7 @@ func (x *RemoveVoucher) ProtoReflect() protoreflect.Message {
// Deprecated: Use RemoveVoucher.ProtoReflect.Descriptor instead. // Deprecated: Use RemoveVoucher.ProtoReflect.Descriptor instead.
func (*RemoveVoucher) Descriptor() ([]byte, []int) { func (*RemoveVoucher) Descriptor() ([]byte, []int) {
return file_cart_proto_rawDescGZIP(), []int{9} return file_cart_proto_rawDescGZIP(), []int{10}
} }
func (x *RemoveVoucher) GetId() uint32 { func (x *RemoveVoucher) GetId() uint32 {
@@ -731,7 +806,7 @@ type UpsertSubscriptionDetails struct {
func (x *UpsertSubscriptionDetails) Reset() { func (x *UpsertSubscriptionDetails) Reset() {
*x = UpsertSubscriptionDetails{} *x = UpsertSubscriptionDetails{}
mi := &file_cart_proto_msgTypes[10] mi := &file_cart_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -743,7 +818,7 @@ func (x *UpsertSubscriptionDetails) String() string {
func (*UpsertSubscriptionDetails) ProtoMessage() {} func (*UpsertSubscriptionDetails) ProtoMessage() {}
func (x *UpsertSubscriptionDetails) ProtoReflect() protoreflect.Message { func (x *UpsertSubscriptionDetails) ProtoReflect() protoreflect.Message {
mi := &file_cart_proto_msgTypes[10] mi := &file_cart_proto_msgTypes[11]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -756,7 +831,7 @@ func (x *UpsertSubscriptionDetails) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpsertSubscriptionDetails.ProtoReflect.Descriptor instead. // Deprecated: Use UpsertSubscriptionDetails.ProtoReflect.Descriptor instead.
func (*UpsertSubscriptionDetails) Descriptor() ([]byte, []int) { func (*UpsertSubscriptionDetails) Descriptor() ([]byte, []int) {
return file_cart_proto_rawDescGZIP(), []int{10} return file_cart_proto_rawDescGZIP(), []int{11}
} }
func (x *UpsertSubscriptionDetails) GetId() string { func (x *UpsertSubscriptionDetails) GetId() string {
@@ -799,6 +874,7 @@ type Mutation struct {
// *Mutation_LineItemMarking // *Mutation_LineItemMarking
// *Mutation_RemoveLineItemMarking // *Mutation_RemoveLineItemMarking
// *Mutation_SubscriptionAdded // *Mutation_SubscriptionAdded
// *Mutation_SetLineItemCustomFields
// *Mutation_AddVoucher // *Mutation_AddVoucher
// *Mutation_RemoveVoucher // *Mutation_RemoveVoucher
// *Mutation_UpsertSubscriptionDetails // *Mutation_UpsertSubscriptionDetails
@@ -809,7 +885,7 @@ type Mutation struct {
func (x *Mutation) Reset() { func (x *Mutation) Reset() {
*x = Mutation{} *x = Mutation{}
mi := &file_cart_proto_msgTypes[11] mi := &file_cart_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -821,7 +897,7 @@ func (x *Mutation) String() string {
func (*Mutation) ProtoMessage() {} func (*Mutation) ProtoMessage() {}
func (x *Mutation) ProtoReflect() protoreflect.Message { func (x *Mutation) ProtoReflect() protoreflect.Message {
mi := &file_cart_proto_msgTypes[11] mi := &file_cart_proto_msgTypes[12]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -834,7 +910,7 @@ func (x *Mutation) ProtoReflect() protoreflect.Message {
// Deprecated: Use Mutation.ProtoReflect.Descriptor instead. // Deprecated: Use Mutation.ProtoReflect.Descriptor instead.
func (*Mutation) Descriptor() ([]byte, []int) { 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 { func (x *Mutation) GetType() isMutation_Type {
@@ -916,6 +992,15 @@ func (x *Mutation) GetSubscriptionAdded() *SubscriptionAdded {
return nil 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 { func (x *Mutation) GetAddVoucher() *AddVoucher {
if x != nil { if x != nil {
if x, ok := x.Type.(*Mutation_AddVoucher); ok { 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"` 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 { type Mutation_AddVoucher struct {
AddVoucher *AddVoucher `protobuf:"bytes,20,opt,name=add_voucher,json=addVoucher,proto3,oneof"` 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_SubscriptionAdded) isMutation_Type() {}
func (*Mutation_SetLineItemCustomFields) isMutation_Type() {}
func (*Mutation_AddVoucher) isMutation_Type() {} func (*Mutation_AddVoucher) isMutation_Type() {}
func (*Mutation_RemoveVoucher) isMutation_Type() {} func (*Mutation_RemoveVoucher) isMutation_Type() {}
@@ -1019,7 +1110,7 @@ const file_cart_proto_rawDesc = "" +
"\n" + "\n" +
"\n" + "\n" +
"cart.proto\x12\rcart_messages\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x12\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" + "\aAddItem\x12\x17\n" +
"\aitem_id\x18\x01 \x01(\rR\x06itemId\x12\x1a\n" + "\aitem_id\x18\x01 \x01(\rR\x06itemId\x12\x1a\n" +
"\bquantity\x18\x02 \x01(\x05R\bquantity\x12\x14\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" + "\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" + "\bparentId\x18\x17 \x01(\rH\x02R\bparentId\x88\x01\x01\x12\x10\n" +
"\x03cgm\x18\x19 \x01(\tR\x03cgm\x12O\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" + "\a_outletB\n" +
"\n" + "\n" +
"\b_storeIdB\v\n" + "\b_storeIdB\v\n" +
@@ -1072,7 +1169,13 @@ const file_cart_proto_rawDesc = "" +
"\x04type\x18\x02 \x01(\rR\x04type\x12\x18\n" + "\x04type\x18\x02 \x01(\rR\x04type\x12\x18\n" +
"\amarking\x18\x03 \x01(\tR\amarking\"'\n" + "\amarking\x18\x03 \x01(\tR\amarking\"'\n" +
"\x15RemoveLineItemMarking\x12\x0e\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" + "\x11SubscriptionAdded\x12\x16\n" +
"\x06itemId\x18\x01 \x01(\rR\x06itemId\x12\x1c\n" + "\x06itemId\x18\x01 \x01(\rR\x06itemId\x12\x1c\n" +
"\tdetailsId\x18\x03 \x01(\tR\tdetailsId\x12&\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" + "\fofferingCode\x18\x02 \x01(\tR\fofferingCode\x12 \n" +
"\vsigningType\x18\x03 \x01(\tR\vsigningType\x12(\n" + "\vsigningType\x18\x03 \x01(\tR\vsigningType\x12(\n" +
"\x04data\x18\x04 \x01(\v2\x14.google.protobuf.AnyR\x04dataB\x05\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" + "\bMutation\x12@\n" +
"\n" + "\n" +
"clear_cart\x18\x01 \x01(\v2\x1f.cart_messages.ClearCartRequestH\x00R\tclearCart\x123\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" + "\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" + "\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" + "\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" + "\vadd_voucher\x18\x14 \x01(\v2\x19.cart_messages.AddVoucherH\x00R\n" +
"addVoucher\x12E\n" + "addVoucher\x12E\n" +
"\x0eremove_voucher\x18\x15 \x01(\v2\x1c.cart_messages.RemoveVoucherH\x00R\rremoveVoucher\x12j\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 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{ var file_cart_proto_goTypes = []any{
(*ClearCartRequest)(nil), // 0: cart_messages.ClearCartRequest (*ClearCartRequest)(nil), // 0: cart_messages.ClearCartRequest
(*AddItem)(nil), // 1: cart_messages.AddItem (*AddItem)(nil), // 1: cart_messages.AddItem
@@ -1129,33 +1233,39 @@ var file_cart_proto_goTypes = []any{
(*SetUserId)(nil), // 4: cart_messages.SetUserId (*SetUserId)(nil), // 4: cart_messages.SetUserId
(*LineItemMarking)(nil), // 5: cart_messages.LineItemMarking (*LineItemMarking)(nil), // 5: cart_messages.LineItemMarking
(*RemoveLineItemMarking)(nil), // 6: cart_messages.RemoveLineItemMarking (*RemoveLineItemMarking)(nil), // 6: cart_messages.RemoveLineItemMarking
(*SubscriptionAdded)(nil), // 7: cart_messages.SubscriptionAdded (*SetLineItemCustomFields)(nil), // 7: cart_messages.SetLineItemCustomFields
(*AddVoucher)(nil), // 8: cart_messages.AddVoucher (*SubscriptionAdded)(nil), // 8: cart_messages.SubscriptionAdded
(*RemoveVoucher)(nil), // 9: cart_messages.RemoveVoucher (*AddVoucher)(nil), // 9: cart_messages.AddVoucher
(*UpsertSubscriptionDetails)(nil), // 10: cart_messages.UpsertSubscriptionDetails (*RemoveVoucher)(nil), // 10: cart_messages.RemoveVoucher
(*Mutation)(nil), // 11: cart_messages.Mutation (*UpsertSubscriptionDetails)(nil), // 11: cart_messages.UpsertSubscriptionDetails
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp (*Mutation)(nil), // 12: cart_messages.Mutation
(*anypb.Any)(nil), // 13: google.protobuf.Any 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{ var file_cart_proto_depIdxs = []int32{
12, // 0: cart_messages.AddItem.reservationEndTime:type_name -> google.protobuf.Timestamp 15, // 0: cart_messages.AddItem.reservationEndTime:type_name -> google.protobuf.Timestamp
13, // 1: cart_messages.UpsertSubscriptionDetails.data:type_name -> google.protobuf.Any 13, // 1: cart_messages.AddItem.custom_fields:type_name -> cart_messages.AddItem.CustomFieldsEntry
0, // 2: cart_messages.Mutation.clear_cart:type_name -> cart_messages.ClearCartRequest 14, // 2: cart_messages.SetLineItemCustomFields.custom_fields:type_name -> cart_messages.SetLineItemCustomFields.CustomFieldsEntry
1, // 3: cart_messages.Mutation.add_item:type_name -> cart_messages.AddItem 16, // 3: cart_messages.UpsertSubscriptionDetails.data:type_name -> google.protobuf.Any
2, // 4: cart_messages.Mutation.remove_item:type_name -> cart_messages.RemoveItem 0, // 4: cart_messages.Mutation.clear_cart:type_name -> cart_messages.ClearCartRequest
3, // 5: cart_messages.Mutation.change_quantity:type_name -> cart_messages.ChangeQuantity 1, // 5: cart_messages.Mutation.add_item:type_name -> cart_messages.AddItem
4, // 6: cart_messages.Mutation.set_user_id:type_name -> cart_messages.SetUserId 2, // 6: cart_messages.Mutation.remove_item:type_name -> cart_messages.RemoveItem
5, // 7: cart_messages.Mutation.line_item_marking:type_name -> cart_messages.LineItemMarking 3, // 7: cart_messages.Mutation.change_quantity:type_name -> cart_messages.ChangeQuantity
6, // 8: cart_messages.Mutation.remove_line_item_marking:type_name -> cart_messages.RemoveLineItemMarking 4, // 8: cart_messages.Mutation.set_user_id:type_name -> cart_messages.SetUserId
7, // 9: cart_messages.Mutation.subscription_added:type_name -> cart_messages.SubscriptionAdded 5, // 9: cart_messages.Mutation.line_item_marking:type_name -> cart_messages.LineItemMarking
8, // 10: cart_messages.Mutation.add_voucher:type_name -> cart_messages.AddVoucher 6, // 10: cart_messages.Mutation.remove_line_item_marking:type_name -> cart_messages.RemoveLineItemMarking
9, // 11: cart_messages.Mutation.remove_voucher:type_name -> cart_messages.RemoveVoucher 8, // 11: cart_messages.Mutation.subscription_added:type_name -> cart_messages.SubscriptionAdded
10, // 12: cart_messages.Mutation.upsert_subscription_details:type_name -> cart_messages.UpsertSubscriptionDetails 7, // 12: cart_messages.Mutation.set_line_item_custom_fields:type_name -> cart_messages.SetLineItemCustomFields
13, // [13:13] is the sub-list for method output_type 9, // 13: cart_messages.Mutation.add_voucher:type_name -> cart_messages.AddVoucher
13, // [13:13] is the sub-list for method input_type 10, // 14: cart_messages.Mutation.remove_voucher:type_name -> cart_messages.RemoveVoucher
13, // [13:13] is the sub-list for extension type_name 11, // 15: cart_messages.Mutation.upsert_subscription_details:type_name -> cart_messages.UpsertSubscriptionDetails
13, // [13:13] is the sub-list for extension extendee 16, // [16:16] is the sub-list for method output_type
0, // [0:13] is the sub-list for field type_name 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() } func init() { file_cart_proto_init() }
@@ -1164,8 +1274,8 @@ func file_cart_proto_init() {
return return
} }
file_cart_proto_msgTypes[1].OneofWrappers = []any{} 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_ClearCart)(nil),
(*Mutation_AddItem)(nil), (*Mutation_AddItem)(nil),
(*Mutation_RemoveItem)(nil), (*Mutation_RemoveItem)(nil),
@@ -1174,6 +1284,7 @@ func file_cart_proto_init() {
(*Mutation_LineItemMarking)(nil), (*Mutation_LineItemMarking)(nil),
(*Mutation_RemoveLineItemMarking)(nil), (*Mutation_RemoveLineItemMarking)(nil),
(*Mutation_SubscriptionAdded)(nil), (*Mutation_SubscriptionAdded)(nil),
(*Mutation_SetLineItemCustomFields)(nil),
(*Mutation_AddVoucher)(nil), (*Mutation_AddVoucher)(nil),
(*Mutation_RemoveVoucher)(nil), (*Mutation_RemoveVoucher)(nil),
(*Mutation_UpsertSubscriptionDetails)(nil), (*Mutation_UpsertSubscriptionDetails)(nil),
@@ -1184,7 +1295,7 @@ func file_cart_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_cart_proto_rawDesc), len(file_cart_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_cart_proto_rawDesc), len(file_cart_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 12, NumMessages: 15,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.10 // protoc-gen-go v1.36.11
// protoc v6.33.1 // protoc v7.35.0
// source: checkout.proto // source: checkout.proto
package checkout_messages package checkout_messages
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.10 // protoc-gen-go v1.36.11
// protoc v6.33.1 // protoc v7.35.0
// source: control_plane.proto // source: control_plane.proto
package control_plane_messages package control_plane_messages
+11 -11
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.6.2
// - protoc v6.33.1 // - protoc v7.35.0
// source: control_plane.proto // source: control_plane.proto
package control_plane_messages package control_plane_messages
@@ -170,28 +170,28 @@ type ControlPlaneServer interface {
type UnimplementedControlPlaneServer struct{} type UnimplementedControlPlaneServer struct{}
func (UnimplementedControlPlaneServer) Ping(context.Context, *Empty) (*PingReply, error) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) mustEmbedUnimplementedControlPlaneServer() {}
func (UnimplementedControlPlaneServer) testEmbeddedByValue() {} func (UnimplementedControlPlaneServer) testEmbeddedByValue() {}
@@ -204,7 +204,7 @@ type UnsafeControlPlaneServer interface {
} }
func RegisterControlPlaneServer(s grpc.ServiceRegistrar, srv ControlPlaneServer) { 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 // embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization // unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O. // time to prevent it from happening at runtime later due to I/O.