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 7 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
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ 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.X.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**
* 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

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

22 changes: 20 additions & 2 deletions cart/infrastructure/InMemoryBehaviour.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ 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" {
if couponCode != "valid_voucher" && couponCode != "valid" && couponCode != "100-percent-off" {
err := errors.New("Code invalid")
return nil, nil, err
}
Expand All @@ -438,6 +438,24 @@ func (cob *InMemoryBehaviour) ApplyVoucher(ctx context.Context, cart *domaincart
Code: couponCode,
}
cart.AppliedCouponCodes = append(cart.AppliedCouponCodes, coupon)

// TODO: Move coupon logic to integration test
if couponCode == "100-percent-off" {
for delKey, delivery := range cart.Deliveries {
for itemKey, item := range delivery.Cartitems {
cart.Deliveries[delKey].Cartitems[itemKey].AppliedDiscounts = []domaincart.AppliedDiscount{{
CampaignCode: "100-percent-off",
CouponCode: "100-percent-off",
Label: "100% Off",
Applied: item.RowPriceGross.Inverse(),
Type: "coupon",
IsItemRelated: false,
SortOrder: 0,
}}
}
}
}

err := cob.cartStorage.StoreCart(cart)
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -529,7 +547,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 Down
37 changes: 27 additions & 10 deletions checkout/interfaces/controller/checkoutcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ func (cc *CheckoutController) SubmitCheckoutAction(ctx context.Context, r *web.R
return guardRedirect
}

// reserve an unique order id for later order placing
_, err := cc.applicationCartService.ReserveOrderIDAndSave(ctx, r.Session())
if err != nil {
cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error ", err)
return cc.responder.Render("checkout/carterror", nil).SetNoCache()
}

return cc.showCheckoutFormAndHandleSubmit(ctx, r, "checkout/checkout")
}

