package cart import ( "context" "testing" "time" cart_messages "git.k6n.net/mats/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)) } } // Re-adding the same parent (same ItemId + StoreId, no ParentId on the // re-add itself) merges the quantity, and the proportional cascade resizes // every direct child to match the new parent qty so a "+1 drill" leaves // the bundle internally consistent. func TestAddItem_MergeCascadesToChildren(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: 100, Sku: "P", Quantity: 1, Price: 1000}); err != nil { t.Fatalf("add parent: %v", err) } parentLine := g.Items[0].Id if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 101, Sku: "C1", Quantity: 1, Price: 100, ParentId: &parentLine}); err != nil { t.Fatalf("add child: %v", err) } // Re-add the same parent — same ItemId, no ParentId on the re-add itself. if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 100, Sku: "P", Quantity: 1, Price: 1000}); err != nil { t.Fatalf("re-add parent: %v", err) } // Parent qty 1 -> 2; child qty 1 -> 2 via cascade. for _, it := range g.Items { if it.Quantity != 2 { t.Errorf("line %s qty = %d, want 2 (merge cascade)", it.Sku, it.Quantity) } } } // Item identity for merge purposes includes ParentId: a standalone root // line (ParentId == nil) and a child line (ParentId == &someParentLine) that // happen to share an ItemId are different roles and MUST stay as distinct // lines. Merging would silently turn an accessory into a +N on the root // (or vice versa), corrupting the parent-child link. func TestAddItem_DoesNotMergeWhenParentIdDiffers(t *testing.T) { reg := NewCartMultationRegistry(NewCartMutationContext(nil)) g := NewCartGrain(1, time.Now()) ctx := context.Background() // Standalone root (no ParentId). if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 100, Sku: "P", Quantity: 1, Price: 1000}); err != nil { t.Fatalf("add standalone: %v", err) } parentLine := g.Items[0].Id // Same ItemId+StoreId but with ParentId set — must create a NEW line, // not bump the standalone's qty. if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 100, Sku: "P", Quantity: 1, Price: 1000, ParentId: &parentLine}); err != nil { t.Fatalf("add child-form: %v", err) } if len(g.Items) != 2 { t.Fatalf("items = %d, want 2 (different ParentId => distinct lines)", len(g.Items)) } // Standalone untouched at qty 1; child-form is its own line at qty 1. for _, it := range g.Items { if it.Quantity != 1 { t.Errorf("line %d (parentId=%v) qty = %d, want 1 (no merge)", it.Id, it.ParentId, it.Quantity) } // One of them has nil ParentId and the other has &parentLine — guard // against an accidental merge ever flipping both to non-nil. } var seenNil, seenSet bool for _, it := range g.Items { if it.ParentId == nil { seenNil = true } else if *it.ParentId == parentLine { seenSet = true } } if !seenNil || !seenSet { t.Errorf("expected one line with nil ParentId and one with &parentLine; got nil=%v set=%v", seenNil, seenSet) } } // Positive control: when BOTH sides have ParentId set to the SAME parent // line, the merge proceeds (this guards the new sameParent guard against // accidentally rejecting a legitimate "re-add same child of same parent" // case — e.g. an idempotent retry from a flaky request). func TestAddItem_MergesWhenBothParentIdMatch(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: 100, Sku: "P", Quantity: 1, Price: 1000}); err != nil { t.Fatalf("add parent: %v", err) } parentLine := g.Items[0].Id // First child: ItemId 200 under parent. if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 200, Sku: "C", Quantity: 1, Price: 100, ParentId: &parentLine}); err != nil { t.Fatalf("add child: %v", err) } // Re-add the SAME child (same ItemId+StoreId+ParentId) — must merge into // qty 2, not create a second accessory line. if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 200, Sku: "C", Quantity: 1, Price: 100, ParentId: &parentLine}); err != nil { t.Fatalf("re-add child: %v", err) } if len(g.Items) != 2 { t.Fatalf("items = %d, want 2 (parent + merged child)", len(g.Items)) } for _, it := range g.Items { switch it.Id { case parentLine: if it.Quantity != 1 { t.Errorf("parent qty = %d, want 1 (untouched)", it.Quantity) } default: if it.Quantity != 2 { t.Errorf("child qty = %d, want 2 (merged on same-PARENT re-add)", it.Quantity) } } } } // Same ItemId+StoreId+both with ParentId set to *different* parent lines // must also stay distinct — children belong to their own parent. func TestAddItem_DoesNotMergeAcrossDifferentParentLines(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: 100, Sku: "P1", Quantity: 1, Price: 500}); err != nil { t.Fatalf("add parent A: %v", err) } parentA := g.Items[0].Id if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 200, Sku: "P2", Quantity: 1, Price: 500}); err != nil { t.Fatalf("add parent B: %v", err) } parentB := g.Items[1].Id // Two children of different parents sharing an ItemId — distinct lines. if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 999, Sku: "C", Quantity: 1, Price: 100, ParentId: &parentA}); err != nil { t.Fatalf("add child of A: %v", err) } if _, err := reg.Apply(ctx, g, &cart_messages.AddItem{ItemId: 999, Sku: "C", Quantity: 1, Price: 100, ParentId: &parentB}); err != nil { t.Fatalf("add child of B: %v", err) } // 2 parent lines + 2 child lines = 4 total; no merges across. if len(g.Items) != 4 { t.Errorf("items = %d, want 4 (parents A,B + their distinct children)", len(g.Items)) } for _, it := range g.Items { if it.Quantity != 1 { t.Errorf("line %d qty = %d, want 1 (no accidental merge)", it.Id, it.Quantity) } } }