package cart import ( "context" "testing" "time" cart_messages "git.k6n.net/mats/go-cart-actor/proto/cart" ) // Parent qty 1 → 2 resizes every direct child proportionally. Children all // start at qty 1 so the proportional factor of 2 doubles them. func TestChangeQuantity_ParentCascadesDoublingChildren(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}) mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 102, Sku: "C2", Quantity: 1, Price: 200, ParentId: &parentLine}) if _, err := reg.Apply(ctx, g, &cart_messages.ChangeQuantity{Id: parentLine, Quantity: 2}); err != nil { t.Fatalf("change qty: %v", err) } if g.Items[0].Quantity != 2 { t.Errorf("parent qty = %d, want 2", g.Items[0].Quantity) } for _, it := range g.Items { if it.Id == parentLine { continue } if it.Quantity != 2 { t.Errorf("child %s qty = %d, want 2 (cascaded from parent)", it.Sku, it.Quantity) } } } // Parent qty 2 → 3 with children at qty 2 → 3 (factor 3/2 = 1.5, integer math). func TestChangeQuantity_ParentCascadesNonIntegerRatio(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: 2, Price: 1000}) parentLine := g.Items[0].Id mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 101, Sku: "C1", Quantity: 2, Price: 100, ParentId: &parentLine}) if _, err := reg.Apply(ctx, g, &cart_messages.ChangeQuantity{Id: parentLine, Quantity: 3}); err != nil { t.Fatalf("change qty: %v", err) } // parent: 2 -> 3 (direct set), child: 2 * 3 / 2 = 3 if g.Items[0].Quantity != 3 { t.Errorf("parent qty = %d, want 3", g.Items[0].Quantity) } if g.Items[1].Quantity != 3 { t.Errorf("child qty = %d, want 3 (proportional scale)", g.Items[1].Quantity) } } // Scale-down that would round to 0 (parent 2 → 1, child qty 1 → (1*1)/2 = 0) // must clamp to 1 so the child is never silently dropped by integer math. func TestChangeQuantity_ParentScaleDownFloorsAtOne(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: 2, Price: 1000}) parentLine := g.Items[0].Id mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 101, Sku: "C1", Quantity: 1, Price: 100, ParentId: &parentLine}) if _, err := reg.Apply(ctx, g, &cart_messages.ChangeQuantity{Id: parentLine, Quantity: 1}); err != nil { t.Fatalf("change qty: %v", err) } if g.Items[0].Quantity != 1 { t.Errorf("parent qty = %d, want 1", g.Items[0].Quantity) } if g.Items[1].Quantity != 1 { t.Errorf("child qty = %d, want 1 (floor; integer math dropped to 0 without clamp)", g.Items[1].Quantity) } } // Setting parent qty to 0 delegates to RemoveItem so the existing parent→ // child cascade takes over. Unrelated lines are left untouched. func TestChangeQuantity_ZeroCascadesRemoval(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}) mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 200, Sku: "OTHER", Quantity: 1, Price: 500}) if _, err := reg.Apply(ctx, g, &cart_messages.ChangeQuantity{Id: parentLine, Quantity: 0}); err != nil { t.Fatalf("change qty 0: %v", err) } if len(g.Items) != 1 { t.Fatalf("items = %d, want 1 (only OTHER survives)", len(g.Items)) } if g.Items[0].Sku != "OTHER" { t.Errorf("remaining sku = %q, want OTHER", g.Items[0].Sku) } } // Changing a child's qty directly updates only that child — siblings and // the parent are untouched. (The UI no longer exposes this control, but // the HTTP surface still allows it for tools & direct API calls.) func TestChangeQuantity_ChildDoesNotCascade(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}) mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 102, Sku: "C2", Quantity: 1, Price: 200, ParentId: &parentLine}) targetChild := g.Items[1].Id if _, err := reg.Apply(ctx, g, &cart_messages.ChangeQuantity{Id: targetChild, Quantity: 3}); err != nil { t.Fatalf("change child qty: %v", err) } for _, it := range g.Items { switch it.Id { case parentLine: if it.Quantity != 1 { t.Errorf("parent qty = %d, want 1 (untouched on child edit)", it.Quantity) } case targetChild: if it.Quantity != 3 { t.Errorf("target child qty = %d, want 3", it.Quantity) } default: // Sibling if it.Quantity != 1 { t.Errorf("sibling %s qty = %d, want 1 (untouched on child edit)", it.Sku, it.Quantity) } } } } // Cascade descends into grandchildren so deeply nested accessory trees // remain internally consistent with the top-level parent change. func TestChangeQuantity_NestedChildren(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 mustApply(t, reg, g, &cart_messages.AddItem{ItemId: 102, Sku: "G1", Quantity: 1, Price: 50, ParentId: &childLine}) if _, err := reg.Apply(ctx, g, &cart_messages.ChangeQuantity{Id: parentLine, Quantity: 2}); err != nil { t.Fatalf("change qty: %v", err) } // parent 1 -> 2; child 1 -> 2 (parent -> child cascade); grandchild 1 -> 2 (child -> grandchild cascade) for _, it := range g.Items { if it.Quantity != 2 { t.Errorf("line %s qty = %d, want 2 (nested cascade)", it.Sku, it.Quantity) } } }