Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

checkout: Allow fully discounted carts to be placed. #198

Merged
merged 13 commits into from
Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ the cart has been adjusted and written back to cache
**search**
* Extend `Suggestion` struct with `Type` and `AdditionalAttributes` to be able to distinguish between product/category suggestions

## v3.1.1 [upcoming]
## v3.2.X [upcoming]
**w3cdatalayer**
* Fixed a bug that causes the datalayer to panic if it failed to build an absolute url
* Fixed a bug that causes the datalayer to panic if it failed to build an absolute url

**checkout**
* Controller
* Allow checkout for fully discounted carts without payment processing. Previously all checkouts needed a valid payment to continue.
In case there is nothing to pay this can be skipped.
* Order ID will be reserved as soon as the user hits the checkout previously it was done before starting the payment
* GraphQL
* Update place order process to also allow zero carts which don't need payment, this leads to a state flow that lacks the payment steps.
See module readme for further details.

**cart**
* inMemoryBehaviour: Allow custom logic for GiftCard / Voucher handling
* We introduced two new interfaces `GiftCardHandler` + `VoucherHandler`
* This enables users of the in-memory cart to add project specific gift card and voucher handling

**price**
* IsZero() now uses LikelyEqual() instead of Equal() to avoid issues occurring due to floating-point arithmetic

6 changes: 5 additions & 1 deletion cart/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,11 @@ injector.Bind((*cart.CustomerCartService)(nil)).To(infrastructure.YourAdapter{})
Most of the cart modification methods are part of the `ModifyBehaviour` interface - if you look at the secondary ports you will see, that they need to return an (initialized) implementation of the
`ModifyBehaviour` interface - so in fact this interface needs to be implemented when writing an adapter as well.

There is a "InMemoryAdapter" implementation as part of the package.
**in-memory cart adapter**
There is a "InMemoryAdapter" implementation as part of the package. It allows basic cart operations with a cart that is stored in memory.
Since the cart storage is not persisted in any way we currently recommend the usage only for demo / testing.

The in memory adapter supports custom gift card / voucher logic by implementing the `GiftCardHandler` and `VoucherHandler` interfaces.

**PlaceOrderService**

Expand Down
2 changes: 2 additions & 0 deletions cart/application/cartService_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,8 @@ func (m *MockGuestCartServiceWithModifyBehaviour) GetModifyBehaviour(context.Con
},
nil,
nil,
nil,
nil,
)

return cob, nil
Expand Down
140 changes: 104 additions & 36 deletions cart/infrastructure/InMemoryBehaviour.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import (
"math/rand"
"strconv"

domaincart "flamingo.me/flamingo-commerce/v3/cart/domain/cart"
"flamingo.me/flamingo-commerce/v3/cart/domain/events"
priceDomain "flamingo.me/flamingo-commerce/v3/price/domain"

domaincart "flamingo.me/flamingo-commerce/v3/cart/domain/cart"
"flamingo.me/flamingo-commerce/v3/product/domain"
"flamingo.me/flamingo/v3/framework/flamingo"
"github.com/pkg/errors"
Expand All @@ -25,22 +24,44 @@ type (
itemBuilderProvider domaincart.ItemBuilderProvider
deliveryBuilderProvider domaincart.DeliveryBuilderProvider
cartBuilderProvider domaincart.BuilderProvider
giftCardHandler GiftCardHandler
voucherHandler VoucherHandler
defaultTaxRate float64
}

//CartStorage Interface - might be implemented by other persistence types later as well
// CartStorage Interface - might be implemented by other persistence types later as well
CartStorage interface {
GetCart(id string) (*domaincart.Cart, error)
HasCart(id string) bool
StoreCart(cart *domaincart.Cart) error
RemoveCart(cart *domaincart.Cart) error
}

// GiftCardHandler enables the projects to have specific GiftCard handling within the in-memory cart
GiftCardHandler interface {
ApplyGiftCard(ctx context.Context, cart *domaincart.Cart, giftCardCode string) (*domaincart.Cart, error)
RemoveGiftCard(ctx context.Context, cart *domaincart.Cart, giftCardCode string) (*domaincart.Cart, error)
}

// VoucherHandler enables the projects to have specific Voucher handling within the in-memory cart
VoucherHandler interface {
ApplyVoucher(ctx context.Context, cart *domaincart.Cart, couponCode string) (*domaincart.Cart, error)
RemoveVoucher(ctx context.Context, cart *domaincart.Cart, couponCode string) (*domaincart.Cart, error)
}

// DefaultGiftCardHandler implements a basic gift card handler
DefaultGiftCardHandler struct{}

// DefaultVoucherHandler implements a basic voucher handler
DefaultVoucherHandler struct{}
)

