update
This commit is contained in:
@@ -29,25 +29,30 @@ type ItemMeta struct {
|
||||
SellerName string `json:"sellerName,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Outlet *string `json:"outlet,omitempty"`
|
||||
Cgm string `json:"cgm,omitempty"` // Customer Group Membership
|
||||
}
|
||||
|
||||
type CartItem struct {
|
||||
Id uint32 `json:"id"`
|
||||
ItemId uint32 `json:"itemId,omitempty"`
|
||||
ParentId *uint32 `json:"parentId,omitempty"`
|
||||
Sku string `json:"sku"`
|
||||
Price Price `json:"price"`
|
||||
TotalPrice Price `json:"totalPrice"`
|
||||
OrgPrice *Price `json:"orgPrice,omitempty"`
|
||||
Tax int
|
||||
Stock StockStatus `json:"stock"`
|
||||
Quantity int `json:"qty"`
|
||||
Discount *Price `json:"discount,omitempty"`
|
||||
Disclaimer string `json:"disclaimer,omitempty"`
|
||||
ArticleType string `json:"type,omitempty"`
|
||||
StoreId *string `json:"storeId,omitempty"`
|
||||
Meta *ItemMeta `json:"meta,omitempty"`
|
||||
SaleStatus string `json:"saleStatus"`
|
||||
Id uint32 `json:"id"`
|
||||
ItemId uint32 `json:"itemId,omitempty"`
|
||||
ParentId *uint32 `json:"parentId,omitempty"`
|
||||
Sku string `json:"sku"`
|
||||
Price Price `json:"price"`
|
||||
TotalPrice Price `json:"totalPrice"`
|
||||
OrgPrice *Price `json:"orgPrice,omitempty"`
|
||||
Tax int
|
||||
Stock StockStatus `json:"stock"`
|
||||
Quantity int `json:"qty"`
|
||||
Discount *Price `json:"discount,omitempty"`
|
||||
Disclaimer string `json:"disclaimer,omitempty"`
|
||||
ArticleType string `json:"type,omitempty"`
|
||||
StoreId *string `json:"storeId,omitempty"`
|
||||
Meta *ItemMeta `json:"meta,omitempty"`
|
||||
SaleStatus string `json:"saleStatus"`
|
||||
Marking *Marking `json:"marking,omitempty"`
|
||||
SubscriptionDetailsId string `json:"subscriptionDetailsId,omitempty"`
|
||||
OrderReference string `json:"orderReference,omitempty"`
|
||||
IsSubscribed bool `json:"isSubscribed,omitempty"`
|
||||
}
|
||||
|
||||
type CartDelivery struct {
|
||||
@@ -73,27 +78,38 @@ type SubscriptionDetails struct {
|
||||
Meta json.RawMessage `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type Marking struct {
|
||||
Type uint32 `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type CartGrain struct {
|
||||
mu sync.RWMutex
|
||||
lastItemId uint32
|
||||
lastDeliveryId uint32
|
||||
lastVoucherId uint32
|
||||
lastAccess time.Time
|
||||
lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts)
|
||||
userId string
|
||||
InventoryReserved bool `json:"inventoryReserved"`
|
||||
Id CartId `json:"id"`
|
||||
Items []*CartItem `json:"items"`
|
||||
TotalPrice *Price `json:"totalPrice"`
|
||||
TotalDiscount *Price `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"`
|
||||
Vouchers []*Voucher `json:"vouchers,omitempty"`
|
||||
Notifications []CartNotification `json:"cartNotification,omitempty"`
|
||||
SubscriptionDetails map[string]*SubscriptionDetails `json:"subscriptionDetails,omitempty"`
|
||||
mu sync.RWMutex
|
||||
lastItemId uint32
|
||||
lastDeliveryId uint32
|
||||
lastVoucherId uint32
|
||||
lastAccess time.Time
|
||||
lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts)
|
||||
userId string
|
||||
InventoryReserved bool `json:"inventoryReserved"`
|
||||
Id CartId `json:"id"`
|
||||
Items []*CartItem `json:"items"`
|
||||
TotalPrice *Price `json:"totalPrice"`
|
||||
TotalDiscount *Price `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"`
|
||||
Vouchers []*Voucher `json:"vouchers,omitempty"`
|
||||
Notifications []CartNotification `json:"cartNotification,omitempty"`
|
||||
SubscriptionDetails map[string]*SubscriptionDetails `json:"subscriptionDetails,omitempty"`
|
||||
PaymentDeclinedAt time.Time `json:"paymentDeclinedAt,omitempty"`
|
||||
ConfirmationViewCount int `json:"confirmationViewCount,omitempty"`
|
||||
ConfirmationLastViewedAt time.Time `json:"confirmationLastViewedAt,omitempty"`
|
||||
CheckoutOrderId string `json:"checkoutOrderId,omitempty"`
|
||||
CheckoutStatus string `json:"checkoutStatus,omitempty"`
|
||||
CheckoutCountry string `json:"checkoutCountry,omitempty"`
|
||||
}
|
||||
|
||||
type Voucher struct {
|
||||
|
||||
@@ -51,6 +51,27 @@ func NewCartMultationRegistry() actor.MutationRegistry {
|
||||
actor.NewMutation(PreConditionFailed, func() *messages.PreConditionFailed {
|
||||
return &messages.PreConditionFailed{}
|
||||
}),
|
||||
actor.NewMutation(SetUserId, func() *messages.SetUserId {
|
||||
return &messages.SetUserId{}
|
||||
}),
|
||||
actor.NewMutation(LineItemMarking, func() *messages.LineItemMarking {
|
||||
return &messages.LineItemMarking{}
|
||||
}),
|
||||
actor.NewMutation(RemoveLineItemMarking, func() *messages.RemoveLineItemMarking {
|
||||
return &messages.RemoveLineItemMarking{}
|
||||
}),
|
||||
actor.NewMutation(SubscriptionAdded, func() *messages.SubscriptionAdded {
|
||||
return &messages.SubscriptionAdded{}
|
||||
}),
|
||||
actor.NewMutation(PaymentDeclined, func() *messages.PaymentDeclined {
|
||||
return &messages.PaymentDeclined{}
|
||||
}),
|
||||
actor.NewMutation(ConfirmationViewed, func() *messages.ConfirmationViewed {
|
||||
return &messages.ConfirmationViewed{}
|
||||
}),
|
||||
actor.NewMutation(CreateCheckoutOrder, func() *messages.CreateCheckoutOrder {
|
||||
return &messages.CreateCheckoutOrder{}
|
||||
}),
|
||||
)
|
||||
return reg
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ func AddItem(g *CartGrain, m *messages.AddItem) error {
|
||||
Outlet: m.Outlet,
|
||||
SellerId: m.SellerId,
|
||||
SellerName: m.SellerName,
|
||||
Cgm: m.Cgm,
|
||||
},
|
||||
SaleStatus: m.SaleStatus,
|
||||
ParentId: m.ParentId,
|
||||
|
||||
13
pkg/cart/mutation_confirmation_viewed.go
Normal file
13
pkg/cart/mutation_confirmation_viewed.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
func ConfirmationViewed(grain *CartGrain, req *messages.ConfirmationViewed) error {
|
||||
grain.ConfirmationViewCount++
|
||||
grain.ConfirmationLastViewedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
21
pkg/cart/mutation_create_checkout_order.go
Normal file
21
pkg/cart/mutation_create_checkout_order.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
func CreateCheckoutOrder(grain *CartGrain, req *messages.CreateCheckoutOrder) error {
|
||||
if len(grain.Items) == 0 {
|
||||
return errors.New("cannot checkout empty cart")
|
||||
}
|
||||
if req.Terms != "accepted" {
|
||||
return errors.New("terms must be accepted")
|
||||
}
|
||||
// Validate other fields as needed
|
||||
grain.CheckoutOrderId = uuid.New().String()
|
||||
grain.CheckoutStatus = "pending"
|
||||
grain.CheckoutCountry = req.Country
|
||||
return nil
|
||||
}
|
||||
20
pkg/cart/mutation_line_item_marking.go
Normal file
20
pkg/cart/mutation_line_item_marking.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
func LineItemMarking(grain *CartGrain, req *messages.LineItemMarking) error {
|
||||
for i, item := range grain.Items {
|
||||
if item.Id == req.Id {
|
||||
grain.Items[i].Marking = &Marking{
|
||||
Type: req.Type,
|
||||
Text: req.Marking,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.Id)
|
||||
}
|
||||
16
pkg/cart/mutation_payment_declined.go
Normal file
16
pkg/cart/mutation_payment_declined.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"time"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
func PaymentDeclined(grain *CartGrain, req *messages.PaymentDeclined) error {
|
||||
grain.PaymentStatus = "declined"
|
||||
grain.PaymentDeclinedAt = time.Now()
|
||||
// Optionally clear checkout order if in progress
|
||||
if grain.CheckoutOrderId != "" {
|
||||
grain.CheckoutOrderId = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
16
pkg/cart/mutation_remove_line_item_marking.go
Normal file
16
pkg/cart/mutation_remove_line_item_marking.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
func RemoveLineItemMarking(grain *CartGrain, req *messages.RemoveLineItemMarking) error {
|
||||
for i, item := range grain.Items {
|
||||
if item.Id == req.Id {
|
||||
grain.Items[i].Marking = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.Id)
|
||||
}
|
||||
14
pkg/cart/mutation_set_user_id.go
Normal file
14
pkg/cart/mutation_set_user_id.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"errors"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
func SetUserId(grain *CartGrain, req *messages.SetUserId) error {
|
||||
if req.UserId == "" {
|
||||
return errors.New("user ID cannot be empty")
|
||||
}
|
||||
grain.userId = req.UserId
|
||||
return nil
|
||||
}
|
||||
18
pkg/cart/mutation_subscription_added.go
Normal file
18
pkg/cart/mutation_subscription_added.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
func SubscriptionAdded(grain *CartGrain, req *messages.SubscriptionAdded) error {
|
||||
for i, item := range grain.Items {
|
||||
if item.Id == req.ItemId {
|
||||
grain.Items[i].SubscriptionDetailsId = req.DetailsId
|
||||
grain.Items[i].OrderReference = req.OrderReference
|
||||
grain.Items[i].IsSubscribed = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.ItemId)
|
||||
}
|
||||
@@ -85,6 +85,34 @@ func msgOrderCreated(orderId, status string) *messages.OrderCreated {
|
||||
return &messages.OrderCreated{OrderId: orderId, Status: status}
|
||||
}
|
||||
|
||||
func msgSetUserId(userId string) *messages.SetUserId {
|
||||
return &messages.SetUserId{UserId: userId}
|
||||
}
|
||||
|
||||
func msgLineItemMarking(id uint32, typ uint32, marking string) *messages.LineItemMarking {
|
||||
return &messages.LineItemMarking{Id: id, Type: typ, Marking: marking}
|
||||
}
|
||||
|
||||
func msgRemoveLineItemMarking(id uint32) *messages.RemoveLineItemMarking {
|
||||
return &messages.RemoveLineItemMarking{Id: id}
|
||||
}
|
||||
|
||||
func msgSubscriptionAdded(itemId uint32, detailsId, orderRef string) *messages.SubscriptionAdded {
|
||||
return &messages.SubscriptionAdded{ItemId: itemId, DetailsId: detailsId, OrderReference: orderRef}
|
||||
}
|
||||
|
||||
func msgPaymentDeclined() *messages.PaymentDeclined {
|
||||
return &messages.PaymentDeclined{}
|
||||
}
|
||||
|
||||
func msgConfirmationViewed() *messages.ConfirmationViewed {
|
||||
return &messages.ConfirmationViewed{}
|
||||
}
|
||||
|
||||
func msgCreateCheckoutOrder(terms, country string) *messages.CreateCheckoutOrder {
|
||||
return &messages.CreateCheckoutOrder{Terms: terms, Country: country}
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
// ----------------------
|
||||
@@ -144,6 +172,15 @@ func TestMutationRegistryCoverage(t *testing.T) {
|
||||
"AddVoucher",
|
||||
"RemoveVoucher",
|
||||
"UpsertSubscriptionDetails",
|
||||
"InventoryReserved",
|
||||
"PreConditionFailed",
|
||||
"SetUserId",
|
||||
"LineItemMarking",
|
||||
"RemoveLineItemMarking",
|
||||
"SubscriptionAdded",
|
||||
"PaymentDeclined",
|
||||
"ConfirmationViewed",
|
||||
"CreateCheckoutOrder",
|
||||
}
|
||||
|
||||
names := reg.(*actor.ProtoMutationRegistry).RegisteredMutations()
|
||||
@@ -542,3 +579,134 @@ func TestSubscriptionDetailsJSONValidation(t *testing.T) {
|
||||
t.Fatalf("empty update should not change meta, got %s", string(g.SubscriptionDetails[id].Meta))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetUserId(t *testing.T) {
|
||||
reg := newRegistry()
|
||||
g := newTestGrain()
|
||||
|
||||
applyOK(t, reg, g, msgSetUserId("user123"))
|
||||
if g.userId != "user123" {
|
||||
t.Fatalf("expected userId=user123, got %s", g.userId)
|
||||
}
|
||||
|
||||
applyErrorContains(t, reg, g, msgSetUserId(""), "cannot be empty")
|
||||
}
|
||||
|
||||
func TestLineItemMarking(t *testing.T) {
|
||||
reg := newRegistry()
|
||||
g := newTestGrain()
|
||||
|
||||
applyOK(t, reg, g, msgAddItem("MARK", 1000, 1, nil))
|
||||
id := g.Items[0].Id
|
||||
|
||||
applyOK(t, reg, g, msgLineItemMarking(id, 1, "Gift message"))
|
||||
if g.Items[0].Marking == nil || g.Items[0].Marking.Type != 1 || g.Items[0].Marking.Text != "Gift message" {
|
||||
t.Fatalf("marking not set correctly: %+v", g.Items[0].Marking)
|
||||
}
|
||||
|
||||
applyErrorContains(t, reg, g, msgLineItemMarking(9999, 2, "Test"), "not found")
|
||||
}
|
||||
|
||||
func TestRemoveLineItemMarking(t *testing.T) {
|
||||
reg := newRegistry()
|
||||
g := newTestGrain()
|
||||
|
||||
applyOK(t, reg, g, msgAddItem("REMOVE", 1000, 1, nil))
|
||||
id := g.Items[0].Id
|
||||
|
||||
// First set a marking
|
||||
applyOK(t, reg, g, msgLineItemMarking(id, 1, "Test marking"))
|
||||
if g.Items[0].Marking == nil || g.Items[0].Marking.Text != "Test marking" {
|
||||
t.Fatalf("marking not set")
|
||||
}
|
||||
|
||||
// Now remove it
|
||||
applyOK(t, reg, g, msgRemoveLineItemMarking(id))
|
||||
if g.Items[0].Marking != nil {
|
||||
t.Fatalf("marking not removed")
|
||||
}
|
||||
|
||||
applyErrorContains(t, reg, g, msgRemoveLineItemMarking(9999), "not found")
|
||||
}
|
||||
|
||||
func TestSubscriptionAdded(t *testing.T) {
|
||||
reg := newRegistry()
|
||||
g := newTestGrain()
|
||||
|
||||
applyOK(t, reg, g, msgAddItem("SUB", 1000, 1, nil))
|
||||
id := g.Items[0].Id
|
||||
|
||||
applyOK(t, reg, g, msgSubscriptionAdded(id, "det123", "ord456"))
|
||||
if g.Items[0].SubscriptionDetailsId != "det123" || g.Items[0].OrderReference != "ord456" || !g.Items[0].IsSubscribed {
|
||||
t.Fatalf("subscription not added: detailsId=%s orderRef=%s isSubscribed=%v",
|
||||
g.Items[0].SubscriptionDetailsId, g.Items[0].OrderReference, g.Items[0].IsSubscribed)
|
||||
}
|
||||
|
||||
applyErrorContains(t, reg, g, msgSubscriptionAdded(9999, "", ""), "not found")
|
||||
}
|
||||
|
||||
func TestPaymentDeclined(t *testing.T) {
|
||||
reg := newRegistry()
|
||||
g := newTestGrain()
|
||||
|
||||
g.CheckoutOrderId = "test-order"
|
||||
applyOK(t, reg, g, msgPaymentDeclined())
|
||||
if g.PaymentStatus != "declined" || g.CheckoutOrderId != "" {
|
||||
t.Fatalf("payment declined not handled: status=%s checkoutId=%s", g.PaymentStatus, g.CheckoutOrderId)
|
||||
}
|
||||
if g.PaymentDeclinedAt.IsZero() {
|
||||
t.Fatalf("PaymentDeclinedAt not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmationViewed(t *testing.T) {
|
||||
reg := newRegistry()
|
||||
g := newTestGrain()
|
||||
|
||||
// Initial state
|
||||
if g.ConfirmationViewCount != 0 {
|
||||
t.Fatalf("initial view count should be 0, got %d", g.ConfirmationViewCount)
|
||||
}
|
||||
if !g.ConfirmationLastViewedAt.IsZero() {
|
||||
t.Fatalf("initial last viewed should be zero")
|
||||
}
|
||||
|
||||
// First view
|
||||
applyOK(t, reg, g, msgConfirmationViewed())
|
||||
if g.ConfirmationViewCount != 1 {
|
||||
t.Fatalf("view count should be 1, got %d", g.ConfirmationViewCount)
|
||||
}
|
||||
if g.ConfirmationLastViewedAt.IsZero() {
|
||||
t.Fatalf("ConfirmationLastViewedAt not set")
|
||||
}
|
||||
firstTime := g.ConfirmationLastViewedAt
|
||||
|
||||
// Second view
|
||||
applyOK(t, reg, g, msgConfirmationViewed())
|
||||
if g.ConfirmationViewCount != 2 {
|
||||
t.Fatalf("view count should be 2, got %d", g.ConfirmationViewCount)
|
||||
}
|
||||
if g.ConfirmationLastViewedAt == firstTime {
|
||||
t.Fatalf("ConfirmationLastViewedAt should have updated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCheckoutOrder(t *testing.T) {
|
||||
reg := newRegistry()
|
||||
g := newTestGrain()
|
||||
|
||||
applyOK(t, reg, g, msgAddItem("CHECKOUT", 1000, 1, nil))
|
||||
|
||||
applyOK(t, reg, g, msgCreateCheckoutOrder("accepted", "SE"))
|
||||
if g.CheckoutOrderId == "" || g.CheckoutStatus != "pending" || g.CheckoutCountry != "SE" {
|
||||
t.Fatalf("checkout order not created: id=%s status=%s country=%s",
|
||||
g.CheckoutOrderId, g.CheckoutStatus, g.CheckoutCountry)
|
||||
}
|
||||
|
||||
// Empty cart
|
||||
g2 := newTestGrain()
|
||||
applyErrorContains(t, reg, g2, msgCreateCheckoutOrder("accepted", ""), "empty cart")
|
||||
|
||||
// Terms not accepted
|
||||
applyErrorContains(t, reg, g, msgCreateCheckoutOrder("no", ""), "terms must be accepted")
|
||||
}
|
||||
|
||||
@@ -39,12 +39,7 @@ func UpsertSubscriptionDetails(g *CartGrain, m *messages.UpsertSubscriptionDetai
|
||||
// Update existing entry.
|
||||
existing, ok := g.SubscriptionDetails[*m.Id]
|
||||
if !ok {
|
||||
n := &SubscriptionDetails{
|
||||
Id: *m.Id,
|
||||
Version: 0,
|
||||
}
|
||||
g.SubscriptionDetails[*m.Id] = n
|
||||
existing = n
|
||||
return fmt.Errorf("subscription details with id %s not found", *m.Id)
|
||||
}
|
||||
changed := false
|
||||
if m.OfferingCode != "" {
|
||||
|
||||
Reference in New Issue
Block a user