Complete refactor to new grpc control plane and only http proxy for carts #4
@@ -22,39 +22,46 @@ const (
|
|||||||
InStock StockStatus = 2
|
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 {
|
type CartItem struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
ItemId int `json:"itemId,omitempty"`
|
ItemId int `json:"itemId,omitempty"`
|
||||||
ParentId int `json:"parentId,omitempty"`
|
ParentId int `json:"parentId,omitempty"`
|
||||||
Sku string `json:"sku"`
|
Sku string `json:"sku"`
|
||||||
Name string `json:"name"`
|
|
||||||
Price int64 `json:"price"`
|
Price Price `json:"price"`
|
||||||
TotalPrice int64 `json:"totalPrice"`
|
TotalPrice Price `json:"totalPrice"`
|
||||||
TotalTax int64 `json:"totalTax"`
|
|
||||||
OrgPrice int64 `json:"orgPrice"`
|
OrgPrice *Price `json:"orgPrice,omitempty"`
|
||||||
Stock StockStatus `json:"stock"`
|
Stock StockStatus `json:"stock"`
|
||||||
Quantity int `json:"qty"`
|
Quantity int `json:"qty"`
|
||||||
Tax int `json:"tax"`
|
Discount *Price `json:"discount,omitempty"`
|
||||||
TaxRate int `json:"taxRate"`
|
|
||||||
Brand string `json:"brand,omitempty"`
|
Disclaimer string `json:"disclaimer,omitempty"`
|
||||||
Category string `json:"category,omitempty"`
|
|
||||||
Category2 string `json:"category2,omitempty"`
|
ArticleType string `json:"type,omitempty"`
|
||||||
Category3 string `json:"category3,omitempty"`
|
|
||||||
Category4 string `json:"category4,omitempty"`
|
StoreId *string `json:"storeId,omitempty"`
|
||||||
Category5 string `json:"category5,omitempty"`
|
Meta ItemMeta `json:"meta,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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CartDelivery struct {
|
type CartDelivery struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Price int64 `json:"price"`
|
Price Price `json:"price"`
|
||||||
Items []int `json:"items"`
|
Items []int `json:"items"`
|
||||||
PickupPoint *messages.PickupPoint `json:"pickupPoint,omitempty"`
|
PickupPoint *messages.PickupPoint `json:"pickupPoint,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -75,10 +82,8 @@ type CartGrain struct {
|
|||||||
userId string
|
userId string
|
||||||
Id CartId `json:"id"`
|
Id CartId `json:"id"`
|
||||||
Items []*CartItem `json:"items"`
|
Items []*CartItem `json:"items"`
|
||||||
TotalPrice int64 `json:"totalPrice"`
|
TotalPrice *Price `json:"totalPrice"`
|
||||||
TotalTax int64 `json:"totalTax"`
|
TotalDiscount *Price `json:"totalDiscount"`
|
||||||
TotalDiscount int64 `json:"totalDiscount"`
|
|
||||||
TotalDiscountTax int64 `json:"totalDiscountTax"`
|
|
||||||
Deliveries []*CartDelivery `json:"deliveries,omitempty"`
|
Deliveries []*CartDelivery `json:"deliveries,omitempty"`
|
||||||
Processing bool `json:"processing"`
|
Processing bool `json:"processing"`
|
||||||
PaymentInProgress bool `json:"paymentInProgress"`
|
PaymentInProgress bool `json:"paymentInProgress"`
|
||||||
@@ -112,72 +117,6 @@ func getInt(data float64, ok bool) (int, error) {
|
|||||||
return int(data), nil
|
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) {
|
// func (c *CartGrain) AddItem(sku string, qty int, country string, storeId *string) (*CartGrain, error) {
|
||||||
// cartItem, err := getItemData(sku, qty, country)
|
// cartItem, err := getItemData(sku, qty, country)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
@@ -229,11 +168,6 @@ func (c *CartGrain) FindItemWithSku(sku string) (*CartItem, bool) {
|
|||||||
return nil, false
|
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) {
|
// func (c *CartGrain) Apply(content proto.Message, isReplay bool) (*CartGrain, error) {
|
||||||
|
|
||||||
// updated, err := ApplyRegistered(c, content)
|
// updated, err := ApplyRegistered(c, content)
|
||||||
@@ -255,28 +189,32 @@ func GetTaxAmount(total int64, tax int) int64 {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
func (c *CartGrain) UpdateTotals() {
|
func (c *CartGrain) UpdateTotals() {
|
||||||
c.TotalPrice = 0
|
c.TotalPrice = NewPrice()
|
||||||
c.TotalTax = 0
|
c.TotalDiscount = NewPrice()
|
||||||
c.TotalDiscount = 0
|
|
||||||
c.TotalDiscountTax = 0
|
|
||||||
for _, item := range c.Items {
|
for _, item := range c.Items {
|
||||||
rowTotal := item.Price * int64(item.Quantity)
|
rowTotal := MultiplyPrice(item.Price, int64(item.Quantity))
|
||||||
rowTax := int64(item.Tax) * int64(item.Quantity)
|
|
||||||
item.TotalPrice = rowTotal
|
item.TotalPrice = *rowTotal
|
||||||
item.TotalTax = rowTax
|
|
||||||
c.TotalPrice += rowTotal
|
c.TotalPrice.Add(*rowTotal)
|
||||||
c.TotalTax += rowTax
|
|
||||||
itemDiff := max(0, item.OrgPrice-item.Price)
|
if item.OrgPrice != nil {
|
||||||
c.TotalDiscount += itemDiff * int64(item.Quantity)
|
diff := NewPrice()
|
||||||
c.TotalDiscountTax += GetTaxAmount(c.TotalDiscount, 2500)
|
diff.Add(*item.OrgPrice)
|
||||||
|
diff.Subtract(item.Price)
|
||||||
|
if diff.IncVat > 0 {
|
||||||
|
c.TotalDiscount.Add(*diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
for _, delivery := range c.Deliveries {
|
for _, delivery := range c.Deliveries {
|
||||||
c.TotalPrice += delivery.Price
|
c.TotalPrice.Add(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
|
|
||||||
}
|
}
|
||||||
|
// for _, voucher := range c.Vouchers {
|
||||||
|
// c.TotalPrice -= voucher.Value
|
||||||
|
// c.TotalTax -= voucher.TaxValue
|
||||||
|
// c.TotalDiscountTax += voucher.TaxValue
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
50
cmd/cart/cart_grain_totals_test.go
Normal file
50
cmd/cart/cart_grain_totals_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,20 +64,20 @@ func BuildCheckoutOrderPayload(grain *CartGrain, meta *CheckoutMeta) ([]byte, *C
|
|||||||
lines = append(lines, &Line{
|
lines = append(lines, &Line{
|
||||||
Type: "physical",
|
Type: "physical",
|
||||||
Reference: it.Sku,
|
Reference: it.Sku,
|
||||||
Name: it.Name,
|
Name: it.Meta.Name,
|
||||||
Quantity: it.Quantity,
|
Quantity: it.Quantity,
|
||||||
UnitPrice: int(it.Price),
|
UnitPrice: int(it.Price.IncVat),
|
||||||
TaxRate: 2500, // TODO: derive if variable tax rates are introduced
|
TaxRate: 2500, // TODO: derive if variable tax rates are introduced
|
||||||
QuantityUnit: "st",
|
QuantityUnit: "st",
|
||||||
TotalAmount: int(it.TotalPrice),
|
TotalAmount: int(it.TotalPrice.IncVat),
|
||||||
TotalTaxAmount: int(it.TotalTax),
|
TotalTaxAmount: int(it.TotalPrice.TotalVat()),
|
||||||
ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", it.Image),
|
ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", it.Meta.Image),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delivery lines
|
// Delivery lines
|
||||||
for _, d := range grain.Deliveries {
|
for _, d := range grain.Deliveries {
|
||||||
if d == nil || d.Price <= 0 {
|
if d == nil || d.Price.IncVat <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lines = append(lines, &Line{
|
lines = append(lines, &Line{
|
||||||
@@ -85,11 +85,11 @@ func BuildCheckoutOrderPayload(grain *CartGrain, meta *CheckoutMeta) ([]byte, *C
|
|||||||
Reference: d.Provider,
|
Reference: d.Provider,
|
||||||
Name: "Delivery",
|
Name: "Delivery",
|
||||||
Quantity: 1,
|
Quantity: 1,
|
||||||
UnitPrice: int(d.Price),
|
UnitPrice: int(d.Price.IncVat),
|
||||||
TaxRate: 2500,
|
TaxRate: 2500,
|
||||||
QuantityUnit: "st",
|
QuantityUnit: "st",
|
||||||
TotalAmount: int(d.Price),
|
TotalAmount: int(d.Price.IncVat),
|
||||||
TotalTaxAmount: int(GetTaxAmount(d.Price, 2500)),
|
TotalTaxAmount: int(d.Price.TotalVat()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +97,8 @@ func BuildCheckoutOrderPayload(grain *CartGrain, meta *CheckoutMeta) ([]byte, *C
|
|||||||
PurchaseCountry: country,
|
PurchaseCountry: country,
|
||||||
PurchaseCurrency: currency,
|
PurchaseCurrency: currency,
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
OrderAmount: int(grain.TotalPrice),
|
OrderAmount: int(grain.TotalPrice.IncVat),
|
||||||
OrderTaxAmount: int(grain.TotalTax),
|
OrderTaxAmount: int(grain.TotalPrice.TotalVat()),
|
||||||
OrderLines: lines,
|
OrderLines: lines,
|
||||||
MerchantReference1: grain.Id.String(),
|
MerchantReference1: grain.Id.String(),
|
||||||
MerchantURLS: &CheckoutMerchantURLS{
|
MerchantURLS: &CheckoutMerchantURLS{
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ func main() {
|
|||||||
Deliveries: []*CartDelivery{},
|
Deliveries: []*CartDelivery{},
|
||||||
Id: CartId(id),
|
Id: CartId(id),
|
||||||
Items: []*CartItem{},
|
Items: []*CartItem{},
|
||||||
TotalPrice: 0,
|
TotalPrice: NewPrice(),
|
||||||
}
|
}
|
||||||
// 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.
|
||||||
ret.lastChange = time.Now()
|
ret.lastChange = time.Now()
|
||||||
|
|||||||
@@ -38,39 +38,50 @@ func AddItem(g *CartGrain, m *messages.AddItem) error {
|
|||||||
defer g.mu.Unlock()
|
defer g.mu.Unlock()
|
||||||
|
|
||||||
g.lastItemId++
|
g.lastItemId++
|
||||||
taxRate := 2500
|
taxRate := float32(25.0)
|
||||||
if m.Tax > 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{
|
g.Items = append(g.Items, &CartItem{
|
||||||
Id: g.lastItemId,
|
Id: g.lastItemId,
|
||||||
ItemId: int(m.ItemId),
|
ItemId: int(m.ItemId),
|
||||||
Quantity: int(m.Quantity),
|
Quantity: int(m.Quantity),
|
||||||
Sku: m.Sku,
|
Sku: m.Sku,
|
||||||
Name: m.Name,
|
Meta: ItemMeta{
|
||||||
Price: m.Price,
|
Name: m.Name,
|
||||||
TotalPrice: m.Price * int64(m.Quantity),
|
Image: m.Image,
|
||||||
TotalTax: int64(taxAmountPerUnit * int64(m.Quantity)),
|
Brand: m.Brand,
|
||||||
Image: m.Image,
|
Category: m.Category,
|
||||||
Stock: StockStatus(m.Stock),
|
Category2: m.Category2,
|
||||||
Disclaimer: m.Disclaimer,
|
Category3: m.Category3,
|
||||||
Brand: m.Brand,
|
Category4: m.Category4,
|
||||||
Category: m.Category,
|
Category5: m.Category5,
|
||||||
Category2: m.Category2,
|
Outlet: m.Outlet,
|
||||||
Category3: m.Category3,
|
SellerId: m.SellerId,
|
||||||
Category4: m.Category4,
|
SellerName: m.SellerName,
|
||||||
Category5: m.Category5,
|
},
|
||||||
OrgPrice: m.OrgPrice,
|
|
||||||
|
Price: *pricePerItem,
|
||||||
|
TotalPrice: *MultiplyPrice(*pricePerItem, int64(m.Quantity)),
|
||||||
|
|
||||||
|
Stock: StockStatus(m.Stock),
|
||||||
|
Disclaimer: m.Disclaimer,
|
||||||
|
|
||||||
|
OrgPrice: getOrgPrice(m.OrgPrice, taxRate),
|
||||||
ArticleType: m.ArticleType,
|
ArticleType: m.ArticleType,
|
||||||
Outlet: m.Outlet,
|
|
||||||
SellerId: m.SellerId,
|
StoreId: m.StoreId,
|
||||||
SellerName: m.SellerName,
|
|
||||||
Tax: int(taxAmountPerUnit),
|
|
||||||
TaxRate: taxRate,
|
|
||||||
StoreId: m.StoreId,
|
|
||||||
})
|
})
|
||||||
g.UpdateTotals()
|
g.UpdateTotals()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOrgPrice(orgPrice int64, taxRate float32) *Price {
|
||||||
|
if orgPrice <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return NewPriceFromIncVat(orgPrice, taxRate)
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func SetDelivery(g *CartGrain, m *messages.SetDelivery) error {
|
|||||||
Id: newId,
|
Id: newId,
|
||||||
Provider: m.Provider,
|
Provider: m.Provider,
|
||||||
PickupPoint: m.PickupPoint,
|
PickupPoint: m.PickupPoint,
|
||||||
Price: 4900, // TODO: externalize pricing
|
Price: *NewPriceFromIncVat(4900, 25.0),
|
||||||
Items: targetItems,
|
Items: targetItems,
|
||||||
})
|
})
|
||||||
g.mu.Unlock()
|
g.mu.Unlock()
|
||||||
|
|||||||
131
cmd/cart/price.go
Normal file
131
cmd/cart/price.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
122
cmd/cart/price_test.go
Normal file
122
cmd/cart/price_test.go
Normal file
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||||
"github.com/matst80/slask-finder/pkg/index"
|
"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)
|
err = json.NewDecoder(res.Body).Decode(&item)
|
||||||
return &item, err
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ package actor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
//"github.com/gogo/protobuf/proto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Grain[V any] interface {
|
type Grain[V any] interface {
|
||||||
GetId() uint64
|
GetId() uint64
|
||||||
//Apply(content proto.Message, isReplay bool) (*V, error)
|
|
||||||
GetLastAccess() time.Time
|
GetLastAccess() time.Time
|
||||||
GetLastChange() time.Time
|
GetLastChange() time.Time
|
||||||
GetCurrentState() (*V, error)
|
GetCurrentState() (*V, error)
|
||||||
|
|||||||
@@ -7,11 +7,12 @@
|
|||||||
package messages
|
package messages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
sync "sync"
|
sync "sync"
|
||||||
unsafe "unsafe"
|
unsafe "unsafe"
|
||||||
|
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -2,82 +2,76 @@ syntax = "proto3";
|
|||||||
package messages;
|
package messages;
|
||||||
option go_package = "git.tornberg.me/go-cart-actor/proto;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 ClearCartRequest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddItem {
|
message AddItem {
|
||||||
int64 item_id = 1;
|
int64 item_id = 1;
|
||||||
int32 quantity = 2;
|
int32 quantity = 2;
|
||||||
int64 price = 3;
|
int64 price = 3;
|
||||||
int64 orgPrice = 9;
|
int64 orgPrice = 9;
|
||||||
string sku = 4;
|
string sku = 4;
|
||||||
string name = 5;
|
string name = 5;
|
||||||
string image = 6;
|
string image = 6;
|
||||||
int32 stock = 7;
|
int32 stock = 7;
|
||||||
int32 tax = 8;
|
int32 tax = 8;
|
||||||
string brand = 13;
|
string brand = 13;
|
||||||
string category = 14;
|
string category = 14;
|
||||||
string category2 = 15;
|
string category2 = 15;
|
||||||
string category3 = 16;
|
string category3 = 16;
|
||||||
string category4 = 17;
|
string category4 = 17;
|
||||||
string category5 = 18;
|
string category5 = 18;
|
||||||
string disclaimer = 10;
|
string disclaimer = 10;
|
||||||
string articleType = 11;
|
string articleType = 11;
|
||||||
string sellerId = 19;
|
string sellerId = 19;
|
||||||
string sellerName = 20;
|
string sellerName = 20;
|
||||||
string country = 21;
|
string country = 21;
|
||||||
optional string outlet = 12;
|
optional string outlet = 12;
|
||||||
optional string storeId = 22;
|
optional string storeId = 22;
|
||||||
|
optional uint32 parentId = 23;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RemoveItem {
|
message RemoveItem {
|
||||||
int64 Id = 1;
|
int64 Id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ChangeQuantity {
|
message ChangeQuantity {
|
||||||
int64 id = 1;
|
int64 id = 1;
|
||||||
int32 quantity = 2;
|
int32 quantity = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetDelivery {
|
message SetDelivery {
|
||||||
string provider = 1;
|
string provider = 1;
|
||||||
repeated int64 items = 2;
|
repeated int64 items = 2;
|
||||||
optional PickupPoint pickupPoint = 3;
|
optional PickupPoint pickupPoint = 3;
|
||||||
string country = 4;
|
string country = 4;
|
||||||
string zip = 5;
|
string zip = 5;
|
||||||
optional string address = 6;
|
optional string address = 6;
|
||||||
optional string city = 7;
|
optional string city = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetPickupPoint {
|
message SetPickupPoint {
|
||||||
int64 deliveryId = 1;
|
int64 deliveryId = 1;
|
||||||
string id = 2;
|
string id = 2;
|
||||||
optional string name = 3;
|
optional string name = 3;
|
||||||
optional string address = 4;
|
optional string address = 4;
|
||||||
optional string city = 5;
|
optional string city = 5;
|
||||||
optional string zip = 6;
|
optional string zip = 6;
|
||||||
optional string country = 7;
|
optional string country = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PickupPoint {
|
message PickupPoint {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
optional string name = 2;
|
optional string name = 2;
|
||||||
optional string address = 3;
|
optional string address = 3;
|
||||||
optional string city = 4;
|
optional string city = 4;
|
||||||
optional string zip = 5;
|
optional string zip = 5;
|
||||||
optional string country = 6;
|
optional string country = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RemoveDelivery {
|
message RemoveDelivery {
|
||||||
int64 id = 1;
|
int64 id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateCheckoutOrder {
|
message CreateCheckoutOrder {
|
||||||
@@ -90,18 +84,18 @@ message CreateCheckoutOrder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message OrderCreated {
|
message OrderCreated {
|
||||||
string orderId = 1;
|
string orderId = 1;
|
||||||
string status = 2;
|
string status = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Noop {
|
message Noop {
|
||||||
// Intentionally empty - used for ownership acquisition or health pings
|
// Intentionally empty - used for ownership acquisition or health pings
|
||||||
}
|
}
|
||||||
|
|
||||||
message InitializeCheckout {
|
message InitializeCheckout {
|
||||||
string orderId = 1;
|
string orderId = 1;
|
||||||
string status = 2;
|
string status = 2;
|
||||||
bool paymentInProgress = 3;
|
bool paymentInProgress = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddVoucher {
|
message AddVoucher {
|
||||||
|
|||||||
Reference in New Issue
Block a user