fixes
This commit is contained in:
+32
-23
@@ -28,29 +28,38 @@ type ItemMeta struct {
|
||||
}
|
||||
|
||||
type CartItem struct {
|
||||
Id uint32 `json:"id"`
|
||||
ItemId uint32 `json:"itemId,omitempty"`
|
||||
ParentId *uint32 `json:"parentId,omitempty"`
|
||||
Sku string `json:"sku"`
|
||||
Price Price `json:"price"`
|
||||
TotalPrice Price `json:"totalPrice"`
|
||||
SellerId string `json:"sellerId,omitempty"`
|
||||
OrgPrice *Price `json:"orgPrice,omitempty"`
|
||||
Cgm string `json:"cgm,omitempty"`
|
||||
Tax int
|
||||
Stock uint16 `json:"stock"`
|
||||
Quantity uint16 `json:"qty"`
|
||||
Discount *Price `json:"discount,omitempty"`
|
||||
Disclaimer string `json:"disclaimer,omitempty"`
|
||||
ArticleType string `json:"type,omitempty"`
|
||||
StoreId *string `json:"storeId,omitempty"`
|
||||
Meta *ItemMeta `json:"meta,omitempty"`
|
||||
SaleStatus string `json:"saleStatus"`
|
||||
Marking *Marking `json:"marking,omitempty"`
|
||||
SubscriptionDetailsId string `json:"subscriptionDetailsId,omitempty"`
|
||||
OrderReference string `json:"orderReference,omitempty"`
|
||||
IsSubscribed bool `json:"isSubscribed,omitempty"`
|
||||
ReservationEndTime *time.Time `json:"reservationEndTime,omitempty"`
|
||||
Id uint32 `json:"id"`
|
||||
ItemId uint32 `json:"itemId,omitempty"`
|
||||
ParentId *uint32 `json:"parentId,omitempty"`
|
||||
Sku string `json:"sku"`
|
||||
Price Price `json:"price"`
|
||||
TotalPrice Price `json:"totalPrice"`
|
||||
SellerId string `json:"sellerId,omitempty"`
|
||||
OrgPrice *Price `json:"orgPrice,omitempty"`
|
||||
Cgm string `json:"cgm,omitempty"`
|
||||
Tax int
|
||||
Stock uint16 `json:"stock"`
|
||||
Quantity uint16 `json:"qty"`
|
||||
Discount *Price `json:"discount,omitempty"`
|
||||
Disclaimer string `json:"disclaimer,omitempty"`
|
||||
ArticleType string `json:"type,omitempty"`
|
||||
StoreId *string `json:"storeId,omitempty"`
|
||||
Meta *ItemMeta `json:"meta,omitempty"`
|
||||
SaleStatus string `json:"saleStatus"`
|
||||
Marking *Marking `json:"marking,omitempty"`
|
||||
// CustomFields holds optional user-supplied input fields for this line
|
||||
// (engraving text, configurator notes, ...), keyed by field name.
|
||||
CustomFields map[string]string `json:"customFields,omitempty"`
|
||||
SubscriptionDetailsId string `json:"subscriptionDetailsId,omitempty"`
|
||||
OrderReference string `json:"orderReference,omitempty"`
|
||||
IsSubscribed bool `json:"isSubscribed,omitempty"`
|
||||
ReservationEndTime *time.Time `json:"reservationEndTime,omitempty"`
|
||||
|
||||
// Extra holds arbitrary dynamic product data. Its keys are flattened onto
|
||||
// the item object in JSON (see cart_item_json.go), so they are returned
|
||||
// over the API alongside the typed fields. Typed fields win on key
|
||||
// collisions.
|
||||
Extra map[string]json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
type CartNotification struct {
|
||||
|
||||
@@ -79,6 +79,7 @@ func NewCartMultationRegistry(context *CartMutationContext) actor.MutationRegist
|
||||
actor.NewMutation(SetUserId),
|
||||
actor.NewMutation(LineItemMarking),
|
||||
actor.NewMutation(RemoveLineItemMarking),
|
||||
actor.NewMutation(SetLineItemCustomFields),
|
||||
actor.NewMutation(SubscriptionAdded),
|
||||
// actor.NewMutation(SubscriptionRemoved),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// cartItemAlias mirrors CartItem but has no custom (Un)MarshalJSON, so it is
|
||||
// used to (de)serialize the typed fields without recursing.
|
||||
type cartItemAlias CartItem
|
||||
|
||||
// knownItemKeys is the set of top-level JSON object keys owned by typed
|
||||
// CartItem fields. Any other key in an item object is dynamic and kept in
|
||||
// CartItem.Extra.
|
||||
var knownItemKeys = buildKnownItemKeys()
|
||||
|
||||
func buildKnownItemKeys() map[string]struct{} {
|
||||
keys := make(map[string]struct{})
|
||||
t := reflect.TypeOf(CartItem{})
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",")
|
||||
if name == "" {
|
||||
name = t.Field(i).Name // untagged fields serialize under their Go name
|
||||
}
|
||||
if name == "-" {
|
||||
continue // e.g. Extra itself
|
||||
}
|
||||
keys[name] = struct{}{}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// MarshalJSON emits the typed fields and flattens Extra onto the same object.
|
||||
// Typed fields take precedence on key collisions.
|
||||
func (c CartItem) MarshalJSON() ([]byte, error) {
|
||||
core, err := json.Marshal(cartItemAlias(c))
|
||||
if err != nil || len(c.Extra) == 0 {
|
||||
return core, err
|
||||
}
|
||||
merged := make(map[string]json.RawMessage, len(c.Extra)+8)
|
||||
if err := json.Unmarshal(core, &merged); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range c.Extra {
|
||||
if _, taken := merged[k]; taken {
|
||||
continue
|
||||
}
|
||||
merged[k] = v
|
||||
}
|
||||
return json.Marshal(merged)
|
||||
}
|
||||
|
||||
// UnmarshalJSON reads the typed fields and collects every other key into Extra.
|
||||
func (c *CartItem) UnmarshalJSON(data []byte) error {
|
||||
var alias cartItemAlias
|
||||
if err := json.Unmarshal(data, &alias); err != nil {
|
||||
return err
|
||||
}
|
||||
*c = CartItem(alias)
|
||||
|
||||
all := make(map[string]json.RawMessage)
|
||||
if err := json.Unmarshal(data, &all); err != nil {
|
||||
return err
|
||||
}
|
||||
for k := range all {
|
||||
if _, known := knownItemKeys[k]; known {
|
||||
delete(all, k)
|
||||
}
|
||||
}
|
||||
if len(all) > 0 {
|
||||
c.Extra = all
|
||||
} else {
|
||||
c.Extra = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCartItem_FlattensDynamicKeys(t *testing.T) {
|
||||
item := CartItem{
|
||||
Id: 7,
|
||||
Sku: "YD10050921H11406",
|
||||
Quantity: 2,
|
||||
Extra: map[string]json.RawMessage{
|
||||
"glas": json.RawMessage(`"V"`),
|
||||
"hangning": json.RawMessage(`"H"`),
|
||||
"deliveryWeek": json.RawMessage(`10`),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&item)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
var flat map[string]json.RawMessage
|
||||
if err := json.Unmarshal(b, &flat); err != nil {
|
||||
t.Fatalf("unmarshal to map: %v", err)
|
||||
}
|
||||
|
||||
// Typed field present at top level.
|
||||
if _, ok := flat["sku"]; !ok {
|
||||
t.Error("expected typed key \"sku\" at top level")
|
||||
}
|
||||
// Dynamic keys flattened to top level (not nested under \"extra\").
|
||||
for _, k := range []string{"glas", "hangning", "deliveryWeek"} {
|
||||
if _, ok := flat[k]; !ok {
|
||||
t.Errorf("expected dynamic key %q flattened to top level", k)
|
||||
}
|
||||
}
|
||||
if string(flat["glas"]) != `"V"` {
|
||||
t.Errorf("glas = %s, want \"V\"", flat["glas"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCartItem_RoundTrip(t *testing.T) {
|
||||
original := CartItem{
|
||||
Id: 3,
|
||||
Sku: "ABC",
|
||||
Quantity: 1,
|
||||
Extra: map[string]json.RawMessage{
|
||||
"color": json.RawMessage(`"red"`),
|
||||
"weight": json.RawMessage(`12.5`),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&original)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
var got CartItem
|
||||
if err := json.Unmarshal(b, &got); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if got.Sku != original.Sku || got.Id != original.Id || got.Quantity != original.Quantity {
|
||||
t.Errorf("typed fields not preserved: got %+v", got)
|
||||
}
|
||||
if len(got.Extra) != 2 {
|
||||
t.Fatalf("Extra len = %d, want 2 (%v)", len(got.Extra), got.Extra)
|
||||
}
|
||||
if string(got.Extra["color"]) != `"red"` {
|
||||
t.Errorf("color = %s, want \"red\"", got.Extra["color"])
|
||||
}
|
||||
// A typed key must never leak into Extra.
|
||||
if _, leaked := got.Extra["sku"]; leaked {
|
||||
t.Error("typed key \"sku\" leaked into Extra")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCartItem_NoExtraIsCompact(t *testing.T) {
|
||||
item := CartItem{Id: 1, Sku: "X", Quantity: 1}
|
||||
b, err := json.Marshal(&item)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
var flat map[string]json.RawMessage
|
||||
if err := json.Unmarshal(b, &flat); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if _, ok := flat["sku"]; !ok {
|
||||
t.Error("expected \"sku\" in output")
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -11,6 +12,20 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// decodeExtra unpacks the dynamic product data carried as raw JSON. Invalid
|
||||
// payloads are logged and dropped rather than failing the mutation.
|
||||
func decodeExtra(b []byte) map[string]json.RawMessage {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
m := map[string]json.RawMessage{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
log.Printf("AddItem: invalid extra_json: %v", err)
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// mutation_add_item.go
|
||||
//
|
||||
// Registers the AddItem cart mutation in the generic mutation registry.
|
||||
@@ -18,10 +33,13 @@ import (
|
||||
//
|
||||
// Behavior:
|
||||
// - Validates quantity > 0
|
||||
// - If an item with same SKU exists -> increases quantity
|
||||
// - If an item with the same item id (ItemId) exists -> increases quantity
|
||||
// - Else creates a new CartItem with computed tax amounts
|
||||
// - Totals recalculated automatically via WithTotals()
|
||||
//
|
||||
// Item identity is the catalog item id (ItemId), not the SKU: the product
|
||||
// service is looked up by id and the returned SKU is reference-only.
|
||||
//
|
||||
// NOTE: Any future field additions in messages.AddItem that affect pricing / tax
|
||||
// must keep this handler in sync.
|
||||
var ErrPaymentInProgress = errors.New("payment in progress")
|
||||
@@ -35,9 +53,10 @@ func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) er
|
||||
return fmt.Errorf("AddItem: invalid quantity %d", m.Quantity)
|
||||
}
|
||||
|
||||
// Merge with any existing item having same SKU and matching StoreId (including both nil).
|
||||
// Merge with any existing item having the same item id and matching StoreId
|
||||
// (including both nil). Identity is the id; SKU is reference-only.
|
||||
for _, existing := range g.Items {
|
||||
if existing.Sku != m.Sku {
|
||||
if existing.ItemId != m.ItemId {
|
||||
continue
|
||||
}
|
||||
sameStore := (existing.StoreId == nil && m.StoreId == nil) ||
|
||||
@@ -61,6 +80,14 @@ func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) er
|
||||
if existing.StoreId == nil && m.StoreId != nil {
|
||||
existing.StoreId = m.StoreId
|
||||
}
|
||||
// Refresh dynamic product data with the latest payload.
|
||||
if extra := decodeExtra(m.ExtraJson); extra != nil {
|
||||
existing.Extra = extra
|
||||
}
|
||||
// Replace custom fields when provided on the re-add.
|
||||
if len(m.CustomFields) > 0 {
|
||||
existing.CustomFields = m.CustomFields
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -114,6 +141,9 @@ func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) er
|
||||
ArticleType: m.ArticleType,
|
||||
|
||||
StoreId: m.StoreId,
|
||||
|
||||
Extra: decodeExtra(m.ExtraJson),
|
||||
CustomFields: m.CustomFields,
|
||||
}
|
||||
|
||||
if needsReservation && c.UseReservations(cartItem) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
// Item identity is the catalog item id, not the (reference-only) SKU: two adds
|
||||
// with the same ItemId but different SKU strings must merge into one line.
|
||||
func TestAddItem_MergesByItemIdNotSku(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 144047, Sku: "A0162590", Quantity: 1, Price: 1000}); err != nil {
|
||||
t.Fatalf("first add: %v", err)
|
||||
}
|
||||
// Same id, different (reference) sku string -> should merge, not create a line.
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 144047, Sku: "STALE-REF", Quantity: 2, Price: 1000}); err != nil {
|
||||
t.Fatalf("second add: %v", err)
|
||||
}
|
||||
|
||||
if len(g.Items) != 1 {
|
||||
t.Fatalf("items = %d, want 1 (merged by id)", len(g.Items))
|
||||
}
|
||||
if g.Items[0].Quantity != 3 {
|
||||
t.Errorf("quantity = %d, want 3", g.Items[0].Quantity)
|
||||
}
|
||||
|
||||
// A different id is a distinct line.
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 170852, Sku: "A0190103", Quantity: 1, Price: 500}); err != nil {
|
||||
t.Fatalf("third add: %v", err)
|
||||
}
|
||||
if len(g.Items) != 2 {
|
||||
t.Fatalf("items = %d, want 2", len(g.Items))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
func TestAddItem_StoresCustomFields(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{
|
||||
ItemId: 100, Sku: "P", Quantity: 1, Price: 1000,
|
||||
CustomFields: map[string]string{"engraving": "Happy Birthday", "color": "blue"},
|
||||
})
|
||||
|
||||
if len(g.Items) != 1 {
|
||||
t.Fatalf("items = %d, want 1", len(g.Items))
|
||||
}
|
||||
cf := g.Items[0].CustomFields
|
||||
if cf["engraving"] != "Happy Birthday" || cf["color"] != "blue" {
|
||||
t.Fatalf("custom fields = %v, want engraving+color", cf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLineItemCustomFields_Merges(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{
|
||||
ItemId: 100, Sku: "P", Quantity: 1, Price: 1000,
|
||||
CustomFields: map[string]string{"engraving": "v1"},
|
||||
})
|
||||
line := g.Items[0].Id
|
||||
|
||||
// Upsert: overwrite "engraving", add "note", leave others alone.
|
||||
mustApply(t, reg, g, &cart_messages.SetLineItemCustomFields{
|
||||
Id: line,
|
||||
CustomFields: map[string]string{"engraving": "v2", "note": "gift wrap"},
|
||||
})
|
||||
|
||||
cf := g.Items[0].CustomFields
|
||||
if cf["engraving"] != "v2" {
|
||||
t.Errorf("engraving = %q, want v2", cf["engraving"])
|
||||
}
|
||||
if cf["note"] != "gift wrap" {
|
||||
t.Errorf("note = %q, want 'gift wrap'", cf["note"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLineItemCustomFields_UnknownItem(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
// The handler error is reported per-mutation in the ApplyResult (the
|
||||
// registry only returns a top-level error for unregistered mutations).
|
||||
results, _ := reg.Apply(context.Background(), g, &cart_messages.SetLineItemCustomFields{
|
||||
Id: 999, CustomFields: map[string]string{"x": "y"},
|
||||
})
|
||||
if len(results) != 1 || results[0].Error == nil {
|
||||
t.Errorf("expected per-mutation error for unknown item id, got %+v", results)
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,17 @@ import (
|
||||
//
|
||||
// Behavior:
|
||||
// - Removes the cart line whose local cart line Id == payload.Id
|
||||
// - Cascades: also removes any line whose ParentId points at a removed line
|
||||
// (transitively), so removing a parent removes its child sub-articles
|
||||
// - If no such line exists returns an error
|
||||
// - Recalculates cart totals (WithTotals)
|
||||
// - Releases reservations for every removed line and recalculates totals
|
||||
//
|
||||
// Notes:
|
||||
// - This removes only the line item; any deliveries referencing the removed
|
||||
// - This removes only the line items; any deliveries referencing a removed
|
||||
// item are NOT automatically adjusted (mirrors prior logic). If future
|
||||
// semantics require pruning delivery.item_ids you can extend this handler.
|
||||
// - If multiple lines somehow shared the same Id (should not happen), only
|
||||
// the first match would be removed—data integrity relies on unique line Ids.
|
||||
// - If multiple lines somehow shared the same Id (should not happen), all
|
||||
// matches are removed—data integrity relies on unique line Ids.
|
||||
|
||||
func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) error {
|
||||
if m == nil {
|
||||
@@ -32,26 +34,46 @@ func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) e
|
||||
|
||||
targetID := uint32(m.Id)
|
||||
|
||||
index := -1
|
||||
for i, it := range g.Items {
|
||||
found := false
|
||||
for _, it := range g.Items {
|
||||
if it.Id == targetID {
|
||||
index = i
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
if !found {
|
||||
return fmt.Errorf("RemoveItem: item id %d not found", m.Id)
|
||||
}
|
||||
|
||||
item := g.Items[index]
|
||||
if item.ReservationEndTime != nil && item.ReservationEndTime.After(time.Now()) {
|
||||
err := c.ReleaseItem(context.Background(), g.Id, item.Sku, item.StoreId)
|
||||
if err != nil {
|
||||
log.Printf("unable to release item reservation")
|
||||
// Collect the target and, transitively, any children pointing at a removed
|
||||
// line. Loops until no further descendants are found (handles nesting).
|
||||
remove := map[uint32]bool{targetID: true}
|
||||
for {
|
||||
grew := false
|
||||
for _, it := range g.Items {
|
||||
if it.ParentId != nil && remove[*it.ParentId] && !remove[it.Id] {
|
||||
remove[it.Id] = true
|
||||
grew = true
|
||||
}
|
||||
}
|
||||
if !grew {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
g.Items = append(g.Items[:index], g.Items[index+1:]...)
|
||||
kept := g.Items[:0]
|
||||
for _, it := range g.Items {
|
||||
if remove[it.Id] {
|
||||
if it.ReservationEndTime != nil && it.ReservationEndTime.After(time.Now()) {
|
||||
if err := c.ReleaseItem(context.Background(), g.Id, it.Sku, it.StoreId); err != nil {
|
||||
log.Printf("unable to release reservation for item %d: %v", it.Id, err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
kept = append(kept, it)
|
||||
}
|
||||
g.Items = kept
|
||||
g.UpdateTotals()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// Removing a parent line cascades to its child sub-articles, while unrelated
|
||||
// lines are left untouched.
|
||||
func TestRemoveItem_CascadesToChildren(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
ctx := context.Background()
|
||||
|
||||
// Parent (line 1) + two children (lines 2,3) + an unrelated item (line 4).
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 100, Sku: "P", Quantity: 1, Price: 1000})
|
||||
parentLine := g.Items[0].Id
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 101, Sku: "C1", Quantity: 1, Price: 100, ParentId: &parentLine})
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 102, Sku: "C2", Quantity: 1, Price: 200, ParentId: &parentLine})
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 200, Sku: "OTHER", Quantity: 1, Price: 300})
|
||||
|
||||
if len(g.Items) != 4 {
|
||||
t.Fatalf("setup: items = %d, want 4", len(g.Items))
|
||||
}
|
||||
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.RemoveItem{Id: parentLine}); err != nil {
|
||||
t.Fatalf("remove parent: %v", err)
|
||||
}
|
||||
|
||||
if len(g.Items) != 1 {
|
||||
t.Fatalf("after remove: items = %d, want 1 (only the unrelated line)", len(g.Items))
|
||||
}
|
||||
if g.Items[0].Sku != "OTHER" {
|
||||
t.Errorf("remaining item sku = %q, want OTHER", g.Items[0].Sku)
|
||||
}
|
||||
}
|
||||
|
||||
// Removing a child leaves the parent and siblings intact.
|
||||
func TestRemoveItem_ChildDoesNotRemoveParent(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
ctx := context.Background()
|
||||
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 100, Sku: "P", Quantity: 1, Price: 1000})
|
||||
parentLine := g.Items[0].Id
|
||||
mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 101, Sku: "C1", Quantity: 1, Price: 100, ParentId: &parentLine})
|
||||
childLine := g.Items[1].Id
|
||||
|
||||
if _, err := reg.Apply(ctx, g, &cart_messages.RemoveItem{Id: childLine}); err != nil {
|
||||
t.Fatalf("remove child: %v", err)
|
||||
}
|
||||
if len(g.Items) != 1 {
|
||||
t.Fatalf("items = %d, want 1 (parent remains)", len(g.Items))
|
||||
}
|
||||
if g.Items[0].Id != parentLine {
|
||||
t.Errorf("remaining line = %d, want parent %d", g.Items[0].Id, parentLine)
|
||||
}
|
||||
}
|
||||
|
||||
func mustApply(t *testing.T, reg actor.MutationRegistry, g *CartGrain, m proto.Message) {
|
||||
t.Helper()
|
||||
if _, err := reg.Apply(context.Background(), g, m); err != nil {
|
||||
t.Fatalf("apply %T: %v", m, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
// SetLineItemCustomFields sets/merges user-supplied custom input fields on an
|
||||
// existing cart line. It is the dict equivalent of LineItemMarking: keys in the
|
||||
// request are upserted; existing keys not present in the request are left
|
||||
// untouched. (Send an empty value to clear a single field at the API layer if
|
||||
// desired.)
|
||||
func SetLineItemCustomFields(grain *CartGrain, req *messages.SetLineItemCustomFields) error {
|
||||
for _, item := range grain.Items {
|
||||
if item.Id != req.Id {
|
||||
continue
|
||||
}
|
||||
if len(req.CustomFields) == 0 {
|
||||
return nil
|
||||
}
|
||||
if item.CustomFields == nil {
|
||||
item.CustomFields = make(map[string]string, len(req.CustomFields))
|
||||
}
|
||||
for k, v := range req.CustomFields {
|
||||
item.CustomFields[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.Id)
|
||||
}
|
||||
Reference in New Issue
Block a user