var (
_ domaincart.ModifyBehaviour = (*InMemoryBehaviour)(nil)
_ domaincart.GiftCardAndVoucherBehaviour = (*InMemoryBehaviour)(nil)
_ domaincart.CompleteBehaviour = (*InMemoryBehaviour)(nil)
_ GiftCardHandler = (*DefaultGiftCardHandler)(nil)
_ VoucherHandler = (*DefaultVoucherHandler)(nil)
)

// Inject dependencies
Expand All @@ -52,6 +73,8 @@ func (cob *InMemoryBehaviour) Inject(
deliveryBuilderProvider domaincart.DeliveryBuilderProvider,
cartBuilderProvider domaincart.BuilderProvider,
eventPublisher events.EventPublisher,
voucherHandler VoucherHandler,
giftCardHandler GiftCardHandler,
config *struct {
DefaultTaxRate float64 `inject:"config:commerce.cart.inMemoryCartServiceAdapter.defaultTaxRate,optional"`
},
Expand All @@ -62,6 +85,8 @@ func (cob *InMemoryBehaviour) Inject(
cob.itemBuilderProvider = itemBuilderProvider
cob.deliveryBuilderProvider = deliveryBuilderProvider
cob.cartBuilderProvider = cartBuilderProvider
cob.voucherHandler = voucherHandler
cob.giftCardHandler = giftCardHandler
if config != nil {
cob.defaultTaxRate = config.DefaultTaxRate
}
Expand Down Expand Up @@ -429,16 +454,13 @@ func (cob *InMemoryBehaviour) storeCart(cart *domaincart.Cart) error {

// ApplyVoucher applies a voucher to the cart
func (cob *InMemoryBehaviour) ApplyVoucher(ctx context.Context, cart *domaincart.Cart, couponCode string) (*domaincart.Cart, domaincart.DeferEvents, error) {
if couponCode != "valid_voucher" && couponCode != "valid" {
err := errors.New("Code invalid")
cart, err := cob.voucherHandler.ApplyVoucher(ctx, cart, couponCode)

if err != nil {
return nil, nil, err
}

coupon := domaincart.CouponCode{
Code: couponCode,
}
cart.AppliedCouponCodes = append(cart.AppliedCouponCodes, coupon)
err := cob.cartStorage.StoreCart(cart)
err = cob.cartStorage.StoreCart(cart)
if err != nil {
return nil, nil, err
}
Expand All @@ -460,16 +482,12 @@ func (cob *InMemoryBehaviour) ApplyAny(ctx context.Context, cart *domaincart.Car

// RemoveVoucher removes a voucher from the cart
func (cob *InMemoryBehaviour) RemoveVoucher(ctx context.Context, cart *domaincart.Cart, couponCode string) (*domaincart.Cart, domaincart.DeferEvents, error) {
for i, coupon := range cart.AppliedCouponCodes {
if coupon.Code == couponCode {
cart.AppliedCouponCodes[i] = cart.AppliedCouponCodes[len(cart.AppliedCouponCodes)-1]
cart.AppliedCouponCodes[len(cart.AppliedCouponCodes)-1] = domaincart.CouponCode{}
cart.AppliedCouponCodes = cart.AppliedCouponCodes[:len(cart.AppliedCouponCodes)-1]
break
}
cart, err := cob.voucherHandler.RemoveVoucher(ctx, cart, couponCode)
if err != nil {
return nil, nil, err
}

err := cob.cartStorage.StoreCart(cart)
err = cob.cartStorage.StoreCart(cart)
if err != nil {
return nil, nil, err
}
Expand All @@ -480,18 +498,12 @@ func (cob *InMemoryBehaviour) RemoveVoucher(ctx context.Context, cart *domaincar
// ApplyGiftCard applies a gift card to the cart
// if a GiftCard is applied, it will be added to the array AppliedGiftCards on the cart
func (cob *InMemoryBehaviour) ApplyGiftCard(ctx context.Context, cart *domaincart.Cart, giftCardCode string) (*domaincart.Cart, domaincart.DeferEvents, error) {
if giftCardCode != "valid_giftcard" && giftCardCode != "valid" {
err := errors.New("Code invalid")
cart, err := cob.giftCardHandler.ApplyGiftCard(ctx, cart, giftCardCode)
if err != nil {
return nil, nil, err
}

giftCard := domaincart.AppliedGiftCard{
Code: giftCardCode,
Applied: priceDomain.NewFromInt(10, 100, "$"),
Remaining: priceDomain.NewFromInt(0, 100, "$"),
}
cart.AppliedGiftCards = append(cart.AppliedGiftCards, giftCard)
err := cob.cartStorage.StoreCart(cart)
err = cob.cartStorage.StoreCart(cart)
if err != nil {
return nil, nil, err
}
Expand All @@ -501,16 +513,12 @@ func (cob *InMemoryBehaviour) ApplyGiftCard(ctx context.Context, cart *domaincar
// RemoveGiftCard removes a gift card from the cart
// if a GiftCard is removed, it will be removed from the array AppliedGiftCards on the cart
func (cob *InMemoryBehaviour) RemoveGiftCard(ctx context.Context, cart *domaincart.Cart, giftCardCode string) (*domaincart.Cart, domaincart.DeferEvents, error) {
for i, giftcard := range cart.AppliedGiftCards {
if giftcard.Code == giftCardCode {
cart.AppliedGiftCards[i] = cart.AppliedGiftCards[len(cart.AppliedGiftCards)-1]
cart.AppliedGiftCards[len(cart.AppliedGiftCards)-1] = domaincart.AppliedGiftCard{}
cart.AppliedGiftCards = cart.AppliedGiftCards[:len(cart.AppliedGiftCards)-1]
break
}
cart, err := cob.giftCardHandler.RemoveGiftCard(ctx, cart, giftCardCode)
if err != nil {
return nil, nil, err
}

err := cob.cartStorage.StoreCart(cart)
err = cob.cartStorage.StoreCart(cart)
if err != nil {
return nil, nil, err
}
Expand All @@ -529,7 +537,7 @@ func (cob *InMemoryBehaviour) checkPaymentSelection(ctx context.Context, cart *d
}
paymentSelectionTotal := paymentSelection.TotalValue()

if !cart.GrandTotal().Equal(paymentSelectionTotal) {
if !cart.GrandTotal().LikelyEqual(paymentSelectionTotal) {
return errors.New("Payment Total does not match with Grandtotal")
}
return nil
Expand All @@ -549,3 +557,63 @@ func (cob *InMemoryBehaviour) resetPaymentSelectionIfInvalid(ctx context.Context

return cart, nil, nil
}

// ApplyVoucher checks the voucher and adds the voucher to the supplied cart if valid
func (DefaultVoucherHandler) ApplyVoucher(_ context.Context, cart *domaincart.Cart, couponCode string) (*domaincart.Cart, error) {
if couponCode != "valid_voucher" && couponCode != "valid" {
err := errors.New("Code invalid")
return nil, err
carstendietrich marked this conversation as resolved.
Show resolved Hide resolved
}

coupon := domaincart.CouponCode{
Code: couponCode,
}

cart.AppliedCouponCodes = append(cart.AppliedCouponCodes, coupon)
return cart, nil
}

// RemoveVoucher removes the voucher from the cart if possible
func (DefaultVoucherHandler) RemoveVoucher(_ context.Context, cart *domaincart.Cart, couponCode string) (*domaincart.Cart, error) {
for i, coupon := range cart.AppliedCouponCodes {
if coupon.Code == couponCode {
cart.AppliedCouponCodes[i] = cart.AppliedCouponCodes[len(cart.AppliedCouponCodes)-1]
cart.AppliedCouponCodes[len(cart.AppliedCouponCodes)-1] = domaincart.CouponCode{}
cart.AppliedCouponCodes = cart.AppliedCouponCodes[:len(cart.AppliedCouponCodes)-1]
return cart, nil
}
}

return cart, nil
}

// ApplyGiftCard checks the gift card and adds it to the supplied cart if valid
func (DefaultGiftCardHandler) ApplyGiftCard(_ context.Context, cart *domaincart.Cart, giftCardCode string) (*domaincart.Cart, error) {
if giftCardCode != "valid_giftcard" && giftCardCode != "valid" {
err := errors.New("Code invalid")
return nil, err
carstendietrich marked this conversation as resolved.
Show resolved Hide resolved
}

giftCard := domaincart.AppliedGiftCard{
Code: giftCardCode,
Applied: priceDomain.NewFromInt(10, 100, "$"),
Remaining: priceDomain.NewFromInt(0, 100, "$"),
}
cart.AppliedGiftCards = append(cart.AppliedGiftCards, giftCard)

return cart, nil
}

// RemoveGiftCard removes the gift card from the cart if possible
func (DefaultGiftCardHandler) RemoveGiftCard(_ context.Context, cart *domaincart.Cart, giftCardCode string) (*domaincart.Cart, error) {
for i, giftcard := range cart.AppliedGiftCards {
if giftcard.Code == giftCardCode {
cart.AppliedGiftCards[i] = cart.AppliedGiftCards[len(cart.AppliedGiftCards)-1]
cart.AppliedGiftCards[len(cart.AppliedGiftCards)-1] = domaincart.AppliedGiftCard{}
cart.AppliedGiftCards = cart.AppliedGiftCards[:len(cart.AppliedGiftCards)-1]
return cart, nil
}
}

return cart, nil
}
16 changes: 16 additions & 0 deletions cart/infrastructure/InMemoryBehaviour_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func TestInMemoryBehaviour_CleanCart(t *testing.T) {
},
nil,
nil,
nil,
nil,
)
cart := &domaincart.Cart{
ID: "17",
Expand Down Expand Up @@ -174,6 +176,8 @@ func TestInMemoryBehaviour_CleanDelivery(t *testing.T) {
},
nil,
nil,
nil,
nil,
)
if err := cob.cartStorage.StoreCart(tt.args.cart); err != nil {
t.Fatalf("cart could not be initialized")
Expand Down Expand Up @@ -241,6 +245,8 @@ func TestInMemoryBehaviour_ApplyVoucher(t *testing.T) {
nil,
nil,
nil,
&DefaultVoucherHandler{},
&DefaultGiftCardHandler{},
nil,
)
got, _, err := cob.ApplyVoucher(context.Background(), tt.args.cart, tt.args.voucherCode)
Expand Down Expand Up @@ -334,6 +340,8 @@ func TestInMemoryBehaviour_RemoveVoucher(t *testing.T) {
return &domaincart.Builder{}
},
nil,
&DefaultVoucherHandler{},
&DefaultGiftCardHandler{},
nil,
)

Expand Down Expand Up @@ -399,6 +407,8 @@ func TestInMemoryBehaviour_ApplyGiftCard(t *testing.T) {
nil,
nil,
nil,
&DefaultVoucherHandler{},
&DefaultGiftCardHandler{},
nil,
)
got, _, err := cob.ApplyGiftCard(context.Background(), tt.args.cart, tt.args.giftCardCode)
Expand Down Expand Up @@ -466,6 +476,8 @@ func TestInMemoryBehaviour_RemoveGiftCard(t *testing.T) {
nil,
nil,
nil,
&DefaultVoucherHandler{},
&DefaultGiftCardHandler{},
nil,
)
got, _, err := cob.RemoveGiftCard(context.Background(), tt.args.cart, tt.args.giftCardCode)
Expand All @@ -492,6 +504,8 @@ func TestInMemoryBehaviour_Complete(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
)
cart := &domaincart.Cart{ID: "test-id"}
require.NoError(t, cob.storeCart(cart))
Expand All @@ -517,6 +531,8 @@ func TestInMemoryBehaviour_Restore(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
)
cart := &domaincart.Cart{ID: "1234"}

Expand Down
2 changes: 1 addition & 1 deletion cart/infrastructure/inMemoryStorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type (
}
)

/** Implementation fo the storage **/
var _ CartStorage = &InMemoryCartStorage{}

func (s *InMemoryCartStorage) init() {
if s.guestCarts == nil {
Expand Down
2 changes: 2 additions & 0 deletions cart/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func (m *Module) Inject(
func (m *Module) Configure(injector *dingo.Injector) {
if m.useInMemoryCart {
injector.Bind((*infrastructure.CartStorage)(nil)).To(infrastructure.InMemoryCartStorage{}).AsEagerSingleton()
injector.Bind((*infrastructure.GiftCardHandler)(nil)).To(infrastructure.DefaultGiftCardHandler{})
injector.Bind((*infrastructure.VoucherHandler)(nil)).To(infrastructure.DefaultVoucherHandler{})
injector.Bind((*cart.GuestCartService)(nil)).To(infrastructure.InMemoryGuestCartService{})
injector.Bind((*cart.CustomerCartService)(nil)).To(infrastructure.InMemoryCustomerCartService{})
}
Expand Down
6 changes: 5 additions & 1 deletion checkout/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,14 @@ The start and failed states are defined by an annotated binding with annotations
Exposed states implement the interface `checkout/interfaces/graphql/dto.State`. To map internal states to exposed states,
we use a map binding on the `dto.State` interface with the internal state names as keys and the exposed state as target.

The default implementation defines the state flow as follows:
The default implementation defines the state flow as follows (for an cart that needs payment):

![](domain/placeorder/states/transitions.png)

Fully discounted carts don't need a payment, therefore the state flow is similar but lacks the payment creation/validation:

![](domain/placeorder/states/transitions_zeropay.png)

### Context store

The place order context must be stored aside of the session, since it is manipulated by a background process.
Expand Down
Loading