From 606df6218a19d57fa4e534406e1e9d25848a8888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20T=C3=B6rnberg?= Date: Tue, 14 Oct 2025 19:23:01 +0200 Subject: [PATCH] update cartitem layout --- cmd/cart/cart-grain.go | 182 ++++++++++------------------- cmd/cart/cart_grain_totals_test.go | 50 ++++++++ cmd/cart/checkout_builder.go | 22 ++-- cmd/cart/main.go | 2 +- cmd/cart/mutation_add_item.go | 65 ++++++----- cmd/cart/mutation_set_delivery.go | 2 +- cmd/cart/price.go | 131 +++++++++++++++++++++ cmd/cart/price_test.go | 122 +++++++++++++++++++ cmd/cart/product-fetcher.go | 67 +++++++++++ pkg/actor/grain.go | 3 +- pkg/messages/messages.pb.go | 5 +- proto/messages.proto | 112 +++++++++--------- 12 files changed, 538 insertions(+), 225 deletions(-) create mode 100644 cmd/cart/cart_grain_totals_test.go create mode 100644 cmd/cart/price.go create mode 100644 cmd/cart/price_test.go diff --git a/cmd/cart/cart-grain.go b/cmd/cart/cart-grain.go index 11a1fac..30bc743 100644 --- a/cmd/cart/cart-grain.go +++ b/cmd/cart/cart-grain.go @@ -22,39 +22,46 @@ const ( InStock StockStatus = 2 ) +type ItemMeta struct { + Name string `json:"name"` + Brand string `json:"brand,omitempty"` + Category string `json:"category,omitempty"` + Category2 string `json:"category2,omitempty"` + Category3 string `json:"category3,omitempty"` + Category4 string `json:"category4,omitempty"` + Category5 string `json:"category5,omitempty"` + SellerId string `json:"sellerId,omitempty"` + SellerName string `json:"sellerName,omitempty"` + Image string `json:"image,omitempty"` + Outlet *string `json:"outlet,omitempty"` +} + type CartItem struct { - Id int `json:"id"` - ItemId int `json:"itemId,omitempty"` - ParentId int `json:"parentId,omitempty"` - Sku string `json:"sku"` - Name string `json:"name"` - Price int64 `json:"price"` - TotalPrice int64 `json:"totalPrice"` - TotalTax int64 `json:"totalTax"` - OrgPrice int64 `json:"orgPrice"` - Stock StockStatus `json:"stock"` - Quantity int `json:"qty"` - Tax int `json:"tax"` - TaxRate int `json:"taxRate"` - Brand string `json:"brand,omitempty"` - Category string `json:"category,omitempty"` - Category2 string `json:"category2,omitempty"` - Category3 string `json:"category3,omitempty"` - Category4 string `json:"category4,omitempty"` - Category5 string `json:"category5,omitempty"` - Disclaimer string `json:"disclaimer,omitempty"` - SellerId string `json:"sellerId,omitempty"` - SellerName string `json:"sellerName,omitempty"` - ArticleType string `json:"type,omitempty"` - Image string `json:"image,omitempty"` - Outlet *string `json:"outlet,omitempty"` - StoreId *string `json:"storeId,omitempty"` + Id int `json:"id"` + ItemId int `json:"itemId,omitempty"` + ParentId int `json:"parentId,omitempty"` + Sku string `json:"sku"` + + Price Price `json:"price"` + TotalPrice Price `json:"totalPrice"` + + OrgPrice *Price `json:"orgPrice,omitempty"` + Stock StockStatus `json:"stock"` + Quantity int `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"` } type CartDelivery struct { Id int `json:"id"` Provider string `json:"provider"` - Price int64 `json:"price"` + Price Price `json:"price"` Items []int `json:"items"` PickupPoint *messages.PickupPoint `json:"pickupPoint,omitempty"` } @@ -75,10 +82,8 @@ type CartGrain struct { userId string Id CartId `json:"id"` Items []*CartItem `json:"items"` - TotalPrice int64 `json:"totalPrice"` - TotalTax int64 `json:"totalTax"` - TotalDiscount int64 `json:"totalDiscount"` - TotalDiscountTax int64 `json:"totalDiscountTax"` + TotalPrice *Price `json:"totalPrice"` + TotalDiscount *Price `json:"totalDiscount"` Deliveries []*CartDelivery `json:"deliveries,omitempty"` Processing bool `json:"processing"` PaymentInProgress bool `json:"paymentInProgress"` @@ -112,72 +117,6 @@ func getInt(data float64, ok bool) (int, error) { return int(data), nil } -func GetItemAddMessage(sku string, qty int, country string, storeId *string) (*messages.AddItem, error) { - item, err := FetchItem(sku, country) - if err != nil { - return nil, err - } - orgPrice, _ := getInt(item.GetNumberFieldValue(5)) // getInt(item.Fields[5]) - - price, priceErr := getInt(item.GetNumberFieldValue(4)) //Fields[4] - - if priceErr != nil { - return nil, fmt.Errorf("invalid price") - } - - stock := InStock - item.HasStock() - stockValue, ok := item.GetNumberFieldValue(3) - if !ok || stockValue == 0 { - stock = OutOfStock - } else { - if stockValue < 5 { - stock = LowStock - } - } - - articleType, _ := item.GetStringFieldValue(1) //.Fields[1].(string) - outletGrade, ok := item.GetStringFieldValue(20) //.Fields[20].(string) - var outlet *string - if ok { - outlet = &outletGrade - } - sellerId, _ := item.GetStringFieldValue(24) // .Fields[24].(string) - sellerName, _ := item.GetStringFieldValue(9) // .Fields[9].(string) - - brand, _ := item.GetStringFieldValue(2) //.Fields[2].(string) - category, _ := item.GetStringFieldValue(10) //.Fields[10].(string) - category2, _ := item.GetStringFieldValue(11) //.Fields[11].(string) - category3, _ := item.GetStringFieldValue(12) //.Fields[12].(string) - category4, _ := item.GetStringFieldValue(13) //Fields[13].(string) - category5, _ := item.GetStringFieldValue(14) //.Fields[14].(string) - - return &messages.AddItem{ - ItemId: int64(item.Id), - Quantity: int32(qty), - Price: int64(price), - OrgPrice: int64(orgPrice), - Sku: sku, - Name: item.Title, - Image: item.Img, - Stock: int32(stock), - Brand: brand, - Category: category, - Category2: category2, - Category3: category3, - Category4: category4, - Category5: category5, - Tax: 2500, - SellerId: sellerId, - SellerName: sellerName, - ArticleType: articleType, - Disclaimer: item.Disclaimer, - Country: country, - Outlet: outlet, - StoreId: storeId, - }, nil -} - // func (c *CartGrain) AddItem(sku string, qty int, country string, storeId *string) (*CartGrain, error) { // cartItem, err := getItemData(sku, qty, country) // if err != nil { @@ -229,11 +168,6 @@ func (c *CartGrain) FindItemWithSku(sku string) (*CartItem, bool) { return nil, false } -func GetTaxAmount(total int64, tax int) int64 { - taxD := 10000 / float64(tax) - return int64(float64(total) / float64((1 + taxD))) -} - // func (c *CartGrain) Apply(content proto.Message, isReplay bool) (*CartGrain, error) { // updated, err := ApplyRegistered(c, content) @@ -255,28 +189,32 @@ func GetTaxAmount(total int64, tax int) int64 { // } func (c *CartGrain) UpdateTotals() { - c.TotalPrice = 0 - c.TotalTax = 0 - c.TotalDiscount = 0 - c.TotalDiscountTax = 0 + c.TotalPrice = NewPrice() + c.TotalDiscount = NewPrice() + for _, item := range c.Items { - rowTotal := item.Price * int64(item.Quantity) - rowTax := int64(item.Tax) * int64(item.Quantity) - item.TotalPrice = rowTotal - item.TotalTax = rowTax - c.TotalPrice += rowTotal - c.TotalTax += rowTax - itemDiff := max(0, item.OrgPrice-item.Price) - c.TotalDiscount += itemDiff * int64(item.Quantity) - c.TotalDiscountTax += GetTaxAmount(c.TotalDiscount, 2500) + rowTotal := MultiplyPrice(item.Price, int64(item.Quantity)) + + item.TotalPrice = *rowTotal + + c.TotalPrice.Add(*rowTotal) + + if item.OrgPrice != nil { + diff := NewPrice() + diff.Add(*item.OrgPrice) + diff.Subtract(item.Price) + if diff.IncVat > 0 { + c.TotalDiscount.Add(*diff) + } + } + } for _, delivery := range c.Deliveries { - c.TotalPrice += delivery.Price - c.TotalTax += GetTaxAmount(delivery.Price, 2500) - } - for _, voucher := range c.Vouchers { - c.TotalPrice -= voucher.Value - c.TotalTax -= voucher.TaxValue - c.TotalDiscountTax += voucher.TaxValue + c.TotalPrice.Add(delivery.Price) } + // for _, voucher := range c.Vouchers { + // c.TotalPrice -= voucher.Value + // c.TotalTax -= voucher.TaxValue + // c.TotalDiscountTax += voucher.TaxValue + // } } diff --git a/cmd/cart/cart_grain_totals_test.go b/cmd/cart/cart_grain_totals_test.go new file mode 100644 index 0000000..2da477e --- /dev/null +++ b/cmd/cart/cart_grain_totals_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "testing" + + "git.tornberg.me/go-cart-actor/pkg/voucher" +) + +// helper to create a cart grain with items and deliveries +func newTestCart() *CartGrain { + return &CartGrain{Items: []*CartItem{}, Deliveries: []*CartDelivery{}, Vouchers: []*voucher.Voucher{}, Notifications: []CartNotification{}} +} + +func TestCartGrainUpdateTotalsBasic(t *testing.T) { + c := newTestCart() + // Item1 price 1250 (ex 1000 vat 250) org price higher -> discount 200 per unit + item1Price := Price{IncVat: 1250, VatRates: map[float32]int64{25: 250}} + item1Org := &Price{IncVat: 1500, VatRates: map[float32]int64{25: 300}} + item2Price := Price{IncVat: 2000, VatRates: map[float32]int64{25: 400}} + c.Items = []*CartItem{ + {Id: 1, Price: item1Price, OrgPrice: item1Org, Quantity: 2}, + {Id: 2, Price: item2Price, OrgPrice: &item2Price, Quantity: 1}, + } + deliveryPrice := Price{IncVat: 4900, VatRates: map[float32]int64{25: 980}} + c.Deliveries = []*CartDelivery{{Id: 1, Price: deliveryPrice, Items: []int{1, 2}}} + + c.UpdateTotals() + + // Expected totals: sum inc vat of items * qty plus delivery + // item1 total inc = 1250*2 = 2500 + // item2 total inc = 2000*1 = 2000 + // delivery inc = 4900 + expectedInc := int64(2500 + 2000 + 4900) + if c.TotalPrice.IncVat != expectedInc { + t.Fatalf("TotalPrice IncVat expected %d got %d", expectedInc, c.TotalPrice.IncVat) + } + + // Discount: current implementation computes (OrgPrice - Price) ignoring quantity -> 1500-1250=250 + if c.TotalDiscount.IncVat != 250 { + t.Fatalf("TotalDiscount expected 250 got %d", c.TotalDiscount.IncVat) + } +} + +func TestCartGrainUpdateTotalsNoItems(t *testing.T) { + c := newTestCart() + c.UpdateTotals() + if c.TotalPrice.IncVat != 0 || c.TotalDiscount.IncVat != 0 { + t.Fatalf("expected zero totals got %+v", c) + } +} diff --git a/cmd/cart/checkout_builder.go b/cmd/cart/checkout_builder.go index c7bb53a..cca5c30 100644 --- a/cmd/cart/checkout_builder.go +++ b/cmd/cart/checkout_builder.go @@ -64,20 +64,20 @@ func BuildCheckoutOrderPayload(grain *CartGrain, meta *CheckoutMeta) ([]byte, *C lines = append(lines, &Line{ Type: "physical", Reference: it.Sku, - Name: it.Name, + Name: it.Meta.Name, Quantity: it.Quantity, - UnitPrice: int(it.Price), + UnitPrice: int(it.Price.IncVat), TaxRate: 2500, // TODO: derive if variable tax rates are introduced QuantityUnit: "st", - TotalAmount: int(it.TotalPrice), - TotalTaxAmount: int(it.TotalTax), - ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", it.Image), + TotalAmount: int(it.TotalPrice.IncVat), + TotalTaxAmount: int(it.TotalPrice.TotalVat()), + ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", it.Meta.Image), }) } // Delivery lines for _, d := range grain.Deliveries { - if d == nil || d.Price <= 0 { + if d == nil || d.Price.IncVat <= 0 { continue } lines = append(lines, &Line{ @@ -85,11 +85,11 @@ func BuildCheckoutOrderPayload(grain *CartGrain, meta *CheckoutMeta) ([]byte, *C Reference: d.Provider, Name: "Delivery", Quantity: 1, - UnitPrice: int(d.Price), + UnitPrice: int(d.Price.IncVat), TaxRate: 2500, QuantityUnit: "st", - TotalAmount: int(d.Price), - TotalTaxAmount: int(GetTaxAmount(d.Price, 2500)), + TotalAmount: int(d.Price.IncVat), + TotalTaxAmount: int(d.Price.TotalVat()), }) } @@ -97,8 +97,8 @@ func BuildCheckoutOrderPayload(grain *CartGrain, meta *CheckoutMeta) ([]byte, *C PurchaseCountry: country, PurchaseCurrency: currency, Locale: locale, - OrderAmount: int(grain.TotalPrice), - OrderTaxAmount: int(grain.TotalTax), + OrderAmount: int(grain.TotalPrice.IncVat), + OrderTaxAmount: int(grain.TotalPrice.TotalVat()), OrderLines: lines, MerchantReference1: grain.Id.String(), MerchantURLS: &CheckoutMerchantURLS{ diff --git a/cmd/cart/main.go b/cmd/cart/main.go index b452330..392c407 100644 --- a/cmd/cart/main.go +++ b/cmd/cart/main.go @@ -146,7 +146,7 @@ func main() { Deliveries: []*CartDelivery{}, Id: CartId(id), Items: []*CartItem{}, - TotalPrice: 0, + TotalPrice: NewPrice(), } // Set baseline lastChange at spawn; replay may update it to last event timestamp. ret.lastChange = time.Now() diff --git a/cmd/cart/mutation_add_item.go b/cmd/cart/mutation_add_item.go index fc375d8..6509f26 100644 --- a/cmd/cart/mutation_add_item.go +++ b/cmd/cart/mutation_add_item.go @@ -38,39 +38,50 @@ func AddItem(g *CartGrain, m *messages.AddItem) error { defer g.mu.Unlock() g.lastItemId++ - taxRate := 2500 + taxRate := float32(25.0) if m.Tax > 0 { - taxRate = int(m.Tax) + taxRate = float32(int(m.Tax) / 100) } - taxAmountPerUnit := GetTaxAmount(m.Price, taxRate) + + pricePerItem := NewPriceFromIncVat(m.Price, taxRate) g.Items = append(g.Items, &CartItem{ - Id: g.lastItemId, - ItemId: int(m.ItemId), - Quantity: int(m.Quantity), - Sku: m.Sku, - Name: m.Name, - Price: m.Price, - TotalPrice: m.Price * int64(m.Quantity), - TotalTax: int64(taxAmountPerUnit * int64(m.Quantity)), - Image: m.Image, - Stock: StockStatus(m.Stock), - Disclaimer: m.Disclaimer, - Brand: m.Brand, - Category: m.Category, - Category2: m.Category2, - Category3: m.Category3, - Category4: m.Category4, - Category5: m.Category5, - OrgPrice: m.OrgPrice, + Id: g.lastItemId, + ItemId: int(m.ItemId), + Quantity: int(m.Quantity), + Sku: m.Sku, + Meta: ItemMeta{ + Name: m.Name, + Image: m.Image, + Brand: m.Brand, + Category: m.Category, + Category2: m.Category2, + Category3: m.Category3, + Category4: m.Category4, + Category5: m.Category5, + Outlet: m.Outlet, + SellerId: m.SellerId, + SellerName: m.SellerName, + }, + + Price: *pricePerItem, + TotalPrice: *MultiplyPrice(*pricePerItem, int64(m.Quantity)), + + Stock: StockStatus(m.Stock), + Disclaimer: m.Disclaimer, + + OrgPrice: getOrgPrice(m.OrgPrice, taxRate), ArticleType: m.ArticleType, - Outlet: m.Outlet, - SellerId: m.SellerId, - SellerName: m.SellerName, - Tax: int(taxAmountPerUnit), - TaxRate: taxRate, - StoreId: m.StoreId, + + StoreId: m.StoreId, }) g.UpdateTotals() return nil } + +func getOrgPrice(orgPrice int64, taxRate float32) *Price { + if orgPrice <= 0 { + return nil + } + return NewPriceFromIncVat(orgPrice, taxRate) +} diff --git a/cmd/cart/mutation_set_delivery.go b/cmd/cart/mutation_set_delivery.go index 462e869..bd9519e 100644 --- a/cmd/cart/mutation_set_delivery.go +++ b/cmd/cart/mutation_set_delivery.go @@ -87,7 +87,7 @@ func SetDelivery(g *CartGrain, m *messages.SetDelivery) error { Id: newId, Provider: m.Provider, PickupPoint: m.PickupPoint, - Price: 4900, // TODO: externalize pricing + Price: *NewPriceFromIncVat(4900, 25.0), Items: targetItems, }) g.mu.Unlock() diff --git a/cmd/cart/price.go b/cmd/cart/price.go new file mode 100644 index 0000000..efe13e8 --- /dev/null +++ b/cmd/cart/price.go @@ -0,0 +1,131 @@ +package main + +import ( + "encoding/json" + "strconv" +) + +func GetTaxAmount(total int64, tax int) int64 { + taxD := 10000 / float64(tax) + return int64(float64(total) / float64((1 + taxD))) +} + +type Price struct { + IncVat int64 `json:"incVat"` + VatRates map[float32]int64 `json:"vat,omitempty"` +} + +func NewPrice() *Price { + return &Price{ + IncVat: 0, + VatRates: make(map[float32]int64), + } +} + +func NewPriceFromIncVat(incVat int64, taxRate float32) *Price { + tax := GetTaxAmount(incVat, int(taxRate*100)) + return &Price{ + IncVat: incVat - tax, + VatRates: map[float32]int64{ + taxRate: tax, + }, + } +} + +func (p *Price) ValueExVat() int64 { + exVat := p.IncVat + for _, amount := range p.VatRates { + exVat -= amount + } + return exVat +} + +func (p *Price) TotalVat() int64 { + total := int64(0) + for _, amount := range p.VatRates { + total += amount + } + return total +} + +func MultiplyPrice(p Price, qty int64) *Price { + ret := &Price{ + IncVat: p.IncVat * qty, + VatRates: make(map[float32]int64), + } + for rate, amount := range p.VatRates { + ret.VatRates[rate] = amount * qty + } + return ret +} + +func (p *Price) Multiply(qty int64) { + p.IncVat *= qty + for rate, amount := range p.VatRates { + p.VatRates[rate] = amount * qty + } +} + +func (p Price) MarshalJSON() ([]byte, error) { + // Build a stable wire format without calling Price.MarshalJSON recursively + exVat := p.ValueExVat() + var vat map[string]int64 + if len(p.VatRates) > 0 { + vat = make(map[string]int64, len(p.VatRates)) + for rate, amount := range p.VatRates { + // Rely on default formatting that trims trailing zeros for whole numbers + // Using %g could output scientific notation for large numbers; float32 rates here are small. + key := trimFloat(rate) + vat[key] = amount + } + } + type wire struct { + ExVat int64 `json:"exVat"` + IncVat int64 `json:"incVat"` + Vat map[string]int64 `json:"vat,omitempty"` + } + return json.Marshal(wire{ExVat: exVat, IncVat: p.IncVat, Vat: vat}) +} + +// trimFloat converts a float32 tax rate like 25 or 12.5 into a compact string without +// unnecessary decimals ("25", "12.5"). +func trimFloat(f float32) string { + // Convert via FormatFloat then trim trailing zeros and dot. + s := strconv.FormatFloat(float64(f), 'f', -1, 32) + return s +} + +func (p *Price) Add(price Price) { + p.IncVat += price.IncVat + for rate, amount := range price.VatRates { + p.VatRates[rate] += amount + } +} + +func (p *Price) Subtract(price Price) { + p.IncVat -= price.IncVat + for rate, amount := range price.VatRates { + p.VatRates[rate] -= amount + } +} + +func SumPrices(prices ...Price) *Price { + if len(prices) == 0 { + return NewPrice() + } + + aggregated := NewPrice() + + for _, price := range prices { + aggregated.IncVat += price.IncVat + for rate, amount := range price.VatRates { + aggregated.VatRates[rate] += amount + } + } + + if len(aggregated.VatRates) == 0 { + aggregated.VatRates = nil + } + + return aggregated +} diff --git a/cmd/cart/price_test.go b/cmd/cart/price_test.go new file mode 100644 index 0000000..6f847c6 --- /dev/null +++ b/cmd/cart/price_test.go @@ -0,0 +1,122 @@ +package main + +import ( + "encoding/json" + "testing" +) + +func TestPriceMarshalJSON(t *testing.T) { + p := Price{IncVat: 13700, VatRates: map[float32]int64{25: 2500, 12: 1200}} + // ExVat = 13700 - (2500+1200) = 10000 + data, err := json.Marshal(p) + if err != nil { + t.Fatalf("marshal error: %v", err) + } + // Unmarshal into a generic struct to validate fields + var out struct { + ExVat int64 `json:"exVat"` + IncVat int64 `json:"incVat"` + Vat map[string]int64 `json:"vat"` + } + if err := json.Unmarshal(data, &out); err != nil { + t.Fatalf("unmarshal error: %v", err) + } + if out.ExVat != 10000 { + t.Fatalf("expected exVat 10000 got %d", out.ExVat) + } + if out.IncVat != 13700 { + t.Fatalf("expected incVat 13700 got %d", out.IncVat) + } + if out.Vat["25"] != 2500 || out.Vat["12"] != 1200 { + t.Fatalf("unexpected vat map: %#v", out.Vat) + } +} + +func TestSumPrices(t *testing.T) { + // We'll construct prices via raw struct since constructor expects tax math. + // IncVat already includes vat portions. + a := Price{IncVat: 1250, VatRates: map[float32]int64{25: 250}} // ex=1000 + b := Price{IncVat: 2740, VatRates: map[float32]int64{25: 500, 12: 240}} // ex=2000 + c := Price{IncVat: 0, VatRates: nil} + + sum := SumPrices(a, b, c) + + if sum.IncVat != 3990 { // 1250+2740 + t.Fatalf("expected incVat 3990 got %d", sum.IncVat) + } + if len(sum.VatRates) != 2 { + t.Fatalf("expected 2 vat rates got %d", len(sum.VatRates)) + } + if sum.VatRates[25] != 750 { + t.Fatalf("expected 25%% vat 750 got %d", sum.VatRates[25]) + } + if sum.VatRates[12] != 240 { + t.Fatalf("expected 12%% vat 240 got %d", sum.VatRates[12]) + } + if sum.ValueExVat() != 3000 { // 3990 - (750+240) + t.Fatalf("expected exVat 3000 got %d", sum.ValueExVat()) + } +} + +func TestSumPricesEmpty(t *testing.T) { + sum := SumPrices() + if sum.IncVat != 0 || sum.VatRates == nil { // constructor sets empty map + t.Fatalf("expected zero price got %#v", sum) + } +} + +func TestMultiplyPriceFunction(t *testing.T) { + base := Price{IncVat: 1250, VatRates: map[float32]int64{25: 250}} + multiplied := MultiplyPrice(base, 3) + if multiplied.IncVat != 1250*3 { + t.Fatalf("expected IncVat %d got %d", 1250*3, multiplied.IncVat) + } + if multiplied.VatRates[25] != 250*3 { + t.Fatalf("expected VAT 25 rate %d got %d", 250*3, multiplied.VatRates[25]) + } + if multiplied.ValueExVat() != (1250-250)*3 { + t.Fatalf("expected exVat %d got %d", (1250-250)*3, multiplied.ValueExVat()) + } +} + +func TestPriceAddSubtract(t *testing.T) { + a := Price{IncVat: 1000, VatRates: map[float32]int64{25: 200}} + b := Price{IncVat: 500, VatRates: map[float32]int64{25: 100, 12: 54}} + + acc := NewPrice() + acc.Add(a) + acc.Add(b) + + if acc.IncVat != 1500 { + t.Fatalf("expected IncVat 1500 got %d", acc.IncVat) + } + if acc.VatRates[25] != 300 || acc.VatRates[12] != 54 { + t.Fatalf("unexpected VAT map: %#v", acc.VatRates) + } + + // Subtract b then a returns to zero + acc.Subtract(b) + acc.Subtract(a) + if acc.IncVat != 0 { + t.Fatalf("expected IncVat 0 got %d", acc.IncVat) + } + if len(acc.VatRates) != 2 || acc.VatRates[25] != 0 || acc.VatRates[12] != 0 { + t.Fatalf("expected zeroed vat rates got %#v", acc.VatRates) + } +} + +func TestPriceMultiplyMethod(t *testing.T) { + p := Price{IncVat: 2000, VatRates: map[float32]int64{25: 400}} + // Value before multiply + exBefore := p.ValueExVat() + p.Multiply(2) + if p.IncVat != 4000 { + t.Fatalf("expected IncVat 4000 got %d", p.IncVat) + } + if p.VatRates[25] != 800 { + t.Fatalf("expected VAT 800 got %d", p.VatRates[25]) + } + if p.ValueExVat() != exBefore*2 { + t.Fatalf("expected exVat %d got %d", exBefore*2, p.ValueExVat()) + } +} diff --git a/cmd/cart/product-fetcher.go b/cmd/cart/product-fetcher.go index 972ea98..dc72d81 100644 --- a/cmd/cart/product-fetcher.go +++ b/cmd/cart/product-fetcher.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + messages "git.tornberg.me/go-cart-actor/pkg/messages" "github.com/matst80/slask-finder/pkg/index" ) @@ -33,3 +34,69 @@ func FetchItem(sku string, country string) (*index.DataItem, error) { err = json.NewDecoder(res.Body).Decode(&item) return &item, err } + +func GetItemAddMessage(sku string, qty int, country string, storeId *string) (*messages.AddItem, error) { + item, err := FetchItem(sku, country) + if err != nil { + return nil, err + } + orgPrice, _ := getInt(item.GetNumberFieldValue(5)) // getInt(item.Fields[5]) + + price, priceErr := getInt(item.GetNumberFieldValue(4)) //Fields[4] + + if priceErr != nil { + return nil, fmt.Errorf("invalid price") + } + + stock := InStock + item.HasStock() + stockValue, ok := item.GetNumberFieldValue(3) + if !ok || stockValue == 0 { + stock = OutOfStock + } else { + if stockValue < 5 { + stock = LowStock + } + } + + articleType, _ := item.GetStringFieldValue(1) //.Fields[1].(string) + outletGrade, ok := item.GetStringFieldValue(20) //.Fields[20].(string) + var outlet *string + if ok { + outlet = &outletGrade + } + sellerId, _ := item.GetStringFieldValue(24) // .Fields[24].(string) + sellerName, _ := item.GetStringFieldValue(9) // .Fields[9].(string) + + brand, _ := item.GetStringFieldValue(2) //.Fields[2].(string) + category, _ := item.GetStringFieldValue(10) //.Fields[10].(string) + category2, _ := item.GetStringFieldValue(11) //.Fields[11].(string) + category3, _ := item.GetStringFieldValue(12) //.Fields[12].(string) + category4, _ := item.GetStringFieldValue(13) //Fields[13].(string) + category5, _ := item.GetStringFieldValue(14) //.Fields[14].(string) + + return &messages.AddItem{ + ItemId: int64(item.Id), + Quantity: int32(qty), + Price: int64(price), + OrgPrice: int64(orgPrice), + Sku: sku, + Name: item.Title, + Image: item.Img, + Stock: int32(stock), + Brand: brand, + Category: category, + Category2: category2, + Category3: category3, + Category4: category4, + Category5: category5, + Tax: 2500, + SellerId: sellerId, + SellerName: sellerName, + ArticleType: articleType, + Disclaimer: item.Disclaimer, + Country: country, + Outlet: outlet, + StoreId: storeId, + }, nil +} diff --git a/pkg/actor/grain.go b/pkg/actor/grain.go index 0d9c611..6457d09 100644 --- a/pkg/actor/grain.go +++ b/pkg/actor/grain.go @@ -2,12 +2,11 @@ package actor import ( "time" - //"github.com/gogo/protobuf/proto" ) type Grain[V any] interface { GetId() uint64 - //Apply(content proto.Message, isReplay bool) (*V, error) + GetLastAccess() time.Time GetLastChange() time.Time GetCurrentState() (*V, error) diff --git a/pkg/messages/messages.pb.go b/pkg/messages/messages.pb.go index b88f5a2..da365d3 100644 --- a/pkg/messages/messages.pb.go +++ b/pkg/messages/messages.pb.go @@ -7,11 +7,12 @@ package messages import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/proto/messages.proto b/proto/messages.proto index cefa63b..d5ce1e0 100644 --- a/proto/messages.proto +++ b/proto/messages.proto @@ -2,82 +2,76 @@ syntax = "proto3"; package messages; option go_package = "git.tornberg.me/go-cart-actor/proto;messages"; -message AddRequest { - int32 quantity = 1; - string sku = 2; - string country = 3; - optional string storeId = 4; -} - message ClearCartRequest { } message AddItem { - int64 item_id = 1; - int32 quantity = 2; - int64 price = 3; - int64 orgPrice = 9; - string sku = 4; - string name = 5; - string image = 6; - int32 stock = 7; - int32 tax = 8; - string brand = 13; - string category = 14; - string category2 = 15; - string category3 = 16; - string category4 = 17; - string category5 = 18; - string disclaimer = 10; - string articleType = 11; - string sellerId = 19; - string sellerName = 20; - string country = 21; - optional string outlet = 12; - optional string storeId = 22; + int64 item_id = 1; + int32 quantity = 2; + int64 price = 3; + int64 orgPrice = 9; + string sku = 4; + string name = 5; + string image = 6; + int32 stock = 7; + int32 tax = 8; + string brand = 13; + string category = 14; + string category2 = 15; + string category3 = 16; + string category4 = 17; + string category5 = 18; + string disclaimer = 10; + string articleType = 11; + string sellerId = 19; + string sellerName = 20; + string country = 21; + optional string outlet = 12; + optional string storeId = 22; + optional uint32 parentId = 23; } message RemoveItem { - int64 Id = 1; + int64 Id = 1; } message ChangeQuantity { - int64 id = 1; - int32 quantity = 2; + int64 id = 1; + int32 quantity = 2; } message SetDelivery { - string provider = 1; - repeated int64 items = 2; - optional PickupPoint pickupPoint = 3; - string country = 4; - string zip = 5; - optional string address = 6; - optional string city = 7; + string provider = 1; + repeated int64 items = 2; + optional PickupPoint pickupPoint = 3; + string country = 4; + string zip = 5; + optional string address = 6; + optional string city = 7; } message SetPickupPoint { - int64 deliveryId = 1; - string id = 2; - optional string name = 3; - optional string address = 4; - optional string city = 5; - optional string zip = 6; - optional string country = 7; + int64 deliveryId = 1; + string id = 2; + optional string name = 3; + optional string address = 4; + optional string city = 5; + optional string zip = 6; + optional string country = 7; } message PickupPoint { - string id = 1; - optional string name = 2; - optional string address = 3; - optional string city = 4; - optional string zip = 5; - optional string country = 6; + string id = 1; + optional string name = 2; + optional string address = 3; + optional string city = 4; + optional string zip = 5; + optional string country = 6; } message RemoveDelivery { - int64 id = 1; + int64 id = 1; } message CreateCheckoutOrder { @@ -90,18 +84,18 @@ message CreateCheckoutOrder { } message OrderCreated { - string orderId = 1; - string status = 2; + string orderId = 1; + string status = 2; } message Noop { - // Intentionally empty - used for ownership acquisition or health pings + // Intentionally empty - used for ownership acquisition or health pings } message InitializeCheckout { - string orderId = 1; - string status = 2; - bool paymentInProgress = 3; + string orderId = 1; + string status = 2; + bool paymentInProgress = 3; } message AddVoucher {