package main import ( "encoding/json" "fmt" "slices" "sync" "time" messages "git.tornberg.me/go-cart-actor/proto" klarna "github.com/Flaconi/go-klarna" ) 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"` ParentId int `json:"parentId,omitempty"` Sku string `json:"sku"` Name string `json:"name"` Price int64 `json:"price"` OrgPrice int64 `json:"orgPrice"` Stock StockStatus `json:"stock"` Quantity int `json:"qty"` Tax int `json:"tax"` Disclaimer string `json:"disclaimer,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"` Deliveries []*CartDelivery `json:"deliveries,omitempty"` Processing bool `json:"processing"` PaymentInProgress bool `json:"paymentInProgress"` OrderReference string `json:"orderReference,omitempty"` PaymentReference string `json:"paymentReference,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) (*messages.AddItem, error) { item, err := FetchItem(sku) 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 } return &messages.AddItem{ Quantity: int32(qty), Price: int64(price), OrgPrice: int64(orgPrice), Sku: sku, Name: item.Title, Image: item.Img, Stock: int32(stock), Tax: 2500, ArticleType: articleType, Disclaimer: item.Disclaimer, Outlet: outlet, }, nil } func (c *CartGrain) AddItem(sku string, qty int) (*FrameWithPayload, error) { cartItem, err := getItemData(sku, qty) 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 int, tax int) int { taxD := 10000 / float64(tax) return int(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)) } } 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.TotalPrice += existingItem.Price * int64(msg.Quantity) } else { return c.AddItem(msg.Sku, int(msg.Quantity)) // extent AddRequest to include quantity } } 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.TotalPrice += existingItem.Price * int64(msg.Quantity) } else { c.mu.Lock() c.lastItemId++ tax := 2500 if msg.Tax > 0 { tax = int(msg.Tax) } c.Items = append(c.Items, &CartItem{ Id: c.lastItemId, Quantity: int(msg.Quantity), Sku: msg.Sku, Name: msg.Name, Price: msg.Price, Image: msg.Image, Stock: StockStatus(msg.Stock), Disclaimer: msg.Disclaimer, OrgPrice: msg.OrgPrice, ArticleType: msg.ArticleType, Outlet: msg.Outlet, Tax: tax, }) c.TotalPrice += msg.Price * int64(msg.Quantity) 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 } } } 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 } 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, Price: 49, Items: items, }) c.Processing = true go func() { time.Sleep(5 * time.Second) c.Processing = false }() } } 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 } 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([]*klarna.Line, 0, len(c.Items)) totalTax := 0 c.PaymentInProgress = true c.Processing = true for _, item := range c.Items { total := int(item.Price) * item.Quantity taxAmount := GetTaxAmount(total, item.Tax) totalTax += taxAmount orderLines = append(orderLines, &klarna.Line{ Type: "physical", Reference: item.Sku, Name: item.Name, Quantity: item.Quantity, UnitPrice: int(item.Price), TaxRate: int(item.Tax), QuantityUnit: "st", TotalAmount: total, TotalTaxAmount: taxAmount, ImageURL: item.Image, }) } order := klarna.CheckoutOrder{ PurchaseCountry: "SE", PurchaseCurrency: "SEK", Locale: "sv-se", OrderAmount: int(c.TotalPrice), OrderTaxAmount: totalTax, OrderLines: orderLines, MerchantReference1: c.Id.String(), MerchantURLS: &klarna.CheckoutMerchantURLS{ Terms: msg.Terms, Checkout: msg.Checkout, Confirmation: msg.Confirmation, Push: msg.Push, }, } orderPayload, err := json.Marshal(order) if err != nil { return nil, err } result := MakeFrameWithPayload(RemoteCreateOrderReply, 200, orderPayload) return &result, nil } case OrderCompletedType: msg, ok := message.Content.(*messages.OrderCreated) if !ok { err = fmt.Errorf("expected OrderCompleted") } else { c.OrderReference = msg.OrderId c.PaymentReference = msg.Status } 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 }