606 lines
15 KiB
Go
606 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
messages "git.tornberg.me/go-cart-actor/proto"
|
|
)
|
|
|
|
type CartId [16]byte
|
|
|
|
func (id CartId) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(id.String())
|
|
}
|
|
|
|
func (id *CartId) UnmarshalJSON(data []byte) error {
|
|
var str string
|
|
err := json.Unmarshal(data, &str)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
copy(id[:], []byte(str))
|
|
return nil
|
|
}
|
|
|
|
type StockStatus int
|
|
|
|
const (
|
|
OutOfStock StockStatus = 0
|
|
LowStock StockStatus = 1
|
|
InStock StockStatus = 2
|
|
)
|
|
|
|
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"`
|
|
}
|
|
|
|
type CartDelivery struct {
|
|
Id int `json:"id"`
|
|
Provider string `json:"provider"`
|
|
Price int64 `json:"price"`
|
|
Items []int `json:"items"`
|
|
PickupPoint *messages.PickupPoint `json:"pickupPoint,omitempty"`
|
|
}
|
|
|
|
type CartGrain struct {
|
|
mu sync.RWMutex
|
|
lastItemId int
|
|
lastDeliveryId int
|
|
storageMessages []Message
|
|
Id CartId `json:"id"`
|
|
Items []*CartItem `json:"items"`
|
|
TotalPrice int64 `json:"totalPrice"`
|
|
TotalTax int64 `json:"totalTax"`
|
|
TotalDiscount int64 `json:"totalDiscount"`
|
|
Deliveries []*CartDelivery `json:"deliveries,omitempty"`
|
|
Processing bool `json:"processing"`
|
|
PaymentInProgress bool `json:"paymentInProgress"`
|
|
OrderReference string `json:"orderReference,omitempty"`
|
|
PaymentStatus string `json:"paymentStatus,omitempty"`
|
|
}
|
|
|
|
type Grain interface {
|
|
GetId() CartId
|
|
HandleMessage(message *Message, isReplay bool) (*FrameWithPayload, error)
|
|
GetCurrentState() (*FrameWithPayload, error)
|
|
}
|
|
|
|
func (c *CartGrain) GetId() CartId {
|
|
return c.Id
|
|
}
|
|
|
|
func (c *CartGrain) GetLastChange() int64 {
|
|
if len(c.storageMessages) == 0 {
|
|
return 0
|
|
}
|
|
return *c.storageMessages[len(c.storageMessages)-1].TimeStamp
|
|
}
|
|
|
|
func (c *CartGrain) GetCurrentState() (*FrameWithPayload, error) {
|
|
result, err := json.Marshal(c)
|
|
if err != nil {
|
|
ret := MakeFrameWithPayload(0, 400, []byte(err.Error()))
|
|
return &ret, nil
|
|
}
|
|
ret := MakeFrameWithPayload(0, 200, result)
|
|
return &ret, nil
|
|
}
|
|
|
|
func getInt(data interface{}) (int, error) {
|
|
switch v := data.(type) {
|
|
case float64:
|
|
return int(v), nil
|
|
case int:
|
|
return v, nil
|
|
default:
|
|
return 0, fmt.Errorf("invalid type")
|
|
}
|
|
}
|
|
|
|
func getItemData(sku string, qty int, country string) (*messages.AddItem, error) {
|
|
item, err := FetchItem(sku, country)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
orgPrice, _ := getInt(item.Fields[5])
|
|
|
|
price, priceErr := getInt(item.Fields[4])
|
|
|
|
if priceErr != nil {
|
|
return nil, fmt.Errorf("invalid price")
|
|
}
|
|
|
|
stock := InStock
|
|
if item.StockLevel == "0" || item.StockLevel == "" {
|
|
stock = OutOfStock
|
|
} else if item.StockLevel == "5+" {
|
|
stock = LowStock
|
|
}
|
|
articleType, _ := item.Fields[1].(string)
|
|
outletGrade, ok := item.Fields[20].(string)
|
|
var outlet *string
|
|
if ok {
|
|
outlet = &outletGrade
|
|
}
|
|
sellerId, _ := item.Fields[24].(string)
|
|
sellerName, _ := item.Fields[9].(string)
|
|
|
|
brand, _ := item.Fields[2].(string)
|
|
category, _ := item.Fields[10].(string)
|
|
category2, _ := item.Fields[11].(string)
|
|
category3, _ := item.Fields[12].(string)
|
|
category4, _ := item.Fields[13].(string)
|
|
category5, _ := item.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,
|
|
}, nil
|
|
}
|
|
|
|
func (c *CartGrain) AddItem(sku string, qty int, country string) (*FrameWithPayload, error) {
|
|
cartItem, err := getItemData(sku, qty, country)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.HandleMessage(&Message{
|
|
Type: 2,
|
|
Content: cartItem,
|
|
}, false)
|
|
}
|
|
|
|
func (c *CartGrain) GetStorageMessage(since int64) []StorableMessage {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
ret := make([]StorableMessage, 0)
|
|
|
|
for _, message := range c.storageMessages {
|
|
if *message.TimeStamp > since {
|
|
ret = append(ret, message)
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (c *CartGrain) GetState() ([]byte, error) {
|
|
return json.Marshal(c)
|
|
}
|
|
|
|
func (c *CartGrain) ItemsWithDelivery() []int {
|
|
ret := make([]int, 0, len(c.Items))
|
|
for _, item := range c.Items {
|
|
for _, delivery := range c.Deliveries {
|
|
for _, id := range delivery.Items {
|
|
if item.Id == id {
|
|
ret = append(ret, id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (c *CartGrain) ItemsWithoutDelivery() []int {
|
|
ret := make([]int, 0, len(c.Items))
|
|
hasDelivery := c.ItemsWithDelivery()
|
|
for _, item := range c.Items {
|
|
found := false
|
|
for _, id := range hasDelivery {
|
|
if item.Id == id {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
ret = append(ret, item.Id)
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (c *CartGrain) FindItemWithSku(sku string) (*CartItem, bool) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
for _, item := range c.Items {
|
|
if item.Sku == sku {
|
|
return item, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func GetTaxAmount(total int64, tax int) int64 {
|
|
taxD := 10000 / float64(tax)
|
|
return int64(float64(total) / float64((1 + taxD)))
|
|
}
|
|
|
|
func (c *CartGrain) HandleMessage(message *Message, isReplay bool) (*FrameWithPayload, error) {
|
|
if message.TimeStamp == nil {
|
|
now := time.Now().Unix()
|
|
message.TimeStamp = &now
|
|
}
|
|
grainMutations.Inc()
|
|
var err error
|
|
switch message.Type {
|
|
case SetCartItemsType:
|
|
msg, ok := message.Content.(*messages.SetCartRequest)
|
|
if !ok {
|
|
err = fmt.Errorf("expected SetCartItems")
|
|
} else {
|
|
|
|
c.mu.Lock()
|
|
c.Items = make([]*CartItem, 0, len(msg.Items))
|
|
c.mu.Unlock()
|
|
for _, item := range msg.Items {
|
|
c.AddItem(item.Sku, int(item.Quantity), item.Country)
|
|
}
|
|
|
|
}
|
|
case AddRequestType:
|
|
msg, ok := message.Content.(*messages.AddRequest)
|
|
if !ok {
|
|
err = fmt.Errorf("expected AddRequest")
|
|
} else {
|
|
|
|
existingItem, found := c.FindItemWithSku(msg.Sku)
|
|
if found {
|
|
existingItem.Quantity += int(msg.Quantity)
|
|
c.UpdateTotals()
|
|
} else {
|
|
return c.AddItem(msg.Sku, int(msg.Quantity), msg.Country)
|
|
}
|
|
|
|
}
|
|
case AddItemType:
|
|
msg, ok := message.Content.(*messages.AddItem)
|
|
if !ok {
|
|
err = fmt.Errorf("expected AddItem")
|
|
} else {
|
|
|
|
if msg.Quantity < 1 {
|
|
return nil, fmt.Errorf("invalid quantity")
|
|
}
|
|
existingItem, found := c.FindItemWithSku(msg.Sku)
|
|
if found {
|
|
existingItem.Quantity += int(msg.Quantity)
|
|
c.UpdateTotals()
|
|
} else {
|
|
c.mu.Lock()
|
|
c.lastItemId++
|
|
tax := 2500
|
|
if msg.Tax > 0 {
|
|
tax = int(msg.Tax)
|
|
}
|
|
|
|
taxAmount := GetTaxAmount(msg.Price, tax)
|
|
|
|
c.Items = append(c.Items, &CartItem{
|
|
Id: c.lastItemId,
|
|
ItemId: int(msg.ItemId),
|
|
Quantity: int(msg.Quantity),
|
|
Sku: msg.Sku,
|
|
Name: msg.Name,
|
|
Price: msg.Price,
|
|
TotalPrice: msg.Price * int64(msg.Quantity),
|
|
TotalTax: int64(taxAmount * int64(msg.Quantity)),
|
|
Image: msg.Image,
|
|
Stock: StockStatus(msg.Stock),
|
|
Disclaimer: msg.Disclaimer,
|
|
Brand: msg.Brand,
|
|
Category: msg.Category,
|
|
Category2: msg.Category2,
|
|
Category3: msg.Category3,
|
|
Category4: msg.Category4,
|
|
Category5: msg.Category5,
|
|
OrgPrice: msg.OrgPrice,
|
|
ArticleType: msg.ArticleType,
|
|
Outlet: msg.Outlet,
|
|
SellerId: msg.SellerId,
|
|
SellerName: msg.SellerName,
|
|
Tax: int(taxAmount),
|
|
TaxRate: tax,
|
|
})
|
|
c.UpdateTotals()
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
}
|
|
case ChangeQuantityType:
|
|
msg, ok := message.Content.(*messages.ChangeQuantity)
|
|
if !ok {
|
|
err = fmt.Errorf("expected ChangeQuantity")
|
|
} else {
|
|
|
|
for i, item := range c.Items {
|
|
if item.Id == int(msg.Id) {
|
|
if msg.Quantity <= 0 {
|
|
//c.TotalPrice -= item.Price * int64(item.Quantity)
|
|
c.Items = append(c.Items[:i], c.Items[i+1:]...)
|
|
} else {
|
|
//diff := int(msg.Quantity) - item.Quantity
|
|
item.Quantity = int(msg.Quantity)
|
|
//c.TotalPrice += item.Price * int64(diff)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
c.UpdateTotals()
|
|
|
|
}
|
|
case RemoveItemType:
|
|
msg, ok := message.Content.(*messages.RemoveItem)
|
|
if !ok {
|
|
err = fmt.Errorf("expected RemoveItem")
|
|
} else {
|
|
|
|
items := make([]*CartItem, 0, len(c.Items))
|
|
for _, item := range c.Items {
|
|
if item.Id == int(msg.Id) {
|
|
//c.TotalPrice -= item.Price * int64(item.Quantity)
|
|
} else {
|
|
items = append(items, item)
|
|
}
|
|
}
|
|
c.Items = items
|
|
c.UpdateTotals()
|
|
}
|
|
case SetDeliveryType:
|
|
msg, ok := message.Content.(*messages.SetDelivery)
|
|
if !ok {
|
|
err = fmt.Errorf("expected SetDelivery")
|
|
} else {
|
|
|
|
c.lastDeliveryId++
|
|
items := make([]int, 0)
|
|
withDelivery := c.ItemsWithDelivery()
|
|
if len(msg.Items) == 0 {
|
|
items = append(items, c.ItemsWithoutDelivery()...)
|
|
} else {
|
|
for _, id := range msg.Items {
|
|
for _, item := range c.Items {
|
|
if item.Id == int(id) {
|
|
if slices.Contains(withDelivery, item.Id) {
|
|
return nil, fmt.Errorf("item already has delivery")
|
|
}
|
|
items = append(items, int(item.Id))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(items) > 0 {
|
|
c.Deliveries = append(c.Deliveries, &CartDelivery{
|
|
Id: c.lastDeliveryId,
|
|
Provider: msg.Provider,
|
|
PickupPoint: msg.PickupPoint,
|
|
Price: 4900,
|
|
Items: items,
|
|
})
|
|
|
|
c.UpdateTotals()
|
|
|
|
}
|
|
|
|
}
|
|
case RemoveDeliveryType:
|
|
msg, ok := message.Content.(*messages.RemoveDelivery)
|
|
if !ok {
|
|
err = fmt.Errorf("expected RemoveDelivery")
|
|
} else {
|
|
|
|
deliveries := make([]*CartDelivery, 0, len(c.Deliveries))
|
|
for _, delivery := range c.Deliveries {
|
|
if delivery.Id == int(msg.Id) {
|
|
c.TotalPrice -= delivery.Price
|
|
} else {
|
|
deliveries = append(deliveries, delivery)
|
|
}
|
|
}
|
|
c.Deliveries = deliveries
|
|
c.UpdateTotals()
|
|
}
|
|
case SetPickupPointType:
|
|
msg, ok := message.Content.(*messages.SetPickupPoint)
|
|
if !ok {
|
|
err = fmt.Errorf("expected SetPickupPoint")
|
|
} else {
|
|
|
|
for _, delivery := range c.Deliveries {
|
|
if delivery.Id == int(msg.DeliveryId) {
|
|
delivery.PickupPoint = &messages.PickupPoint{
|
|
Id: msg.Id,
|
|
Address: msg.Address,
|
|
City: msg.City,
|
|
Zip: msg.Zip,
|
|
Country: msg.Country,
|
|
Name: msg.Name,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
}
|
|
case CreateCheckoutOrderType:
|
|
msg, ok := message.Content.(*messages.CreateCheckoutOrder)
|
|
if !ok {
|
|
err = fmt.Errorf("expected CreateCheckoutOrder")
|
|
} else {
|
|
|
|
orderLines := make([]*Line, 0, len(c.Items))
|
|
|
|
c.PaymentInProgress = true
|
|
c.Processing = true
|
|
for _, item := range c.Items {
|
|
|
|
orderLines = append(orderLines, &Line{
|
|
Type: "physical",
|
|
Reference: item.Sku,
|
|
Name: item.Name,
|
|
Quantity: item.Quantity,
|
|
UnitPrice: int(item.Price),
|
|
TaxRate: 2500, // item.TaxRate,
|
|
QuantityUnit: "st",
|
|
TotalAmount: int(item.TotalPrice),
|
|
TotalTaxAmount: int(item.TotalTax),
|
|
ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", item.Image),
|
|
})
|
|
}
|
|
for _, line := range c.Deliveries {
|
|
if line.Price > 0 {
|
|
orderLines = append(orderLines, &Line{
|
|
Type: "shipping_fee",
|
|
Reference: line.Provider,
|
|
Name: "Delivery",
|
|
Quantity: 1,
|
|
UnitPrice: int(line.Price),
|
|
TaxRate: 2500, // item.TaxRate,
|
|
QuantityUnit: "st",
|
|
TotalAmount: int(line.Price),
|
|
TotalTaxAmount: int(GetTaxAmount(line.Price, 2500)),
|
|
})
|
|
}
|
|
}
|
|
order := CheckoutOrder{
|
|
PurchaseCountry: "SE",
|
|
PurchaseCurrency: "SEK",
|
|
Locale: "sv-se",
|
|
OrderAmount: int(c.TotalPrice),
|
|
OrderTaxAmount: int(c.TotalTax),
|
|
OrderLines: orderLines,
|
|
MerchantReference1: c.Id.String(),
|
|
MerchantURLS: &CheckoutMerchantURLS{
|
|
Terms: msg.Terms,
|
|
Checkout: msg.Checkout,
|
|
Confirmation: msg.Confirmation,
|
|
Validation: msg.Validation,
|
|
Push: msg.Push,
|
|
},
|
|
}
|
|
orderPayload, err := json.Marshal(order)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var klarnaOrder *CheckoutOrder
|
|
if c.OrderReference != "" {
|
|
log.Printf("Updating order id %s", c.OrderReference)
|
|
klarnaOrder, err = KlarnaInstance.UpdateOrder(c.OrderReference, bytes.NewReader(orderPayload))
|
|
} else {
|
|
klarnaOrder, err = KlarnaInstance.CreateOrder(bytes.NewReader(orderPayload))
|
|
}
|
|
// log.Printf("Order result: %+v", klarnaOrder)
|
|
if nil != err {
|
|
log.Printf("error from klarna: %v", err)
|
|
return nil, err
|
|
}
|
|
if c.OrderReference == "" {
|
|
c.OrderReference = klarnaOrder.ID
|
|
c.PaymentStatus = klarnaOrder.Status
|
|
}
|
|
|
|
orderData, err := json.Marshal(klarnaOrder)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
|
|
result := MakeFrameWithPayload(RemoteCreateOrderReply, 200, orderData)
|
|
return &result, nil
|
|
}
|
|
case OrderCompletedType:
|
|
msg, ok := message.Content.(*messages.OrderCreated)
|
|
if !ok {
|
|
log.Printf("expected OrderCompleted, got %T", message.Content)
|
|
err = fmt.Errorf("expected OrderCompleted")
|
|
} else {
|
|
c.OrderReference = msg.OrderId
|
|
c.PaymentStatus = msg.Status
|
|
c.PaymentInProgress = false
|
|
}
|
|
default:
|
|
err = fmt.Errorf("unknown message type %d", message.Type)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !isReplay {
|
|
c.mu.Lock()
|
|
c.storageMessages = append(c.storageMessages, *message)
|
|
c.mu.Unlock()
|
|
}
|
|
result, err := json.Marshal(c)
|
|
msg := MakeFrameWithPayload(RemoteHandleMutationReply, 200, result)
|
|
return &msg, err
|
|
}
|
|
|
|
func (c *CartGrain) UpdateTotals() {
|
|
c.TotalPrice = 0
|
|
c.TotalTax = 0
|
|
c.TotalDiscount = 0
|
|
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)
|
|
}
|
|
for _, delivery := range c.Deliveries {
|
|
c.TotalPrice += delivery.Price
|
|
c.TotalTax += GetTaxAmount(delivery.Price, 2500)
|
|
}
|
|
|
|
}
|