better
This commit is contained in:
@@ -100,7 +100,7 @@ func getInt(data float64, ok bool) (int, error) {
|
||||
return int(data), nil
|
||||
}
|
||||
|
||||
func getItemData(sku string, qty int, country string) (*messages.AddItem, error) {
|
||||
func GetItemAddMessage(sku string, qty int, country string, storeId *string) (*messages.AddItem, error) {
|
||||
item, err := FetchItem(sku, country)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -162,17 +162,18 @@ func getItemData(sku string, qty int, country string) (*messages.AddItem, error)
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
cartItem.StoreId = storeId
|
||||
return c.Apply(cartItem, false)
|
||||
}
|
||||
// func (c *CartGrain) AddItem(sku string, qty int, country string, storeId *string) (*CartGrain, error) {
|
||||
// cartItem, err := getItemData(sku, qty, country)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// cartItem.StoreId = storeId
|
||||
// return c.Apply(cartItem, false)
|
||||
// }
|
||||
|
||||
func (c *CartGrain) GetState() ([]byte, error) {
|
||||
return json.Marshal(c)
|
||||
@@ -221,25 +222,25 @@ func GetTaxAmount(total int64, tax int) int64 {
|
||||
return int64(float64(total) / float64((1 + taxD)))
|
||||
}
|
||||
|
||||
func (c *CartGrain) Apply(content interface{}, isReplay bool) (*CartGrain, error) {
|
||||
// func (c *CartGrain) Apply(content proto.Message, isReplay bool) (*CartGrain, error) {
|
||||
|
||||
updated, err := ApplyRegistered(c, content)
|
||||
if err != nil {
|
||||
if err == ErrMutationNotRegistered {
|
||||
return nil, fmt.Errorf("unsupported mutation type %T (not registered)", content)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// updated, err := ApplyRegistered(c, content)
|
||||
// if err != nil {
|
||||
// if err == ErrMutationNotRegistered {
|
||||
// return nil, fmt.Errorf("unsupported mutation type %T (not registered)", content)
|
||||
// }
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// Sliding TTL: update lastChange only for non-replay successful mutations.
|
||||
if updated != nil && !isReplay {
|
||||
c.lastChange = time.Now()
|
||||
c.lastAccess = time.Now()
|
||||
go AppendCartEvent(c.Id, content)
|
||||
}
|
||||
// // Sliding TTL: update lastChange only for non-replay successful mutations.
|
||||
// if updated != nil && !isReplay {
|
||||
// c.lastChange = time.Now()
|
||||
// c.lastAccess = time.Now()
|
||||
// go AppendCartEvent(c.Id, content)
|
||||
// }
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
// return updated, nil
|
||||
// }
|
||||
|
||||
func (c *CartGrain) UpdateTotals() {
|
||||
c.TotalPrice = 0
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
gob.Register(map[uint64]int64{})
|
||||
}
|
||||
|
||||
type DiskStorage struct {
|
||||
stateFile string
|
||||
lastSave time.Time
|
||||
LastSaves map[uint64]time.Time
|
||||
}
|
||||
|
||||
func NewDiskStorage(stateFile string) (*DiskStorage, error) {
|
||||
ret := &DiskStorage{
|
||||
stateFile: stateFile,
|
||||
LastSaves: make(map[uint64]time.Time),
|
||||
}
|
||||
//err := ret.loadState()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// func saveMessages(_ interface{}, _ CartId) error {
|
||||
// // No-op: legacy event log persistence removed in oneof refactor.
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// func getCartPath(id string) string {
|
||||
// return fmt.Sprintf("data/%s.prot", id)
|
||||
// }
|
||||
|
||||
// func loadMessages(_ Grain, _ CartId) error {
|
||||
// // No-op: legacy replay removed in oneof refactor.
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// func (s *DiskStorage) saveState() error {
|
||||
// tmpFile := s.stateFile + "_tmp"
|
||||
// file, err := os.Create(tmpFile)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer file.Close()
|
||||
// err = gob.NewEncoder(file).Encode(s.LastSaves)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// os.Remove(s.stateFile + ".bak")
|
||||
// os.Rename(s.stateFile, s.stateFile+".bak")
|
||||
// return os.Rename(tmpFile, s.stateFile)
|
||||
// }
|
||||
|
||||
// func (s *DiskStorage) loadState() error {
|
||||
// file, err := os.Open(s.stateFile)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer file.Close()
|
||||
// return gob.NewDecoder(file).Decode(&s.LastSaves)
|
||||
// }
|
||||
|
||||
func (s *DiskStorage) Store(id CartId, _ *CartGrain) error {
|
||||
// With the removal of the legacy message log, we only update the timestamp.
|
||||
ts := time.Now()
|
||||
s.LastSaves[uint64(id)] = ts
|
||||
s.lastSave = ts
|
||||
return nil
|
||||
}
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
@@ -205,7 +207,7 @@ func AppendCartEvent(id CartId, mutation interface{}) error {
|
||||
|
||||
// ReplayCartEvents replays an existing cart's event log into the provided grain.
|
||||
// It applies mutation payloads in order, skipping unknown types.
|
||||
func ReplayCartEvents(grain *CartGrain, id CartId) error {
|
||||
func ReplayCartEvents(grain *CartGrain, id CartId, registry actor.MutationRegistry) error {
|
||||
start := time.Now()
|
||||
path := EventLogPath(id)
|
||||
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
||||
@@ -237,18 +239,19 @@ func ReplayCartEvents(grain *CartGrain, id CartId) error {
|
||||
eventReplayFailuresTotal.Inc()
|
||||
continue // skip malformed line
|
||||
}
|
||||
factory, ok := eventTypeFactories[raw.Type]
|
||||
|
||||
instance, ok := registry.Create(raw.Type)
|
||||
if !ok {
|
||||
eventUnknownTypesTotal.Inc()
|
||||
continue // skip unknown mutation type
|
||||
log.Printf("loading failed for unknown mutation type: %s", raw.Type)
|
||||
eventReplayFailuresTotal.Inc()
|
||||
continue // skip unknown type
|
||||
}
|
||||
instance := factory()
|
||||
if err := json.Unmarshal(raw.Payload, instance); err != nil {
|
||||
eventMutationErrorsTotal.Inc()
|
||||
continue
|
||||
}
|
||||
// Apply mutation directly using internal registration (bypass AppendCartEvent recursion).
|
||||
if _, applyErr := ApplyRegistered(grain, instance); applyErr != nil {
|
||||
if applyErr := registry.Apply(grain, instance); applyErr != nil {
|
||||
eventMutationErrorsTotal.Inc()
|
||||
continue
|
||||
} else {
|
||||
|
||||
@@ -39,34 +39,12 @@ var (
|
||||
})
|
||||
)
|
||||
|
||||
func spawn(id uint64) (actor.Grain[CartGrain], error) {
|
||||
grainSpawns.Inc()
|
||||
ret := &CartGrain{
|
||||
lastItemId: 0,
|
||||
lastDeliveryId: 0,
|
||||
Deliveries: []*CartDelivery{},
|
||||
Id: CartId(id),
|
||||
Items: []*CartItem{},
|
||||
TotalPrice: 0,
|
||||
}
|
||||
// Set baseline lastChange at spawn; replay may update it to last event timestamp.
|
||||
ret.lastChange = time.Now()
|
||||
ret.lastAccess = time.Now()
|
||||
|
||||
// Legacy loadMessages (no-op) retained; then replay append-only event log
|
||||
//_ = loadMessages(ret, id)
|
||||
err := ReplayCartEvents(ret, CartId(id))
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func init() {
|
||||
os.Mkdir("data", 0755)
|
||||
}
|
||||
|
||||
type App struct {
|
||||
pool *actor.SimpleGrainPool[CartGrain]
|
||||
storage *DiskStorage
|
||||
pool *actor.SimpleGrainPool[CartGrain]
|
||||
}
|
||||
|
||||
var podIp = os.Getenv("POD_IP")
|
||||
@@ -91,7 +69,10 @@ func getCountryFromHost(host string) string {
|
||||
if strings.Contains(strings.ToLower(host), "-no") {
|
||||
return "no"
|
||||
}
|
||||
return "se"
|
||||
if strings.Contains(strings.ToLower(host), "-se") {
|
||||
return "se"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetDiscovery() discovery.Discovery {
|
||||
@@ -112,21 +93,71 @@ func GetDiscovery() discovery.Discovery {
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
controlPlaneConfig := actor.DefaultServerConfig()
|
||||
storage, err := NewDiskStorage(fmt.Sprintf("data/s_%s.gob", name))
|
||||
if err != nil {
|
||||
log.Printf("Error loading state: %v\n", err)
|
||||
|
||||
reg := actor.NewMutationRegistry()
|
||||
reg.RegisterMutations(
|
||||
actor.NewMutation(AddItem, func() *messages.AddItem {
|
||||
return &messages.AddItem{}
|
||||
}),
|
||||
actor.NewMutation(ChangeQuantity, func() *messages.ChangeQuantity {
|
||||
return &messages.ChangeQuantity{}
|
||||
}),
|
||||
actor.NewMutation(RemoveItem, func() *messages.RemoveItem {
|
||||
return &messages.RemoveItem{}
|
||||
}),
|
||||
actor.NewMutation(InitializeCheckout, func() *messages.InitializeCheckout {
|
||||
return &messages.InitializeCheckout{}
|
||||
}),
|
||||
actor.NewMutation(RemoveDelivery, func() *messages.RemoveDelivery {
|
||||
return &messages.RemoveDelivery{}
|
||||
}),
|
||||
actor.NewMutation(SetDelivery, func() *messages.SetDelivery {
|
||||
return &messages.SetDelivery{}
|
||||
}),
|
||||
actor.NewMutation(SetPickupPoint, func() *messages.SetPickupPoint {
|
||||
return &messages.SetPickupPoint{}
|
||||
}),
|
||||
)
|
||||
diskStorage := actor.NewDiskStorage[CartGrain]("data", reg)
|
||||
poolConfig := actor.GrainPoolConfig[CartGrain]{
|
||||
MutationRegistry: reg,
|
||||
Storage: diskStorage,
|
||||
Spawn: func(id uint64) (actor.Grain[CartGrain], error) {
|
||||
grainSpawns.Inc()
|
||||
ret := &CartGrain{
|
||||
lastItemId: 0,
|
||||
lastDeliveryId: 0,
|
||||
Deliveries: []*CartDelivery{},
|
||||
Id: CartId(id),
|
||||
Items: []*CartItem{},
|
||||
TotalPrice: 0,
|
||||
}
|
||||
// Set baseline lastChange at spawn; replay may update it to last event timestamp.
|
||||
ret.lastChange = time.Now()
|
||||
ret.lastAccess = time.Now()
|
||||
|
||||
// Legacy loadMessages (no-op) retained; then replay append-only event log
|
||||
//_ = loadMessages(ret, id)
|
||||
err := diskStorage.LoadEvents(id, ret)
|
||||
|
||||
return ret, err
|
||||
},
|
||||
SpawnHost: func(host string) (actor.Host, error) {
|
||||
return proxy.NewRemoteHost(host)
|
||||
},
|
||||
TTL: 15 * time.Minute,
|
||||
PoolSize: 2 * 65535,
|
||||
Hostname: podIp,
|
||||
}
|
||||
|
||||
pool, err := actor.NewSimpleGrainPool(2*65535, 15*time.Minute, podIp, spawn, func(host string) (actor.Host, error) {
|
||||
return proxy.NewRemoteHost(host)
|
||||
})
|
||||
pool, err := actor.NewSimpleGrainPool(poolConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating cart pool: %v\n", err)
|
||||
}
|
||||
app := &App{
|
||||
pool: pool,
|
||||
storage: storage,
|
||||
pool: pool,
|
||||
}
|
||||
|
||||
grpcSrv, err := actor.NewControlServer[*CartGrain](controlPlaneConfig, pool)
|
||||
|
||||
@@ -20,63 +20,57 @@ import (
|
||||
// NOTE: Any future field additions in messages.AddItem that affect pricing / tax
|
||||
// must keep this handler in sync.
|
||||
|
||||
func init() {
|
||||
RegisterMutation[messages.AddItem](
|
||||
"AddItem",
|
||||
func(g *CartGrain, m *messages.AddItem) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("AddItem: nil payload")
|
||||
}
|
||||
if m.Quantity < 1 {
|
||||
return fmt.Errorf("AddItem: invalid quantity %d", m.Quantity)
|
||||
}
|
||||
func AddItem(g *CartGrain, m *messages.AddItem) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("AddItem: nil payload")
|
||||
}
|
||||
if m.Quantity < 1 {
|
||||
return fmt.Errorf("AddItem: invalid quantity %d", m.Quantity)
|
||||
}
|
||||
|
||||
// Fast path: merge with existing item having same SKU
|
||||
if existing, found := g.FindItemWithSku(m.Sku); found {
|
||||
existing.Quantity += int(m.Quantity)
|
||||
return nil
|
||||
}
|
||||
// Fast path: merge with existing item having same SKU
|
||||
if existing, found := g.FindItemWithSku(m.Sku); found {
|
||||
existing.Quantity += int(m.Quantity)
|
||||
return nil
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
g.lastItemId++
|
||||
taxRate := 2500
|
||||
if m.Tax > 0 {
|
||||
taxRate = int(m.Tax)
|
||||
}
|
||||
taxAmountPerUnit := GetTaxAmount(m.Price, taxRate)
|
||||
g.lastItemId++
|
||||
taxRate := 2500
|
||||
if m.Tax > 0 {
|
||||
taxRate = int(m.Tax)
|
||||
}
|
||||
taxAmountPerUnit := GetTaxAmount(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,
|
||||
ArticleType: m.ArticleType,
|
||||
Outlet: m.Outlet,
|
||||
SellerId: m.SellerId,
|
||||
SellerName: m.SellerName,
|
||||
Tax: int(taxAmountPerUnit),
|
||||
TaxRate: taxRate,
|
||||
StoreId: m.StoreId,
|
||||
})
|
||||
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,
|
||||
ArticleType: m.ArticleType,
|
||||
Outlet: m.Outlet,
|
||||
SellerId: m.SellerId,
|
||||
SellerName: m.SellerName,
|
||||
Tax: int(taxAmountPerUnit),
|
||||
TaxRate: taxRate,
|
||||
StoreId: m.StoreId,
|
||||
})
|
||||
|
||||
return nil
|
||||
},
|
||||
WithTotals(), // Recalculate totals after successful mutation
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
// mutation_add_request.go
|
||||
//
|
||||
// Registers the AddRequest mutation. This mutation is a higher-level intent
|
||||
@@ -30,32 +24,28 @@ import (
|
||||
// - Stock validation before increasing quantity
|
||||
// - Reservation logic or concurrency guards around stock updates
|
||||
// - Coupon / pricing rules applied conditionally during add-by-sku
|
||||
func init() {
|
||||
RegisterMutation[messages.AddRequest](
|
||||
"AddRequest",
|
||||
func(g *CartGrain, m *messages.AddRequest) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("AddRequest: nil payload")
|
||||
}
|
||||
if m.Sku == "" {
|
||||
return fmt.Errorf("AddRequest: sku is empty")
|
||||
}
|
||||
if m.Quantity < 1 {
|
||||
return fmt.Errorf("AddRequest: invalid quantity %d", m.Quantity)
|
||||
}
|
||||
|
||||
// Existing line: accumulate quantity only.
|
||||
if existing, found := g.FindItemWithSku(m.Sku); found {
|
||||
existing.Quantity += int(m.Quantity)
|
||||
return nil
|
||||
}
|
||||
// func HandleAddRequest(g *CartGrain, m *messages.AddRequest) error {
|
||||
// if m == nil {
|
||||
// return fmt.Errorf("AddRequest: nil payload")
|
||||
// }
|
||||
// if m.Sku == "" {
|
||||
// return fmt.Errorf("AddRequest: sku is empty")
|
||||
// }
|
||||
// if m.Quantity < 1 {
|
||||
// return fmt.Errorf("AddRequest: invalid quantity %d", m.Quantity)
|
||||
// }
|
||||
|
||||
// New line: delegate to higher-level AddItem flow (product lookup).
|
||||
// We intentionally ignore the returned *CartGrain; registry will
|
||||
// do totals again after this handler returns (harmless).
|
||||
_, err := g.AddItem(m.Sku, int(m.Quantity), m.Country, m.StoreId)
|
||||
return err
|
||||
},
|
||||
WithTotals(),
|
||||
)
|
||||
}
|
||||
// // Existing line: accumulate quantity only.
|
||||
// if existing, found := g.FindItemWithSku(m.Sku); found {
|
||||
// existing.Quantity += int(m.Quantity)
|
||||
// return nil
|
||||
// }
|
||||
// data, err := GetItemAddMessage(m.Sku, int(m.Quantity), m.Country, m.StoreId)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return AddItem(g, data)
|
||||
|
||||
// return err
|
||||
// }
|
||||
|
||||
@@ -25,34 +25,29 @@ import (
|
||||
// the grain's implicit expectation that higher layers control access.
|
||||
// (If strict locking is required around every mutation, wrap logic in
|
||||
// an explicit g.mu.Lock()/Unlock(), but current model mirrors prior code.)
|
||||
func init() {
|
||||
RegisterMutation[messages.ChangeQuantity](
|
||||
"ChangeQuantity",
|
||||
func(g *CartGrain, m *messages.ChangeQuantity) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("ChangeQuantity: nil payload")
|
||||
}
|
||||
|
||||
foundIndex := -1
|
||||
for i, it := range g.Items {
|
||||
if it.Id == int(m.Id) {
|
||||
foundIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundIndex == -1 {
|
||||
return fmt.Errorf("ChangeQuantity: item id %d not found", m.Id)
|
||||
}
|
||||
func ChangeQuantity(g *CartGrain, m *messages.ChangeQuantity) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("ChangeQuantity: nil payload")
|
||||
}
|
||||
|
||||
if m.Quantity <= 0 {
|
||||
// Remove the item
|
||||
g.Items = append(g.Items[:foundIndex], g.Items[foundIndex+1:]...)
|
||||
return nil
|
||||
}
|
||||
foundIndex := -1
|
||||
for i, it := range g.Items {
|
||||
if it.Id == int(m.Id) {
|
||||
foundIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundIndex == -1 {
|
||||
return fmt.Errorf("ChangeQuantity: item id %d not found", m.Id)
|
||||
}
|
||||
|
||||
g.Items[foundIndex].Quantity = int(m.Quantity)
|
||||
return nil
|
||||
},
|
||||
WithTotals(),
|
||||
)
|
||||
if m.Quantity <= 0 {
|
||||
// Remove the item
|
||||
g.Items = append(g.Items[:foundIndex], g.Items[foundIndex+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
g.Items[foundIndex].Quantity = int(m.Quantity)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -28,22 +28,17 @@ import (
|
||||
// parallel checkout attempts are possible, add higher-level guards
|
||||
// (e.g. reject if PaymentInProgress already true unless reusing
|
||||
// the same OrderReference).
|
||||
func init() {
|
||||
RegisterMutation[messages.InitializeCheckout](
|
||||
"InitializeCheckout",
|
||||
func(g *CartGrain, m *messages.InitializeCheckout) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("InitializeCheckout: nil payload")
|
||||
}
|
||||
if m.OrderId == "" {
|
||||
return fmt.Errorf("InitializeCheckout: missing orderId")
|
||||
}
|
||||
|
||||
g.OrderReference = m.OrderId
|
||||
g.PaymentStatus = m.Status
|
||||
g.PaymentInProgress = m.PaymentInProgress
|
||||
return nil
|
||||
},
|
||||
// No WithTotals(): monetary aggregates are unaffected.
|
||||
)
|
||||
func InitializeCheckout(g *CartGrain, m *messages.InitializeCheckout) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("InitializeCheckout: nil payload")
|
||||
}
|
||||
if m.OrderId == "" {
|
||||
return fmt.Errorf("InitializeCheckout: missing orderId")
|
||||
}
|
||||
|
||||
g.OrderReference = m.OrderId
|
||||
g.PaymentStatus = m.Status
|
||||
g.PaymentInProgress = m.PaymentInProgress
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,22 +32,17 @@ import (
|
||||
// - Relies on the higher-level guarantee that Apply() calls are serialized
|
||||
// per grain. If out-of-order events are possible, embed versioning or
|
||||
// timestamps in the mutation and compare before applying changes.
|
||||
func init() {
|
||||
RegisterMutation[messages.OrderCreated](
|
||||
"OrderCreated",
|
||||
func(g *CartGrain, m *messages.OrderCreated) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("OrderCreated: nil payload")
|
||||
}
|
||||
if m.OrderId == "" {
|
||||
return fmt.Errorf("OrderCreated: missing orderId")
|
||||
}
|
||||
|
||||
g.OrderReference = m.OrderId
|
||||
g.PaymentStatus = m.Status
|
||||
g.PaymentInProgress = false
|
||||
return nil
|
||||
},
|
||||
// No WithTotals(): order completion does not modify pricing or taxes.
|
||||
)
|
||||
func OrderCreated(g *CartGrain, m *messages.OrderCreated) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("OrderCreated: nil payload")
|
||||
}
|
||||
if m.OrderId == "" {
|
||||
return fmt.Errorf("OrderCreated: missing orderId")
|
||||
}
|
||||
|
||||
g.OrderReference = m.OrderId
|
||||
g.PaymentStatus = m.Status
|
||||
g.PaymentInProgress = false
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// mutation_registry.go
|
||||
//
|
||||
// Mutation Registry Infrastructure
|
||||
// --------------------------------
|
||||
// This file introduces a generic registry for cart mutations that:
|
||||
//
|
||||
// 1. Decouples mutation logic from the large type-switch inside CartGrain.Apply.
|
||||
// 2. Enforces (at registration time) that every mutation handler has the correct
|
||||
// signature: func(*CartGrain, *T) error
|
||||
// 3. Optionally auto-updates cart totals after a mutation if flagged.
|
||||
// 4. Provides a single authoritative list of registered mutations for
|
||||
// introspection / coverage testing.
|
||||
// 5. Allows incremental migration: you can first register new mutations here,
|
||||
// and later prune the legacy switch cases.
|
||||
//
|
||||
// Usage Pattern
|
||||
// -------------
|
||||
// // Define your mutation proto message (e.g. messages.ApplyCoupon in messages.proto)
|
||||
// // Regenerate protobufs.
|
||||
//
|
||||
// // In an init() (ideally in a small file like mutations_apply_coupon.go)
|
||||
// func init() {
|
||||
// RegisterMutation[*messages.ApplyCoupon](
|
||||
// "ApplyCoupon",
|
||||
// func(g *CartGrain, m *messages.ApplyCoupon) error {
|
||||
// // domain logic ...
|
||||
// discount := int64(5000)
|
||||
// if g.TotalPrice < discount {
|
||||
// discount = g.TotalPrice
|
||||
// }
|
||||
// g.TotalDiscount += discount
|
||||
// g.TotalPrice -= discount
|
||||
// return nil
|
||||
// },
|
||||
// WithTotals(), // we changed price-related fields; recalc totals
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // To invoke dynamically (alternative to the current switch):
|
||||
// if updated, err := ApplyRegistered(grain, incomingMessage); err == nil {
|
||||
// grain = updated
|
||||
// } else if errors.Is(err, ErrMutationNotRegistered) {
|
||||
// // fallback to legacy switch logic
|
||||
// }
|
||||
//
|
||||
// Migration Strategy
|
||||
// ------------------
|
||||
// 1. For each existing mutation handled in CartGrain.Apply, add a registry
|
||||
// registration with equivalent logic.
|
||||
// 2. Add a test that enumerates all *expected* mutation proto types and asserts
|
||||
// they are present in RegisteredMutationTypes().
|
||||
// 3. Once coverage is 100%, replace the switch in CartGrain.Apply with a call
|
||||
// to ApplyRegistered (and optionally keep a minimal default to produce an
|
||||
// "unsupported mutation" error).
|
||||
//
|
||||
// Thread Safety
|
||||
// -------------
|
||||
// Registration is typically done at init() time; a RWMutex provides safety
|
||||
// should late dynamic registration ever be introduced.
|
||||
//
|
||||
// Auto Totals
|
||||
// -----------
|
||||
// Many mutations require recomputing totals. To avoid forgetting this, pass
|
||||
// WithTotals() when registering. This will invoke grain.UpdateTotals() after
|
||||
// the handler returns successfully.
|
||||
//
|
||||
// Error Semantics
|
||||
// ---------------
|
||||
// - If a handler returns an error, totals are NOT recalculated (even if
|
||||
// WithTotals() was specified).
|
||||
// - ApplyRegistered returns (nil, ErrMutationNotRegistered) if the message type
|
||||
// is absent.
|
||||
//
|
||||
// Extensibility
|
||||
// -------------
|
||||
// It is straightforward to add options like audit hooks, metrics wrappers,
|
||||
// or optimistic concurrency guards by extending MutationOption.
|
||||
//
|
||||
// NOTE: Generics require Go 1.18+. If constrained to earlier Go versions,
|
||||
// replace the generic registration with a non-generic RegisterMutationType
|
||||
// that accepts reflect.Type and an adapter function.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
mutationRegistryMu sync.RWMutex
|
||||
mutationRegistry = make(map[reflect.Type]*registeredMutation)
|
||||
|
||||
// ErrMutationNotRegistered is returned when no handler exists for a given mutation type.
|
||||
ErrMutationNotRegistered = fmt.Errorf("mutation not registered")
|
||||
)
|
||||
|
||||
// MutationOption configures additional behavior for a registered mutation.
|
||||
type MutationOption func(*mutationOptions)
|
||||
|
||||
// mutationOptions holds flags adjustable per registration.
|
||||
type mutationOptions struct {
|
||||
updateTotals bool
|
||||
}
|
||||
|
||||
// WithTotals ensures CartGrain.UpdateTotals() is called after a successful handler.
|
||||
func WithTotals() MutationOption {
|
||||
return func(o *mutationOptions) {
|
||||
o.updateTotals = true
|
||||
}
|
||||
}
|
||||
|
||||
// registeredMutation stores metadata + the execution closure.
|
||||
type registeredMutation struct {
|
||||
name string
|
||||
handler func(*CartGrain, interface{}) error
|
||||
updateTotals bool
|
||||
msgType reflect.Type
|
||||
}
|
||||
|
||||
// RegisterMutation registers a mutation handler for a specific message type T.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// name - a human-readable identifier (used for diagnostics / coverage tests).
|
||||
// handler - business logic operating on the cart grain & strongly typed message.
|
||||
// options - optional behavior flags (e.g., WithTotals()).
|
||||
//
|
||||
// Panics if:
|
||||
// - name is empty
|
||||
// - handler is nil
|
||||
// - duplicate registration for the same message type T
|
||||
//
|
||||
// Typical call is placed in an init() function.
|
||||
func RegisterMutation[T any](name string, handler func(*CartGrain, *T) error, options ...MutationOption) {
|
||||
if name == "" {
|
||||
panic("RegisterMutation: name is required")
|
||||
}
|
||||
if handler == nil {
|
||||
panic("RegisterMutation: handler is nil")
|
||||
}
|
||||
|
||||
// Derive the reflect.Type for *T then its Elem (T) for mapping.
|
||||
var zero *T
|
||||
rtPtr := reflect.TypeOf(zero)
|
||||
if rtPtr.Kind() != reflect.Ptr {
|
||||
panic("RegisterMutation: expected pointer type for generic parameter")
|
||||
}
|
||||
rt := rtPtr.Elem()
|
||||
|
||||
opts := mutationOptions{}
|
||||
for _, opt := range options {
|
||||
opt(&opts)
|
||||
}
|
||||
|
||||
wrapped := func(g *CartGrain, m interface{}) error {
|
||||
typed, ok := m.(*T)
|
||||
if !ok {
|
||||
return fmt.Errorf("mutation type mismatch: have %T want *%s", m, rt.Name())
|
||||
}
|
||||
return handler(g, typed)
|
||||
}
|
||||
|
||||
mutationRegistryMu.Lock()
|
||||
defer mutationRegistryMu.Unlock()
|
||||
|
||||
if _, exists := mutationRegistry[rt]; exists {
|
||||
panic(fmt.Sprintf("RegisterMutation: duplicate registration for type %s", rt.String()))
|
||||
}
|
||||
|
||||
mutationRegistry[rt] = ®isteredMutation{
|
||||
name: name,
|
||||
handler: wrapped,
|
||||
updateTotals: opts.updateTotals,
|
||||
msgType: rt,
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyRegistered attempts to apply a registered mutation.
|
||||
// Returns updated grain if successful.
|
||||
//
|
||||
// If the mutation is not registered, returns (nil, ErrMutationNotRegistered).
|
||||
func ApplyRegistered(grain *CartGrain, msg interface{}) (*CartGrain, error) {
|
||||
if grain == nil {
|
||||
return nil, fmt.Errorf("nil grain")
|
||||
}
|
||||
if msg == nil {
|
||||
return nil, fmt.Errorf("nil mutation message")
|
||||
}
|
||||
|
||||
rt := indirectType(reflect.TypeOf(msg))
|
||||
mutationRegistryMu.RLock()
|
||||
entry, ok := mutationRegistry[rt]
|
||||
mutationRegistryMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil, ErrMutationNotRegistered
|
||||
}
|
||||
|
||||
if err := entry.handler(grain, msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if entry.updateTotals {
|
||||
grain.UpdateTotals()
|
||||
}
|
||||
|
||||
return grain, nil
|
||||
}
|
||||
|
||||
// RegisteredMutations returns metadata for all registered mutations (snapshot).
|
||||
func RegisteredMutations() []string {
|
||||
mutationRegistryMu.RLock()
|
||||
defer mutationRegistryMu.RUnlock()
|
||||
out := make([]string, 0, len(mutationRegistry))
|
||||
for _, entry := range mutationRegistry {
|
||||
out = append(out, entry.name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// RegisteredMutationTypes returns the reflect.Type list of all registered messages.
|
||||
// Useful for coverage tests ensuring expected set matches actual set.
|
||||
func RegisteredMutationTypes() []reflect.Type {
|
||||
mutationRegistryMu.RLock()
|
||||
defer mutationRegistryMu.RUnlock()
|
||||
out := make([]reflect.Type, 0, len(mutationRegistry))
|
||||
for t := range mutationRegistry {
|
||||
out = append(out, t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MustAssertMutationCoverage can be called at startup to ensure every expected
|
||||
// mutation type has been registered. It panics with a descriptive message if any
|
||||
// are missing. Provide a slice of prototype pointers (e.g. []*messages.AddItem{nil} ...)
|
||||
func MustAssertMutationCoverage(expected []interface{}) {
|
||||
mutationRegistryMu.RLock()
|
||||
defer mutationRegistryMu.RUnlock()
|
||||
|
||||
missing := make([]string, 0)
|
||||
for _, ex := range expected {
|
||||
if ex == nil {
|
||||
continue
|
||||
}
|
||||
t := indirectType(reflect.TypeOf(ex))
|
||||
if _, ok := mutationRegistry[t]; !ok {
|
||||
missing = append(missing, t.String())
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
panic(fmt.Sprintf("mutation registry missing handlers for: %v", missing))
|
||||
}
|
||||
}
|
||||
|
||||
// indirectType returns the element type if given a pointer; otherwise the type itself.
|
||||
func indirectType(t reflect.Type) reflect.Type {
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
/*
|
||||
Integration Guide
|
||||
-----------------
|
||||
|
||||
1. Register all existing mutations:
|
||||
|
||||
func init() {
|
||||
RegisterMutation[*messages.AddItem]("AddItem",
|
||||
func(g *CartGrain, m *messages.AddItem) error {
|
||||
// (port logic from existing switch branch)
|
||||
// ...
|
||||
return nil
|
||||
},
|
||||
WithTotals(),
|
||||
)
|
||||
// ... repeat for others
|
||||
}
|
||||
|
||||
2. In CartGrain.Apply (early in the method) add:
|
||||
|
||||
if updated, err := ApplyRegistered(c, content); err == nil {
|
||||
return updated, nil
|
||||
} else if err != ErrMutationNotRegistered {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// existing switch fallback below
|
||||
|
||||
3. Once all mutations are registered, remove the legacy switch cases
|
||||
and leave a single ErrMutationNotRegistered path for unknown types.
|
||||
|
||||
4. Add a coverage test (see docs for example; removed from source for clarity).
|
||||
5. (Optional) Add metrics / tracing wrappers for handlers.
|
||||
|
||||
*/
|
||||
@@ -25,29 +25,24 @@ import (
|
||||
// Future considerations:
|
||||
// - If delivery pricing logic changes (e.g., dynamic taxes per delivery),
|
||||
// UpdateTotals() may need enhancement to incorporate delivery tax properly.
|
||||
func init() {
|
||||
RegisterMutation[messages.RemoveDelivery](
|
||||
"RemoveDelivery",
|
||||
func(g *CartGrain, m *messages.RemoveDelivery) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("RemoveDelivery: nil payload")
|
||||
}
|
||||
targetID := int(m.Id)
|
||||
index := -1
|
||||
for i, d := range g.Deliveries {
|
||||
if d.Id == targetID {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return fmt.Errorf("RemoveDelivery: delivery id %d not found", m.Id)
|
||||
}
|
||||
|
||||
// Remove delivery (order not preserved beyond necessity)
|
||||
g.Deliveries = append(g.Deliveries[:index], g.Deliveries[index+1:]...)
|
||||
return nil
|
||||
},
|
||||
WithTotals(),
|
||||
)
|
||||
func RemoveDelivery(g *CartGrain, m *messages.RemoveDelivery) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("RemoveDelivery: nil payload")
|
||||
}
|
||||
targetID := int(m.Id)
|
||||
index := -1
|
||||
for i, d := range g.Deliveries {
|
||||
if d.Id == targetID {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return fmt.Errorf("RemoveDelivery: delivery id %d not found", m.Id)
|
||||
}
|
||||
|
||||
// Remove delivery (order not preserved beyond necessity)
|
||||
g.Deliveries = append(g.Deliveries[:index], g.Deliveries[index+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,29 +21,24 @@ import (
|
||||
// semantics require pruning delivery.item_ids you can extend this handler.
|
||||
// - If multiple lines somehow shared the same Id (should not happen), only
|
||||
// the first match would be removed—data integrity relies on unique line Ids.
|
||||
func init() {
|
||||
RegisterMutation[messages.RemoveItem](
|
||||
"RemoveItem",
|
||||
func(g *CartGrain, m *messages.RemoveItem) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("RemoveItem: nil payload")
|
||||
}
|
||||
targetID := int(m.Id)
|
||||
|
||||
index := -1
|
||||
for i, it := range g.Items {
|
||||
if it.Id == targetID {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return fmt.Errorf("RemoveItem: item id %d not found", m.Id)
|
||||
}
|
||||
func RemoveItem(g *CartGrain, m *messages.RemoveItem) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("RemoveItem: nil payload")
|
||||
}
|
||||
targetID := int(m.Id)
|
||||
|
||||
g.Items = append(g.Items[:index], g.Items[index+1:]...)
|
||||
return nil
|
||||
},
|
||||
WithTotals(),
|
||||
)
|
||||
index := -1
|
||||
for i, it := range g.Items {
|
||||
if it.Id == targetID {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return fmt.Errorf("RemoveItem: item id %d not found", m.Id)
|
||||
}
|
||||
|
||||
g.Items = append(g.Items[:index], g.Items[index+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
// mutation_set_cart_items.go
|
||||
//
|
||||
// Registers the SetCartRequest mutation. This mutation replaces the entire list
|
||||
@@ -25,33 +19,28 @@ import (
|
||||
// - Deliveries might reference item IDs that are now invalid—original logic
|
||||
// also left deliveries untouched. If that becomes an issue, add a cleanup
|
||||
// pass to remove deliveries whose item IDs no longer exist.
|
||||
func init() {
|
||||
RegisterMutation[messages.SetCartRequest](
|
||||
"SetCartRequest",
|
||||
func(g *CartGrain, m *messages.SetCartRequest) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("SetCartRequest: nil payload")
|
||||
}
|
||||
|
||||
// Clear current items (keep deliveries)
|
||||
g.mu.Lock()
|
||||
g.Items = make([]*CartItem, 0, len(m.Items))
|
||||
g.mu.Unlock()
|
||||
// func HandleSetCartRequest(g *CartGrain, m *messages.SetCartRequest) error {
|
||||
// if m == nil {
|
||||
// return fmt.Errorf("SetCartRequest: nil payload")
|
||||
// }
|
||||
|
||||
for _, it := range m.Items {
|
||||
if it == nil {
|
||||
continue
|
||||
}
|
||||
if it.Sku == "" || it.Quantity < 1 {
|
||||
return fmt.Errorf("SetCartRequest: invalid item (sku='%s' qty=%d)", it.Sku, it.Quantity)
|
||||
}
|
||||
_, err := g.AddItem(it.Sku, int(it.Quantity), it.Country, it.StoreId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetCartRequest: add sku '%s' failed: %w", it.Sku, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
WithTotals(),
|
||||
)
|
||||
}
|
||||
// // Clear current items (keep deliveries)
|
||||
// g.mu.Lock()
|
||||
// g.Items = make([]*CartItem, 0, len(m.Items))
|
||||
// g.mu.Unlock()
|
||||
|
||||
// for _, it := range m.Items {
|
||||
// if it == nil {
|
||||
// continue
|
||||
// }
|
||||
// if it.Sku == "" || it.Quantity < 1 {
|
||||
// return fmt.Errorf("SetCartRequest: invalid item (sku='%s' qty=%d)", it.Sku, it.Quantity)
|
||||
// }
|
||||
// _, err := g.AddItem(it.Sku, int(it.Quantity), it.Country, it.StoreId)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("SetCartRequest: add sku '%s' failed: %w", it.Sku, err)
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
@@ -39,63 +39,58 @@ import (
|
||||
// - Variable delivery pricing (based on weight, distance, provider, etc.)
|
||||
// - Validation of provider codes
|
||||
// - Multi-currency delivery pricing
|
||||
func init() {
|
||||
RegisterMutation[messages.SetDelivery](
|
||||
"SetDelivery",
|
||||
func(g *CartGrain, m *messages.SetDelivery) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("SetDelivery: nil payload")
|
||||
}
|
||||
if m.Provider == "" {
|
||||
return fmt.Errorf("SetDelivery: provider is empty")
|
||||
}
|
||||
|
||||
withDelivery := g.ItemsWithDelivery()
|
||||
targetItems := make([]int, 0)
|
||||
func SetDelivery(g *CartGrain, m *messages.SetDelivery) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("SetDelivery: nil payload")
|
||||
}
|
||||
if m.Provider == "" {
|
||||
return fmt.Errorf("SetDelivery: provider is empty")
|
||||
}
|
||||
|
||||
if len(m.Items) == 0 {
|
||||
// Use every item currently without a delivery
|
||||
targetItems = append(targetItems, g.ItemsWithoutDelivery()...)
|
||||
} else {
|
||||
// Validate explicit list
|
||||
for _, id64 := range m.Items {
|
||||
id := int(id64)
|
||||
found := false
|
||||
for _, it := range g.Items {
|
||||
if it.Id == id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("SetDelivery: item id %d not found", id)
|
||||
}
|
||||
if slices.Contains(withDelivery, id) {
|
||||
return fmt.Errorf("SetDelivery: item id %d already has a delivery", id)
|
||||
}
|
||||
targetItems = append(targetItems, id)
|
||||
withDelivery := g.ItemsWithDelivery()
|
||||
targetItems := make([]int, 0)
|
||||
|
||||
if len(m.Items) == 0 {
|
||||
// Use every item currently without a delivery
|
||||
targetItems = append(targetItems, g.ItemsWithoutDelivery()...)
|
||||
} else {
|
||||
// Validate explicit list
|
||||
for _, id64 := range m.Items {
|
||||
id := int(id64)
|
||||
found := false
|
||||
for _, it := range g.Items {
|
||||
if it.Id == id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(targetItems) == 0 {
|
||||
return fmt.Errorf("SetDelivery: no eligible items to attach")
|
||||
if !found {
|
||||
return fmt.Errorf("SetDelivery: item id %d not found", id)
|
||||
}
|
||||
if slices.Contains(withDelivery, id) {
|
||||
return fmt.Errorf("SetDelivery: item id %d already has a delivery", id)
|
||||
}
|
||||
targetItems = append(targetItems, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Append new delivery
|
||||
g.mu.Lock()
|
||||
g.lastDeliveryId++
|
||||
newId := g.lastDeliveryId
|
||||
g.Deliveries = append(g.Deliveries, &CartDelivery{
|
||||
Id: newId,
|
||||
Provider: m.Provider,
|
||||
PickupPoint: m.PickupPoint,
|
||||
Price: 4900, // TODO: externalize pricing
|
||||
Items: targetItems,
|
||||
})
|
||||
g.mu.Unlock()
|
||||
if len(targetItems) == 0 {
|
||||
return fmt.Errorf("SetDelivery: no eligible items to attach")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
WithTotals(),
|
||||
)
|
||||
// Append new delivery
|
||||
g.mu.Lock()
|
||||
g.lastDeliveryId++
|
||||
newId := g.lastDeliveryId
|
||||
g.Deliveries = append(g.Deliveries, &CartDelivery{
|
||||
Id: newId,
|
||||
Provider: m.Provider,
|
||||
PickupPoint: m.PickupPoint,
|
||||
Price: 4900, // TODO: externalize pricing
|
||||
Items: targetItems,
|
||||
})
|
||||
g.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -28,29 +28,24 @@ import (
|
||||
// - Validate pickup point fields (country code, zip format, etc.)
|
||||
// - Track history / audit of pickup point changes
|
||||
// - Trigger delivery price adjustments (which would then require WithTotals()).
|
||||
func init() {
|
||||
RegisterMutation[messages.SetPickupPoint](
|
||||
"SetPickupPoint",
|
||||
func(g *CartGrain, m *messages.SetPickupPoint) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("SetPickupPoint: nil payload")
|
||||
}
|
||||
|
||||
for _, d := range g.Deliveries {
|
||||
if d.Id == int(m.DeliveryId) {
|
||||
d.PickupPoint = &messages.PickupPoint{
|
||||
Id: m.Id,
|
||||
Name: m.Name,
|
||||
Address: m.Address,
|
||||
City: m.City,
|
||||
Zip: m.Zip,
|
||||
Country: m.Country,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func SetPickupPoint(g *CartGrain, m *messages.SetPickupPoint) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("SetPickupPoint: nil payload")
|
||||
}
|
||||
|
||||
for _, d := range g.Deliveries {
|
||||
if d.Id == int(m.DeliveryId) {
|
||||
d.PickupPoint = &messages.PickupPoint{
|
||||
Id: m.Id,
|
||||
Name: m.Name,
|
||||
Address: m.Address,
|
||||
City: m.City,
|
||||
Zip: m.Zip,
|
||||
Country: m.Country,
|
||||
}
|
||||
return fmt.Errorf("SetPickupPoint: delivery id %d not found", m.DeliveryId)
|
||||
},
|
||||
// No WithTotals(): pickup point does not change pricing / tax.
|
||||
)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("SetPickupPoint: delivery id %d not found", m.DeliveryId)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
type PoolServer struct {
|
||||
@@ -27,7 +28,7 @@ func NewPoolServer(pool actor.GrainPool[*CartGrain], pod_name string, klarnaClie
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PoolServer) ApplyLocal(id CartId, mutation interface{}) (*CartGrain, error) {
|
||||
func (s *PoolServer) ApplyLocal(id CartId, mutation proto.Message) (*CartGrain, error) {
|
||||
return s.pool.Apply(uint64(id), mutation)
|
||||
}
|
||||
|
||||
@@ -42,7 +43,11 @@ func (s *PoolServer) HandleGet(w http.ResponseWriter, r *http.Request, id CartId
|
||||
|
||||
func (s *PoolServer) HandleAddSku(w http.ResponseWriter, r *http.Request, id CartId) error {
|
||||
sku := r.PathValue("sku")
|
||||
data, err := s.ApplyLocal(id, &messages.AddRequest{Sku: sku, Quantity: 1})
|
||||
msg, err := GetItemAddMessage(sku, 1, getCountryFromHost(r.Host), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := s.ApplyLocal(id, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -80,7 +85,7 @@ func (s *PoolServer) HandleDeleteItem(w http.ResponseWriter, r *http.Request, id
|
||||
return s.WriteResult(w, data)
|
||||
}
|
||||
|
||||
type SetDelivery struct {
|
||||
type SetDeliveryRequest struct {
|
||||
Provider string `json:"provider"`
|
||||
Items []int64 `json:"items"`
|
||||
PickupPoint *messages.PickupPoint `json:"pickupPoint,omitempty"`
|
||||
@@ -88,7 +93,7 @@ type SetDelivery struct {
|
||||
|
||||
func (s *PoolServer) HandleSetDelivery(w http.ResponseWriter, r *http.Request, id CartId) error {
|
||||
|
||||
delivery := SetDelivery{}
|
||||
delivery := SetDeliveryRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(&delivery)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -269,8 +274,7 @@ func (s *PoolServer) ApplyCheckoutStarted(klarnaOrder *CheckoutOrder, id CartId)
|
||||
// w.Header().Set("Content-Type", "application/json")
|
||||
// return json.NewEncoder(w).Encode(klarnaOrder)
|
||||
// }
|
||||
//
|
||||
|
||||
//
|
||||
|
||||
func CookieCartIdHandler(fn func(cartId CartId, w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user