Expand Down Expand Up @@ -257,7 +264,13 @@ func (cc *CheckoutController) placeOrderAction(ctx context.Context, r *web.Reque
placedOrderInfo, _ = cc.orderService.LastPlacedOrder(ctx)
cc.orderService.ClearLastPlacedOrder(ctx)
} else {
placedOrderInfo, err = cc.orderService.CurrentCartPlaceOrderWithPaymentProcessing(ctx, session)
if decoratedCart.Cart.GrandTotal().IsZero() {
// Nothing to pay, so cart can be placed without payment processing.
placedOrderInfo, err = cc.orderService.CurrentCartPlaceOrder(ctx, session, placeorder.Payment{})
} else {
placedOrderInfo, err = cc.orderService.CurrentCartPlaceOrderWithPaymentProcessing(ctx, session)
}

cc.orderService.ClearLastPlacedOrder(ctx)

if err != nil {
Expand Down Expand Up @@ -456,20 +469,18 @@ func getViewErrorInfo(err error) ViewErrorInfos {
func (cc *CheckoutController) processPayment(ctx context.Context, r *web.Request) web.Result {
session := web.SessionFromContext(ctx)

// reserve an unique order id for later order placing
_, err := cc.applicationCartService.ReserveOrderIDAndSave(ctx, session)
if err != nil {
cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error ", err)
return cc.responder.Render("checkout/carterror", nil).SetNoCache()
}

// guard clause if cart can not be fetched
decoratedCart, err := cc.applicationCartReceiverService.ViewDecoratedCart(ctx, r.Session())
if err != nil {
cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error ", err)
return cc.responder.Render("checkout/carterror", nil).SetNoCache()
}

// Cart grand total is zero, so no payment needed.
if decoratedCart.Cart.GrandTotal().IsZero() {
return cc.responder.RouteRedirect("checkout.placeorder", nil)
}

// get the payment gateway for the specified payment selection
gateway, err := cc.orderService.GetPaymentGateway(ctx, decoratedCart.Cart.PaymentSelection.Gateway())
if err != nil {
Expand Down Expand Up @@ -542,8 +553,14 @@ func (cc *CheckoutController) ReviewAction(ctx context.Context, r *web.Request)
}

//Everything valid then return
if canProceed && err == nil && decoratedCart.Cart.IsPaymentSelected() {
return cc.processPayment(ctx, r)
if canProceed && err == nil {
if decoratedCart.Cart.IsPaymentSelected() {
return cc.processPayment(ctx, r)
}

if decoratedCart.Cart.GrandTotal().IsZero() {
return cc.responder.RouteRedirect("checkout.placeorder", nil)
}
}

return cc.responder.Render("checkout/review", viewData).SetNoCache()
Expand Down
29 changes: 16 additions & 13 deletions checkout/interfaces/controller/forms/checkoutform.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ func (c *CheckoutFormController) HandleFormAction(ctx context.Context, r *web.Re
return checkoutFormBuilder.getForm(), false, err
}

cart, err := c.applicationCartReceiverService.ViewCart(ctx, r.Session())
if err != nil {
return checkoutFormBuilder.getForm(), false, err
}

//1, #### Process and add Billing Form Controller result
billingForm, success, err := c.billingAddressFormController.HandleFormAction(ctx, newRequestWithResolvedNamespace("billingAddress", r))
overallSuccess = overallSuccess && success
Expand All @@ -189,10 +194,6 @@ func (c *CheckoutFormController) HandleFormAction(ctx context.Context, r *web.Re
if c.useDeliveryForms {
// 2. #### Process ALL the delivery forms:
// Add a Delivery Form for every delivery:
cart, err := c.applicationCartReceiverService.ViewCart(ctx, r.Session())
if err != nil {
return checkoutFormBuilder.getForm(), false, err
}
for _, delivery := range cart.Deliveries {
if !delivery.HasItems() {
continue
Expand Down Expand Up @@ -226,15 +227,17 @@ func (c *CheckoutFormController) HandleFormAction(ctx context.Context, r *web.Re
}
}

//4. ### Add the simplePaymentForm
simplePaymentForm, success, err := c.simplePaymentFormController.HandleFormAction(ctx, newRequestWithResolvedNamespace("payment", r))
overallSuccess = overallSuccess && success
if err != nil {
return checkoutFormBuilder.getForm(), false, err
}
err = checkoutFormBuilder.addSimplePaymentForm(simplePaymentForm)
if err != nil {
return checkoutFormBuilder.getForm(), false, err
if !cart.GrandTotal().IsZero() {
//4. ### Add the simplePaymentForm if payment is required.
simplePaymentForm, success, err := c.simplePaymentFormController.HandleFormAction(ctx, newRequestWithResolvedNamespace("payment", r))
overallSuccess = overallSuccess && success
if err != nil {
return checkoutFormBuilder.getForm(), false, err
}
err = checkoutFormBuilder.addSimplePaymentForm(simplePaymentForm)
if err != nil {
return checkoutFormBuilder.getForm(), false, err
}
}

return checkoutFormBuilder.getForm(), overallSuccess, nil
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ require (
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/go-playground/assert.v1 v1.2.1
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
gotest.tools v2.2.0+incompatible // indirect
gotest.tools v2.2.0+incompatible
)

replace (
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,12 @@ github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRU
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
Expand Down Expand Up @@ -583,6 +585,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e h1:TsjK5I7fXk8f2FQrgu6NS7i5Qih3knl2FL1htyguLRE=
golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -684,6 +687,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
2 changes: 1 addition & 1 deletion price/domain/price.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func (p Price) IsPayable() bool {

//IsZero - returns true if the price represents zero value
func (p Price) IsZero() bool {
return p.Equal(NewZero(p.Currency())) || p.Equal(NewFromFloat(0, p.Currency()))
return p.LikelyEqual(NewZero(p.Currency())) || p.LikelyEqual(NewFromFloat(0, p.Currency()))
}

//FloatAmount gets the current amount as float
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
// +build integration

package rest_test
package frontend_test

import (
"testing"

"gopkg.in/go-playground/assert.v1"

"flamingo.me/flamingo-commerce/v3/test/integrationtest"
"flamingo.me/flamingo-commerce/v3/test/integrationtest/testhelper"
)

func Test_AddToCart(t *testing.T) {
func Test_Cart_AddToCart(t *testing.T) {
t.Run("adding simple product", func(t *testing.T) {
e := integrationtest.NewHTTPExpect(t, "http://"+FlamingoURL)

testhelper.CartAddProduct(e, "fake_simple", 5, "", "")
item := testhelper.CartGetItems(e).MustContain(t, "fake_simple")
CartAddProduct(t, e, "fake_simple", 5, "", "")
item := CartGetItems(t, e).MustContain(t, "fake_simple")

assert.Equal(t, 5, item.Qty)
})

t.Run("adding configurable product", func(t *testing.T) {
e := integrationtest.NewHTTPExpect(t, "http://"+FlamingoURL)

testhelper.CartAddProduct(e, "fake_configurable", 3, "shirt-red-s", "")
item := testhelper.CartGetItems(e).MustContain(t, "fake_configurable")
CartAddProduct(t, e, "fake_configurable", 3, "shirt-red-s", "")
item := CartGetItems(t, e).MustContain(t, "fake_configurable")

assert.Equal(t, 3, item.Qty)
})
Expand Down
Loading