link user
This commit is contained in:
@@ -6,7 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
||||||
|
profile "git.k6n.net/mats/go-cart-actor/pkg/profile"
|
||||||
messages "git.k6n.net/mats/go-cart-actor/proto/checkout"
|
messages "git.k6n.net/mats/go-cart-actor/proto/checkout"
|
||||||
|
profileMessages "git.k6n.net/mats/go-cart-actor/proto/profile"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,6 +162,15 @@ func createOrderFromSettledCheckout(ctx context.Context, s *CheckoutPoolServer,
|
|||||||
Status: "completed",
|
Status: "completed",
|
||||||
CreatedAt: timestamppb.Now(),
|
CreatedAt: timestamppb.Now(),
|
||||||
})
|
})
|
||||||
|
if grain.CartState != nil && grain.CartState.UserId != "" {
|
||||||
|
if pid, ok := profile.ParseProfileId(grain.CartState.UserId); ok {
|
||||||
|
_ = s.ApplyAnywhere(ctx, checkout.CheckoutId(pid), &profileMessages.LinkOrder{
|
||||||
|
OrderReference: result.OrderId,
|
||||||
|
CartId: uint64(grain.CartId),
|
||||||
|
Status: "completed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
createErr = err
|
createErr = err
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ type AppliedPromotion struct {
|
|||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Label string `json:"label,omitempty"`
|
Label string `json:"label,omitempty"`
|
||||||
Discount *Price `json:"discount,omitempty"`
|
Discount *Price `json:"discount,omitempty"`
|
||||||
|
ItemIds []uint32 `json:"itemIds,omitempty"`
|
||||||
|
|
||||||
Pending bool `json:"pending,omitempty"`
|
Pending bool `json:"pending,omitempty"`
|
||||||
Progress map[string]interface{} `json:"progress,omitempty"`
|
Progress map[string]interface{} `json:"progress,omitempty"`
|
||||||
@@ -130,8 +131,8 @@ type CartGrain struct {
|
|||||||
lastVoucherId uint32
|
lastVoucherId uint32
|
||||||
lastAccess time.Time
|
lastAccess time.Time
|
||||||
lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts)
|
lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts)
|
||||||
userId string
|
UserId string `json:"userId,omitempty"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
Version uint `json:"version"`
|
Version uint `json:"version"`
|
||||||
InventoryReserved bool `json:"inventoryReserved"`
|
InventoryReserved bool `json:"inventoryReserved"`
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ func SetUserId(grain *CartGrain, req *messages.SetUserId) error {
|
|||||||
if req.UserId == "" {
|
if req.UserId == "" {
|
||||||
return errors.New("user ID cannot be empty")
|
return errors.New("user ID cannot be empty")
|
||||||
}
|
}
|
||||||
grain.userId = req.UserId
|
grain.UserId = req.UserId
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-17
@@ -31,7 +31,7 @@ type Effect interface {
|
|||||||
// Apply mutates the cart for a qualifying action and returns the discount it
|
// Apply mutates the cart for a qualifying action and returns the discount it
|
||||||
// took (nil for non-monetary effects such as free shipping) plus whether
|
// took (nil for non-monetary effects such as free shipping) plus whether
|
||||||
// anything took effect worth recording.
|
// anything took effect worth recording.
|
||||||
Apply(g *cart.CartGrain, rule PromotionRule, a Action) (discount *cart.Price, applied bool)
|
Apply(g *cart.CartGrain, rule PromotionRule, a Action) (discount *cart.Price, itemIds []uint32, applied bool)
|
||||||
// Progress reports how far the cart is from (further) unlocking this action,
|
// Progress reports how far the cart is from (further) unlocking this action,
|
||||||
// as an open key/value payload. qualified says whether the owning rule
|
// as an open key/value payload. qualified says whether the owning rule
|
||||||
// currently applies, letting the effect distinguish "remaining to unlock"
|
// currently applies, letting the effect distinguish "remaining to unlock"
|
||||||
@@ -77,8 +77,9 @@ func (s *PromotionService) ApplyResults(g *cart.CartGrain, results []EvaluationR
|
|||||||
}
|
}
|
||||||
recorded := false
|
recorded := false
|
||||||
if res.Applicable {
|
if res.Applicable {
|
||||||
if discount, applied := eff.Apply(g, res.Rule, a); applied {
|
if discount, itemIds, applied := eff.Apply(g, res.Rule, a); applied {
|
||||||
entry.Discount = discount
|
entry.Discount = discount
|
||||||
|
entry.ItemIds = itemIds
|
||||||
recorded = true
|
recorded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,9 +112,9 @@ type percentageDiscountEffect struct{}
|
|||||||
|
|
||||||
func (percentageDiscountEffect) Type() ActionType { return ActionPercentageDiscount }
|
func (percentageDiscountEffect) Type() ActionType { return ActionPercentageDiscount }
|
||||||
|
|
||||||
func (percentageDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, bool) {
|
func (percentageDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, []uint32, bool) {
|
||||||
d := applyPercentageDiscount(g, percentFromValue(a.Value))
|
d := applyPercentageDiscount(g, percentFromValue(a.Value))
|
||||||
return d, d != nil
|
return d, nil, d != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (percentageDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
func (percentageDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
||||||
@@ -129,8 +130,8 @@ type freeShippingEffect struct{}
|
|||||||
|
|
||||||
func (freeShippingEffect) Type() ActionType { return ActionFreeShipping }
|
func (freeShippingEffect) Type() ActionType { return ActionFreeShipping }
|
||||||
|
|
||||||
func (freeShippingEffect) Apply(_ *cart.CartGrain, _ PromotionRule, _ Action) (*cart.Price, bool) {
|
func (freeShippingEffect) Apply(_ *cart.CartGrain, _ PromotionRule, _ Action) (*cart.Price, []uint32, bool) {
|
||||||
return nil, true
|
return nil, nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (freeShippingEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
func (freeShippingEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
||||||
@@ -148,9 +149,9 @@ type tieredDiscountEffect struct{}
|
|||||||
|
|
||||||
func (tieredDiscountEffect) Type() ActionType { return ActionTieredDiscount }
|
func (tieredDiscountEffect) Type() ActionType { return ActionTieredDiscount }
|
||||||
|
|
||||||
func (tieredDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, bool) {
|
func (tieredDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, []uint32, bool) {
|
||||||
d := applyTieredDiscount(g, a)
|
d := applyTieredDiscount(g, a)
|
||||||
return d, d != nil
|
return d, nil, d != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tieredDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
func (tieredDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
||||||
@@ -387,7 +388,7 @@ type unitItem struct {
|
|||||||
price money.Cents
|
price money.Cents
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*cart.Price, bool) {
|
func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*cart.Price, []uint32, bool) {
|
||||||
config := a.Config
|
config := a.Config
|
||||||
buy := int(firstNum(config, "buy"))
|
buy := int(firstNum(config, "buy"))
|
||||||
if buy <= 0 {
|
if buy <= 0 {
|
||||||
@@ -416,7 +417,7 @@ func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*c
|
|||||||
n := len(units)
|
n := len(units)
|
||||||
numFree := (n / (buy + get)) * get
|
numFree := (n / (buy + get)) * get
|
||||||
if numFree <= 0 {
|
if numFree <= 0 {
|
||||||
return nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort units by price ascending so we discount the cheapest ones
|
// Sort units by price ascending so we discount the cheapest ones
|
||||||
@@ -431,13 +432,15 @@ func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*c
|
|||||||
})
|
})
|
||||||
|
|
||||||
totalDiscount := cart.NewPrice()
|
totalDiscount := cart.NewPrice()
|
||||||
|
affectedMap := make(map[uint32]bool)
|
||||||
for i := 0; i < numFree; i++ {
|
for i := 0; i < numFree; i++ {
|
||||||
unitDiscount := scalePrice(&units[i].item.Price, discount)
|
unitDiscount := scalePrice(&units[i].item.Price, discount)
|
||||||
totalDiscount.Add(*unitDiscount)
|
totalDiscount.Add(*unitDiscount)
|
||||||
|
affectedMap[units[i].item.Id] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if totalDiscount.IncVat <= 0 {
|
if totalDiscount.IncVat <= 0 {
|
||||||
return nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
// Never discount below zero.
|
// Never discount below zero.
|
||||||
if totalDiscount.IncVat > g.TotalPrice.IncVat {
|
if totalDiscount.IncVat > g.TotalPrice.IncVat {
|
||||||
@@ -445,7 +448,14 @@ func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*c
|
|||||||
}
|
}
|
||||||
g.TotalDiscount.Add(*totalDiscount)
|
g.TotalDiscount.Add(*totalDiscount)
|
||||||
g.TotalPrice.Subtract(*totalDiscount)
|
g.TotalPrice.Subtract(*totalDiscount)
|
||||||
return totalDiscount, true
|
|
||||||
|
var itemIds []uint32
|
||||||
|
for id := range affectedMap {
|
||||||
|
itemIds = append(itemIds, id)
|
||||||
|
}
|
||||||
|
slices.Sort(itemIds)
|
||||||
|
|
||||||
|
return totalDiscount, itemIds, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buyXGetYEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
func (buyXGetYEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
||||||
@@ -460,9 +470,9 @@ type bundleDiscountEffect struct{}
|
|||||||
|
|
||||||
func (bundleDiscountEffect) Type() ActionType { return ActionBundleDiscount }
|
func (bundleDiscountEffect) Type() ActionType { return ActionBundleDiscount }
|
||||||
|
|
||||||
func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, bool) {
|
func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, []uint32, bool) {
|
||||||
if a.BundleConfig == nil {
|
if a.BundleConfig == nil {
|
||||||
return nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Represent all cart items as individual units
|
// 1. Represent all cart items as individual units
|
||||||
@@ -545,10 +555,11 @@ func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(formedBundles) == 0 {
|
if len(formedBundles) == 0 {
|
||||||
return nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
totalDiscount := cart.NewPrice()
|
totalDiscount := cart.NewPrice()
|
||||||
|
affectedMap := make(map[uint32]bool)
|
||||||
for _, bundle := range formedBundles {
|
for _, bundle := range formedBundles {
|
||||||
// Calculate the original bundle price
|
// Calculate the original bundle price
|
||||||
bundlePrice := cart.NewPrice()
|
bundlePrice := cart.NewPrice()
|
||||||
@@ -579,11 +590,14 @@ func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action)
|
|||||||
|
|
||||||
if bundleDiscount != nil && bundleDiscount.IncVat > 0 {
|
if bundleDiscount != nil && bundleDiscount.IncVat > 0 {
|
||||||
totalDiscount.Add(*bundleDiscount)
|
totalDiscount.Add(*bundleDiscount)
|
||||||
|
for _, item := range bundle {
|
||||||
|
affectedMap[item.Id] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if totalDiscount.IncVat <= 0 {
|
if totalDiscount.IncVat <= 0 {
|
||||||
return nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
// Never discount below zero.
|
// Never discount below zero.
|
||||||
if totalDiscount.IncVat > g.TotalPrice.IncVat {
|
if totalDiscount.IncVat > g.TotalPrice.IncVat {
|
||||||
@@ -591,7 +605,14 @@ func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action)
|
|||||||
}
|
}
|
||||||
g.TotalDiscount.Add(*totalDiscount)
|
g.TotalDiscount.Add(*totalDiscount)
|
||||||
g.TotalPrice.Subtract(*totalDiscount)
|
g.TotalPrice.Subtract(*totalDiscount)
|
||||||
return totalDiscount, true
|
|
||||||
|
var itemIds []uint32
|
||||||
|
for id := range affectedMap {
|
||||||
|
itemIds = append(itemIds, id)
|
||||||
|
}
|
||||||
|
slices.Sort(itemIds)
|
||||||
|
|
||||||
|
return totalDiscount, itemIds, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bundleDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
func (bundleDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
||||||
|
|||||||
@@ -184,6 +184,13 @@ func TestBuyXGetYEffect(t *testing.T) {
|
|||||||
if got := g.TotalPrice.IncVat; got != 2000 {
|
if got := g.TotalPrice.IncVat; got != 2000 {
|
||||||
t.Errorf("total price = %d, want 2000", got)
|
t.Errorf("total price = %d, want 2000", got)
|
||||||
}
|
}
|
||||||
|
if len(g.AppliedPromotions) != 1 {
|
||||||
|
t.Fatalf("expected 1 applied promotion, got %d", len(g.AppliedPromotions))
|
||||||
|
}
|
||||||
|
itemIds := g.AppliedPromotions[0].ItemIds
|
||||||
|
if len(itemIds) != 1 || itemIds[0] != 1 {
|
||||||
|
t.Errorf("expected affected itemIds [1], got %v", itemIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleDiscountEffect(t *testing.T) {
|
func TestBundleDiscountEffect(t *testing.T) {
|
||||||
@@ -247,4 +254,11 @@ func TestBundleDiscountEffect(t *testing.T) {
|
|||||||
if got := g.TotalPrice.IncVat; got != 4000 {
|
if got := g.TotalPrice.IncVat; got != 4000 {
|
||||||
t.Errorf("total price = %d, want 4000", got)
|
t.Errorf("total price = %d, want 4000", got)
|
||||||
}
|
}
|
||||||
|
if len(g.AppliedPromotions) != 1 {
|
||||||
|
t.Fatalf("expected 1 applied promotion, got %d", len(g.AppliedPromotions))
|
||||||
|
}
|
||||||
|
itemIds := g.AppliedPromotions[0].ItemIds
|
||||||
|
if len(itemIds) != 2 || itemIds[0] != 1 || itemIds[1] != 2 {
|
||||||
|
t.Errorf("expected affected itemIds [1, 2], got %v", itemIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user