flamingo.me/flamingo-commerce/v3@v3.11.0/checkout/interfaces/controller/checkoutcontroller.go (about)

     1  package controller
     2  
     3  import (
     4  	"context"
     5  	"encoding/gob"
     6  	"errors"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  
    11  	"go.opencensus.io/trace"
    12  
    13  	"flamingo.me/flamingo/v3/core/auth"
    14  	"flamingo.me/flamingo/v3/framework/flamingo"
    15  	"flamingo.me/flamingo/v3/framework/web"
    16  
    17  	cartApplication "flamingo.me/flamingo-commerce/v3/cart/application"
    18  	"flamingo.me/flamingo-commerce/v3/cart/domain/cart"
    19  	"flamingo.me/flamingo-commerce/v3/cart/domain/decorator"
    20  	"flamingo.me/flamingo-commerce/v3/cart/domain/placeorder"
    21  	"flamingo.me/flamingo-commerce/v3/cart/domain/validation"
    22  	"flamingo.me/flamingo-commerce/v3/checkout/application"
    23  	"flamingo.me/flamingo-commerce/v3/checkout/interfaces/controller/forms"
    24  	paymentDomain "flamingo.me/flamingo-commerce/v3/payment/domain"
    25  )
    26  
    27  const (
    28  	// CheckoutErrorFlashKey is the flash key that stores error infos that are shown on the checkout form page
    29  	CheckoutErrorFlashKey = "checkout.error.data"
    30  	// CheckoutSuccessFlashKey is the flash key that stores the order infos which are used on the checkout success page
    31  	CheckoutSuccessFlashKey = "checkout.success.data"
    32  )
    33  
    34  type (
    35  	// CheckoutViewData represents the checkout view data
    36  	CheckoutViewData struct {
    37  		DecoratedCart        decorator.DecoratedCart
    38  		Form                 forms.CheckoutFormComposite
    39  		CartValidationResult validation.Result
    40  		ErrorInfos           ViewErrorInfos
    41  		AvailablePayments    map[string][]paymentDomain.Method
    42  		CustomerLoggedIn     bool
    43  	}
    44  
    45  	// ViewErrorInfos defines the error info struct of the checkout controller views
    46  	ViewErrorInfos struct {
    47  		// HasError  indicates that an general error happened
    48  		HasError bool
    49  		// If there is a general error this field is filled and can be used in the template
    50  		ErrorMessage string
    51  		// if the Error happens during processing payment (can be used in template to behave special in case of payment errors)
    52  		HasPaymentError bool
    53  		// if payment error occurred holds additional infos
    54  		PaymentErrorCode string
    55  	}
    56  
    57  	// SuccessViewData represents the success view data
    58  	SuccessViewData struct {
    59  		PaymentInfos        []application.PlaceOrderPaymentInfo
    60  		PlacedOrderInfos    placeorder.PlacedOrderInfos
    61  		Email               string
    62  		PlacedDecoratedCart decorator.DecoratedCart
    63  	}
    64  
    65  	// ReviewStepViewData represents the success view data
    66  	ReviewStepViewData struct {
    67  		DecoratedCart decorator.DecoratedCart
    68  		ErrorInfos    ViewErrorInfos
    69  	}
    70  
    71  	// PaymentStepViewData represents the payment flow view data
    72  	PaymentStepViewData struct {
    73  		FlowStatus paymentDomain.FlowStatus
    74  		ErrorInfos ViewErrorInfos
    75  	}
    76  
    77  	// PlaceOrderFlashData represents the data passed to the success page - they need to be "glob"able
    78  	PlaceOrderFlashData struct {
    79  		PlacedOrderInfos placeorder.PlacedOrderInfos
    80  		Email            string
    81  		PaymentInfos     []application.PlaceOrderPaymentInfo
    82  		PlacedCart       cart.Cart
    83  	}
    84  
    85  	// EmptyCartInfo struct defines the data info on empty carts
    86  	EmptyCartInfo struct {
    87  		CartExpired bool
    88  	}
    89  
    90  	// CheckoutController represents the checkout controller with its injections
    91  	CheckoutController struct {
    92  		responder *web.Responder
    93  		router    *web.Router
    94  
    95  		orderService         *application.OrderService
    96  		decoratedCartFactory *decorator.DecoratedCartFactory
    97  
    98  		skipStartAction                 bool
    99  		skipReviewAction                bool
   100  		showReviewStepAfterPaymentError bool
   101  		showEmptyCartPageIfNoItems      bool
   102  		redirectToCartOnInvalideCart    bool
   103  		privacyPolicyRequired           bool
   104  
   105  		devMode bool
   106  
   107  		applicationCartService         *cartApplication.CartService
   108  		applicationCartReceiverService *cartApplication.CartReceiverService
   109  
   110  		webIdentityService *auth.WebIdentityService
   111  		logger             flamingo.Logger
   112  
   113  		checkoutFormController *forms.CheckoutFormController
   114  	}
   115  )
   116  
   117  func init() {
   118  	gob.Register(PlaceOrderFlashData{})
   119  	gob.Register(ViewErrorInfos{})
   120  }
   121  
   122  // Inject dependencies
   123  func (cc *CheckoutController) Inject(
   124  	responder *web.Responder,
   125  	router *web.Router,
   126  	orderService *application.OrderService,
   127  	decoratedCartFactory *decorator.DecoratedCartFactory,
   128  	applicationCartService *cartApplication.CartService,
   129  	applicationCartReceiverService *cartApplication.CartReceiverService,
   130  	webIdentityService *auth.WebIdentityService,
   131  	logger flamingo.Logger,
   132  	checkoutFormController *forms.CheckoutFormController,
   133  	config *struct {
   134  		SkipStartAction                 bool `inject:"config:commerce.checkout.skipStartAction,optional"`
   135  		SkipReviewAction                bool `inject:"config:commerce.checkout.skipReviewAction,optional"`
   136  		ShowReviewStepAfterPaymentError bool `inject:"config:commerce.checkout.showReviewStepAfterPaymentError,optional"`
   137  		ShowEmptyCartPageIfNoItems      bool `inject:"config:commerce.checkout.showEmptyCartPageIfNoItems,optional"`
   138  		RedirectToCartOnInvalideCart    bool `inject:"config:commerce.checkout.redirectToCartOnInvalidCart,optional"`
   139  		PrivacyPolicyRequired           bool `inject:"config:commerce.checkout.privacyPolicyRequired,optional"`
   140  		DevMode                         bool `inject:"config:debug.mode,optional"`
   141  	},
   142  ) {
   143  	cc.responder = responder
   144  	cc.router = router
   145  
   146  	cc.checkoutFormController = checkoutFormController
   147  	cc.orderService = orderService
   148  	cc.decoratedCartFactory = decoratedCartFactory
   149  
   150  	cc.skipStartAction = config.SkipStartAction
   151  	cc.skipReviewAction = config.SkipReviewAction
   152  	cc.showReviewStepAfterPaymentError = config.ShowReviewStepAfterPaymentError
   153  	cc.showEmptyCartPageIfNoItems = config.ShowEmptyCartPageIfNoItems
   154  	cc.redirectToCartOnInvalideCart = config.RedirectToCartOnInvalideCart
   155  	cc.privacyPolicyRequired = config.PrivacyPolicyRequired
   156  
   157  	cc.devMode = config.DevMode
   158  
   159  	cc.applicationCartService = applicationCartService
   160  	cc.applicationCartReceiverService = applicationCartReceiverService
   161  
   162  	cc.webIdentityService = webIdentityService
   163  	cc.logger = logger.WithField(flamingo.LogKeyModule, "checkout").WithField(flamingo.LogKeyCategory, "checkoutController")
   164  }
   165  
   166  /*
   167  The checkoutController implements a default process for a checkout:
   168   * StartAction (supposed to show a switch to go to guest or customer)
   169   	* can be skipped with a configuration
   170   * SubmitCheckoutAction
   171   	* This step is supposed to show a big form (validation and default values are configurable as well)
   172  	* payment can be selected in this step or in the next
   173  	* In cases a customer is logged in the form is pre populated
   174   * ReviewAction (can be skipped through configuration)
   175  	* this step is supposed to show the current cart status just before checkout
   176  		* optional the paymentmethod can also be selected here
   177   * PaymentAction
   178  	* Payment gets initialized in SubmitCheckoutAction or ReviewAction
   179  	* Handles different payment stages and reacts to payment status provided by gateway
   180   * PlaceOrderAction
   181  	* Place the order if not already placed
   182  	* Add Information about the order to the session flash messages
   183   * SuccessStep
   184  	* Displays order success page containing the order infos from the previously set flash message
   185  */
   186  
   187  // StartAction handles the checkout start action
   188  func (cc *CheckoutController) StartAction(ctx context.Context, r *web.Request) web.Result {
   189  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/StartAction")
   190  	defer span.End()
   191  
   192  	// Guard Clause if Cart cannot be fetched
   193  	decoratedCart, e := cc.applicationCartReceiverService.ViewDecoratedCart(ctx, r.Session())
   194  	if e != nil {
   195  		cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.viewaction: Error ", e)
   196  		return cc.responder.Render("checkout/carterror", nil).SetNoCache()
   197  	}
   198  	guardRedirect := cc.getCommonGuardRedirects(ctx, r.Session(), decoratedCart)
   199  	if guardRedirect != nil {
   200  		return guardRedirect
   201  	}
   202  
   203  	viewData := cc.getBasicViewData(ctx, r, *decoratedCart)
   204  	// Guard Clause if Cart is empty
   205  	if decoratedCart.Cart.ItemCount() == 0 {
   206  		if cc.showEmptyCartPageIfNoItems {
   207  			return cc.responder.Render("checkout/emptycart", nil)
   208  		}
   209  		return cc.responder.Render("checkout/startcheckout", viewData).SetNoCache()
   210  	}
   211  
   212  	if cc.webIdentityService.Identify(ctx, r) != nil {
   213  		return cc.responder.RouteRedirect("checkout", nil)
   214  	}
   215  
   216  	if cc.skipStartAction {
   217  		return cc.responder.RouteRedirect("checkout", nil)
   218  	}
   219  
   220  	return cc.responder.Render("checkout/startcheckout", viewData).SetNoCache()
   221  }
   222  
   223  // SubmitCheckoutAction handles the main checkout
   224  func (cc *CheckoutController) SubmitCheckoutAction(ctx context.Context, r *web.Request) web.Result {
   225  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/SubmitCheckoutAction")
   226  	defer span.End()
   227  
   228  	// Guard Clause if Cart can not be fetched
   229  	decoratedCart, e := cc.applicationCartReceiverService.ViewDecoratedCart(ctx, r.Session())
   230  	if e != nil {
   231  		cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error ", e)
   232  		return cc.responder.Render("checkout/carterror", nil).SetNoCache()
   233  	}
   234  	guardRedirect := cc.getCommonGuardRedirects(ctx, r.Session(), decoratedCart)
   235  	if guardRedirect != nil {
   236  		return guardRedirect
   237  	}
   238  
   239  	// reserve an unique order id for later order placing
   240  	_, err := cc.applicationCartService.ReserveOrderIDAndSave(ctx, r.Session())
   241  	if err != nil {
   242  		cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error ", err)
   243  		return cc.responder.Render("checkout/carterror", nil).SetNoCache()
   244  	}
   245  
   246  	return cc.showCheckoutFormAndHandleSubmit(ctx, r, "checkout/checkout")
   247  }
   248  
   249  // PlaceOrderAction functions as a return/notification URL for Payment Providers
   250  func (cc *CheckoutController) PlaceOrderAction(ctx context.Context, r *web.Request) web.Result {
   251  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/PlaceOrderAction")
   252  	defer span.End()
   253  
   254  	session := web.SessionFromContext(ctx)
   255  
   256  	decoratedCart, err := cc.orderService.LastPlacedOrCurrentCart(ctx)
   257  	if err != nil {
   258  		cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.placeorderaction: Error ", err)
   259  		return cc.responder.Render("checkout/carterror", nil).SetNoCache()
   260  	}
   261  
   262  	guardRedirect := cc.getCommonGuardRedirects(ctx, session, decoratedCart)
   263  	if guardRedirect != nil {
   264  		return guardRedirect
   265  	}
   266  
   267  	if cc.showEmptyCartPageIfNoItems && decoratedCart.Cart.ItemCount() == 0 {
   268  		return cc.responder.Render("checkout/emptycart", nil).SetNoCache()
   269  	}
   270  	return cc.placeOrderAction(ctx, r, session, decoratedCart)
   271  }
   272  
   273  func (cc *CheckoutController) placeOrderAction(ctx context.Context, r *web.Request, session *web.Session, decoratedCart *decorator.DecoratedCart) web.Result {
   274  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/placeOrderAction")
   275  	defer span.End()
   276  
   277  	var placedOrderInfo *application.PlaceOrderInfo
   278  
   279  	var err error
   280  
   281  	if cc.orderService.HasLastPlacedOrder(ctx) {
   282  		placedOrderInfo, _ = cc.orderService.LastPlacedOrder(ctx)
   283  		cc.orderService.ClearLastPlacedOrder(ctx)
   284  	} else {
   285  		if decoratedCart.Cart.GrandTotal.IsZero() {
   286  			// Nothing to pay, so cart can be placed without payment processing.
   287  			placedOrderInfo, err = cc.orderService.CurrentCartPlaceOrder(ctx, session, placeorder.Payment{})
   288  		} else {
   289  			placedOrderInfo, err = cc.orderService.CurrentCartPlaceOrderWithPaymentProcessing(ctx, session)
   290  		}
   291  
   292  		cc.orderService.ClearLastPlacedOrder(ctx)
   293  
   294  		if err != nil {
   295  			if paymentError, ok := err.(*paymentDomain.Error); ok {
   296  				if cc.showReviewStepAfterPaymentError && !cc.skipReviewAction {
   297  					return cc.redirectToReviewFormWithErrors(ctx, r, paymentError)
   298  				}
   299  			}
   300  			return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   301  		}
   302  	}
   303  
   304  	r.Session().AddFlash(PlaceOrderFlashData{
   305  		PlacedOrderInfos: placedOrderInfo.PlacedOrders,
   306  		Email:            placedOrderInfo.ContactEmail,
   307  		PlacedCart:       decoratedCart.Cart,
   308  		PaymentInfos:     placedOrderInfo.PaymentInfos,
   309  	}, CheckoutSuccessFlashKey)
   310  	return cc.responder.RouteRedirect("checkout.success", nil)
   311  }
   312  
   313  // SuccessAction handles the order success action
   314  func (cc *CheckoutController) SuccessAction(ctx context.Context, r *web.Request) web.Result {
   315  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/SuccessAction")
   316  	defer span.End()
   317  
   318  	flashes := r.Session().Flashes(CheckoutSuccessFlashKey)
   319  	if len(flashes) > 0 {
   320  
   321  		// if in development mode, then restore the last order in flash session.
   322  		if cc.devMode {
   323  			r.Session().AddFlash(flashes[len(flashes)-1], CheckoutSuccessFlashKey)
   324  		}
   325  
   326  		if placeOrderFlashData, ok := flashes[len(flashes)-1].(PlaceOrderFlashData); ok {
   327  			decoratedCart := cc.decoratedCartFactory.Create(ctx, placeOrderFlashData.PlacedCart)
   328  			viewData := SuccessViewData{
   329  				Email:               placeOrderFlashData.Email,
   330  				PaymentInfos:        placeOrderFlashData.PaymentInfos,
   331  				PlacedDecoratedCart: *decoratedCart,
   332  				PlacedOrderInfos:    placeOrderFlashData.PlacedOrderInfos,
   333  			}
   334  
   335  			return cc.responder.Render("checkout/success", viewData).SetNoCache()
   336  		}
   337  	}
   338  	return cc.responder.RouteRedirect("checkout.expired", nil).SetNoCache()
   339  }
   340  
   341  // ExpiredAction handles the expired cart action
   342  func (cc *CheckoutController) ExpiredAction(ctx context.Context, _ *web.Request) web.Result {
   343  	_, span := trace.StartSpan(ctx, "checkout/CheckoutController/ExpiredAction")
   344  	defer span.End()
   345  
   346  	if cc.showEmptyCartPageIfNoItems {
   347  		return cc.responder.Render("checkout/emptycart", EmptyCartInfo{
   348  			CartExpired: true,
   349  		}).SetNoCache()
   350  	}
   351  	return cc.responder.Render("checkout/expired", nil).SetNoCache()
   352  }
   353  
   354  func (cc *CheckoutController) getPaymentReturnURL(r *web.Request) *url.URL {
   355  	paymentURL, _ := cc.router.Absolute(r, "checkout.payment", nil)
   356  	return paymentURL
   357  }
   358  
   359  func (cc *CheckoutController) getBasicViewData(ctx context.Context, request *web.Request, decoratedCart decorator.DecoratedCart) CheckoutViewData {
   360  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/getBasicViewData")
   361  	defer span.End()
   362  
   363  	paymentGatewaysMethods := make(map[string][]paymentDomain.Method)
   364  	for gatewayCode, gateway := range cc.orderService.GetAvailablePaymentGateways(ctx) {
   365  		paymentGatewaysMethods[gatewayCode] = gateway.Methods()
   366  	}
   367  	return CheckoutViewData{
   368  		DecoratedCart:        decoratedCart,
   369  		CartValidationResult: cc.applicationCartService.ValidateCart(ctx, request.Session(), &decoratedCart),
   370  		AvailablePayments:    paymentGatewaysMethods,
   371  		CustomerLoggedIn:     cc.webIdentityService.Identify(ctx, request) != nil,
   372  	}
   373  }
   374  
   375  // showCheckoutFormAndHandleSubmit - Action that shows the form
   376  func (cc *CheckoutController) showCheckoutFormAndHandleSubmit(ctx context.Context, r *web.Request, template string) web.Result {
   377  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/showCheckoutFormAndHandleSubmit")
   378  	defer span.End()
   379  
   380  	session := r.Session()
   381  
   382  	// Guard Clause if Cart can't be fetched
   383  	decoratedCart, e := cc.applicationCartReceiverService.ViewDecoratedCart(ctx, session)
   384  	if e != nil {
   385  		cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error ", e)
   386  		return cc.responder.Render("checkout/carterror", nil).SetNoCache()
   387  	}
   388  
   389  	if len(cc.orderService.GetAvailablePaymentGateways(ctx)) == 0 {
   390  		cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error No Payment set")
   391  		return cc.responder.Render("checkout/carterror", nil).SetNoCache()
   392  	}
   393  	viewData := cc.getBasicViewData(ctx, r, *decoratedCart)
   394  	// Guard Clause if Cart is empty
   395  	if decoratedCart.Cart.ItemCount() == 0 {
   396  		if cc.showEmptyCartPageIfNoItems {
   397  			return cc.responder.Render("checkout/emptycart", nil).SetNoCache()
   398  		}
   399  		return cc.responder.Render(template, viewData).SetNoCache()
   400  	}
   401  
   402  	if r.Request().Method != http.MethodPost {
   403  		// Form not Submitted:
   404  		flashViewErrorInfos, found := cc.getViewErrorsFromSessionFlash(r)
   405  		if found {
   406  			viewData.ErrorInfos = *flashViewErrorInfos
   407  		}
   408  		form, err := cc.checkoutFormController.GetUnsubmittedForm(ctx, r)
   409  		if err != nil {
   410  			if !found {
   411  				viewData.ErrorInfos = getViewErrorInfo(err)
   412  			}
   413  			return cc.responder.Render(template, viewData).SetNoCache()
   414  		}
   415  		viewData.Form = *form
   416  		return cc.responder.Render(template, viewData).SetNoCache()
   417  	}
   418  
   419  	// Form submitted:
   420  	form, success, err := cc.checkoutFormController.HandleFormAction(ctx, r)
   421  	if err != nil {
   422  		viewData.ErrorInfos = getViewErrorInfo(err)
   423  		return cc.responder.Render(template, viewData).SetNoCache()
   424  	}
   425  	viewData.Form = *form
   426  
   427  	if success {
   428  		if cc.skipReviewAction {
   429  			canProceed, err := cc.checkTermsAndPrivacyPolicy(r)
   430  			if !canProceed || err != nil {
   431  				viewData.ErrorInfos = getViewErrorInfo(err)
   432  
   433  				return cc.responder.Render(template, viewData).SetNoCache()
   434  			}
   435  
   436  			cc.logger.WithContext(ctx).Debug("submit checkout succeeded: redirect to checkout.review")
   437  			return cc.processPayment(ctx, r)
   438  		}
   439  		response := cc.responder.RouteRedirect("checkout.review", nil).SetNoCache()
   440  
   441  		return response
   442  	}
   443  
   444  	// Default: show form with its validation result
   445  	return cc.responder.Render(template, viewData).SetNoCache()
   446  }
   447  
   448  // redirectToCheckoutFormWithErrors will store the error as a flash message and redirect to the checkout form where it is then displayed
   449  func (cc *CheckoutController) redirectToCheckoutFormWithErrors(ctx context.Context, r *web.Request, err error) web.Result {
   450  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/redirectToCheckoutFormWithErrors")
   451  	defer span.End()
   452  
   453  	cc.logger.WithContext(ctx).Info("redirect to checkout form and display error: ", err)
   454  	r.Session().AddFlash(getViewErrorInfo(err), CheckoutErrorFlashKey)
   455  	return cc.responder.RouteRedirect("checkout", nil).SetNoCache()
   456  }
   457  
   458  // redirectToReviewFormWithErrors will store the error as a flash message and redirect to the review form where it is then displayed
   459  func (cc *CheckoutController) redirectToReviewFormWithErrors(ctx context.Context, r *web.Request, err error) web.Result {
   460  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/redirectToReviewFormWithErrors")
   461  	defer span.End()
   462  
   463  	cc.logger.WithContext(ctx).Info("redirect to review form and display error: ", err)
   464  	r.Session().AddFlash(getViewErrorInfo(err), CheckoutErrorFlashKey)
   465  	return cc.responder.RouteRedirect("checkout.review", nil).SetNoCache()
   466  }
   467  
   468  func getViewErrorInfo(err error) ViewErrorInfos {
   469  	if err == nil {
   470  		return ViewErrorInfos{
   471  			HasError:        true,
   472  			HasPaymentError: false,
   473  		}
   474  	}
   475  
   476  	hasPaymentError := false
   477  	paymentErrorCode := ""
   478  
   479  	if paymentErr, ok := err.(*paymentDomain.Error); ok {
   480  		hasPaymentError = true
   481  
   482  		if paymentErr.ErrorCode == paymentDomain.PaymentErrorAbortedByCustomer {
   483  			// in case of customer payment abort don't show error message in frontend
   484  			return ViewErrorInfos{
   485  				HasError:        false,
   486  				HasPaymentError: false,
   487  			}
   488  		}
   489  
   490  		paymentErrorCode = paymentErr.ErrorCode
   491  	}
   492  
   493  	return ViewErrorInfos{
   494  		HasError:         true,
   495  		ErrorMessage:     err.Error(),
   496  		HasPaymentError:  hasPaymentError,
   497  		PaymentErrorCode: paymentErrorCode,
   498  	}
   499  }
   500  
   501  func (cc *CheckoutController) processPayment(ctx context.Context, r *web.Request) web.Result {
   502  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/processPayment")
   503  	defer span.End()
   504  
   505  	session := web.SessionFromContext(ctx)
   506  
   507  	// guard clause if cart can not be fetched
   508  	decoratedCart, err := cc.applicationCartReceiverService.ViewDecoratedCart(ctx, r.Session())
   509  	if err != nil {
   510  		cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error ", err)
   511  		return cc.responder.Render("checkout/carterror", nil).SetNoCache()
   512  	}
   513  
   514  	// Cart grand total is zero, so no payment needed.
   515  	if decoratedCart.Cart.GrandTotal.IsZero() {
   516  		return cc.responder.RouteRedirect("checkout.placeorder", nil)
   517  	}
   518  
   519  	// get the payment gateway for the specified payment selection
   520  	gateway, err := cc.orderService.GetPaymentGateway(ctx, decoratedCart.Cart.PaymentSelection.Gateway())
   521  	if err != nil {
   522  		return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   523  	}
   524  
   525  	returnURL := cc.getPaymentReturnURL(r)
   526  
   527  	// start the payment flow
   528  	flowResult, err := gateway.StartFlow(ctx, &decoratedCart.Cart, application.PaymentFlowStandardCorrelationID, returnURL)
   529  	if err != nil {
   530  		return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   531  	}
   532  
   533  	// payment flow requires an early place order
   534  	if flowResult.EarlyPlaceOrder {
   535  		payment, err := gateway.OrderPaymentFromFlow(ctx, &decoratedCart.Cart, application.PaymentFlowStandardCorrelationID)
   536  		if err != nil {
   537  			return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   538  		}
   539  
   540  		_, err = cc.orderService.CurrentCartPlaceOrder(ctx, session, *payment)
   541  		if err != nil {
   542  			return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   543  		}
   544  	}
   545  
   546  	return cc.responder.RouteRedirect("checkout.payment", nil)
   547  }
   548  
   549  // ReviewAction handles the cart review action
   550  func (cc *CheckoutController) ReviewAction(ctx context.Context, r *web.Request) web.Result {
   551  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/ReviewAction")
   552  	defer span.End()
   553  
   554  	if cc.skipReviewAction {
   555  		return cc.responder.Render("checkout/carterror", nil)
   556  	}
   557  
   558  	// payment / order already in process redirect to payment status page
   559  	if cc.orderService.HasLastPlacedOrder(ctx) {
   560  		return cc.responder.RouteRedirect("checkout.payment", nil)
   561  	}
   562  
   563  	// Guard Clause if cart can not be fetched
   564  	decoratedCart, err := cc.applicationCartReceiverService.ViewDecoratedCartWithoutCache(ctx, r.Session())
   565  	if err != nil {
   566  		cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error ", err)
   567  		return cc.responder.Render("checkout/carterror", nil).SetNoCache()
   568  	}
   569  
   570  	if cc.showEmptyCartPageIfNoItems && decoratedCart.Cart.ItemCount() == 0 {
   571  		return cc.responder.Render("checkout/emptycart", nil).SetNoCache()
   572  	}
   573  	guardRedirect := cc.getCommonGuardRedirects(ctx, r.Session(), decoratedCart)
   574  	if guardRedirect != nil {
   575  		return guardRedirect
   576  	}
   577  
   578  	viewData := ReviewStepViewData{
   579  		DecoratedCart: *decoratedCart,
   580  	}
   581  
   582  	flashViewErrorInfos, found := cc.getViewErrorsFromSessionFlash(r)
   583  	if found {
   584  		viewData.ErrorInfos = *flashViewErrorInfos
   585  		return cc.responder.Render("checkout/review", viewData).SetNoCache()
   586  	}
   587  
   588  	// check for terms and conditions and privacy policy
   589  	canProceed, err := cc.checkTermsAndPrivacyPolicy(r)
   590  	if err != nil {
   591  		viewData.ErrorInfos = getViewErrorInfo(err)
   592  	}
   593  
   594  	// Everything valid then return
   595  	if canProceed && err == nil {
   596  		if decoratedCart.Cart.IsPaymentSelected() {
   597  			return cc.processPayment(ctx, r)
   598  		}
   599  
   600  		if decoratedCart.Cart.GrandTotal.IsZero() {
   601  			return cc.responder.RouteRedirect("checkout.placeorder", nil)
   602  		}
   603  	}
   604  
   605  	return cc.responder.Render("checkout/review", viewData).SetNoCache()
   606  
   607  }
   608  
   609  // PaymentAction asks the payment adapter about the current payment status and handle it
   610  func (cc *CheckoutController) PaymentAction(ctx context.Context, r *web.Request) web.Result {
   611  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/PaymentAction")
   612  	defer span.End()
   613  
   614  	session := web.SessionFromContext(ctx)
   615  
   616  	decoratedCart, err := cc.orderService.LastPlacedOrCurrentCart(ctx)
   617  	if err != nil {
   618  		cc.logger.WithContext(ctx).Error(err)
   619  		return cc.responder.Render("checkout/carterror", nil).SetNoCache()
   620  	}
   621  
   622  	if decoratedCart.Cart.PaymentSelection == nil {
   623  		cc.logger.WithContext(ctx).Info("No PaymentSelection for cart with ID ", decoratedCart.Cart.ID)
   624  		return cc.responder.RouteRedirect("checkout.expired", nil).SetNoCache()
   625  	}
   626  
   627  	gateway, err := cc.orderService.GetPaymentGateway(ctx, decoratedCart.Cart.PaymentSelection.Gateway())
   628  	if err != nil {
   629  		return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   630  	}
   631  
   632  	flowStatus, err := gateway.FlowStatus(ctx, &decoratedCart.Cart, application.PaymentFlowStandardCorrelationID)
   633  	if err != nil {
   634  		cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.paymentaction: Error ", err)
   635  
   636  		// payment failed, reopen the cart to make it still usable.
   637  		if cc.orderService.HasLastPlacedOrder(ctx) {
   638  			infos, err := cc.orderService.LastPlacedOrder(ctx)
   639  			if err != nil {
   640  				cc.logger.WithContext(ctx).Error(err)
   641  				return cc.responder.RouteRedirect("checkout", nil)
   642  			}
   643  
   644  			_, err = cc.orderService.CancelOrder(ctx, session, infos)
   645  			if err != nil {
   646  				cc.logger.WithContext(ctx).Error(err)
   647  				return cc.responder.RouteRedirect("checkout", nil)
   648  			}
   649  
   650  			cc.orderService.ClearLastPlacedOrder(ctx)
   651  
   652  			_, _ = cc.applicationCartService.ForceReserveOrderIDAndSave(ctx, r.Session())
   653  		}
   654  
   655  		return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   656  	}
   657  
   658  	viewData := &PaymentStepViewData{
   659  		FlowStatus: *flowStatus,
   660  	}
   661  
   662  	switch flowStatus.Status {
   663  	case paymentDomain.PaymentFlowStatusUnapproved:
   664  		// payment just started render payment page which handles actions
   665  		return cc.responder.Render("checkout/payment", viewData).SetNoCache()
   666  	case paymentDomain.PaymentFlowStatusApproved:
   667  		// payment is done but not confirmed by customer, confirm it and place order
   668  		orderPayment, err := gateway.OrderPaymentFromFlow(ctx, &decoratedCart.Cart, application.PaymentFlowStandardCorrelationID)
   669  		if err != nil {
   670  			viewData.ErrorInfos = getViewErrorInfo(err)
   671  			cc.logger.WithContext(ctx).Error(err)
   672  			return cc.responder.Render("checkout/payment", viewData).SetNoCache()
   673  		}
   674  
   675  		err = gateway.ConfirmResult(ctx, &decoratedCart.Cart, orderPayment)
   676  		if err != nil {
   677  			viewData.ErrorInfos = getViewErrorInfo(err)
   678  			cc.logger.WithContext(ctx).Error(err)
   679  			return cc.responder.Render("checkout/payment", viewData).SetNoCache()
   680  		}
   681  
   682  		return cc.responder.RouteRedirect("checkout.placeorder", nil)
   683  	case paymentDomain.PaymentFlowStatusCompleted:
   684  		// payment is done and confirmed, place order
   685  		return cc.responder.RouteRedirect("checkout.placeorder", nil)
   686  	case paymentDomain.PaymentFlowStatusAborted:
   687  		// payment was aborted by user, redirect to checkout so a new payment can be started
   688  		if cc.orderService.HasLastPlacedOrder(ctx) {
   689  			infos, err := cc.orderService.LastPlacedOrder(ctx)
   690  			if err != nil {
   691  				cc.logger.WithContext(ctx).Error(err)
   692  				if cc.showReviewStepAfterPaymentError && !cc.skipReviewAction {
   693  					return cc.redirectToReviewFormWithErrors(ctx, r, flowStatus.Error)
   694  				}
   695  
   696  				return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   697  			}
   698  
   699  			// ignore restored cart here since it gets fetched newly in checkout
   700  			_, err = cc.orderService.CancelOrder(ctx, session, infos)
   701  			if err != nil {
   702  				cc.logger.WithContext(ctx).Error(err)
   703  			}
   704  
   705  			cc.orderService.ClearLastPlacedOrder(ctx)
   706  			_, _ = cc.applicationCartService.ForceReserveOrderIDAndSave(ctx, r.Session())
   707  		}
   708  
   709  		// mark payment selection as new payment to allow the user to retry
   710  		newPaymentSelection, err := decoratedCart.Cart.PaymentSelection.GenerateNewIdempotencyKey()
   711  		if err != nil {
   712  			cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.paymentaction: Error during GenerateNewIdempotencyKey:", err)
   713  		} else {
   714  			err = cc.applicationCartService.UpdatePaymentSelection(ctx, session, newPaymentSelection)
   715  			if err != nil {
   716  				cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.paymentaction: Error during UpdatePaymentSelection:", err)
   717  			}
   718  		}
   719  
   720  		if cc.showReviewStepAfterPaymentError && !cc.skipReviewAction {
   721  			return cc.responder.RouteRedirect("checkout.review", nil)
   722  		}
   723  
   724  		return cc.responder.RouteRedirect("checkout", nil)
   725  	case paymentDomain.PaymentFlowStatusFailed, paymentDomain.PaymentFlowStatusCancelled:
   726  		// payment failed or is cancelled by payment provider, redirect back to checkout
   727  		if cc.orderService.HasLastPlacedOrder(ctx) {
   728  			infos, err := cc.orderService.LastPlacedOrder(ctx)
   729  			if err != nil {
   730  				cc.logger.WithContext(ctx).Error(err)
   731  				if cc.showReviewStepAfterPaymentError && !cc.skipReviewAction {
   732  					return cc.redirectToReviewFormWithErrors(ctx, r, flowStatus.Error)
   733  				}
   734  
   735  				return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   736  			}
   737  
   738  			_, err = cc.orderService.CancelOrder(ctx, session, infos)
   739  			if err != nil {
   740  				cc.logger.WithContext(ctx).Error(err)
   741  			}
   742  
   743  			cc.orderService.ClearLastPlacedOrder(ctx)
   744  			_, _ = cc.applicationCartService.ForceReserveOrderIDAndSave(ctx, r.Session())
   745  		}
   746  
   747  		// mark payment selection as new payment to allow the user to retry
   748  		newPaymentSelection, err := decoratedCart.Cart.PaymentSelection.GenerateNewIdempotencyKey()
   749  		if err != nil {
   750  			cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.paymentaction: Error during GenerateNewIdempotencyKey:", err)
   751  		} else {
   752  			err = cc.applicationCartService.UpdatePaymentSelection(ctx, session, newPaymentSelection)
   753  			if err != nil {
   754  				cc.logger.WithContext(ctx).Error("cart.checkoutcontroller.paymentaction: Error during UpdatePaymentSelection:", err)
   755  			}
   756  		}
   757  
   758  		err = flowStatus.Error
   759  		if flowStatus.Error == nil {
   760  			// fallback if payment gateway didn't respond with a proper error
   761  			switch flowStatus.Status {
   762  			case paymentDomain.PaymentFlowStatusCancelled:
   763  				err = &paymentDomain.Error{
   764  					ErrorCode:    paymentDomain.PaymentErrorCodeCancelled,
   765  					ErrorMessage: paymentDomain.PaymentErrorCodeCancelled,
   766  				}
   767  			case paymentDomain.PaymentFlowStatusFailed:
   768  				err = &paymentDomain.Error{
   769  					ErrorCode:    paymentDomain.PaymentErrorCodeFailed,
   770  					ErrorMessage: paymentDomain.PaymentErrorCodeFailed,
   771  				}
   772  			}
   773  		}
   774  
   775  		if cc.showReviewStepAfterPaymentError && !cc.skipReviewAction {
   776  			return cc.redirectToReviewFormWithErrors(ctx, r, err)
   777  		}
   778  
   779  		return cc.redirectToCheckoutFormWithErrors(ctx, r, err)
   780  	case paymentDomain.PaymentFlowWaitingForCustomer:
   781  		// payment pending, waiting for customer
   782  		return cc.responder.Render("checkout/payment", viewData).SetNoCache()
   783  	default:
   784  		// show payment page which can react to unknown payment status
   785  		return cc.responder.Render("checkout/payment", viewData).SetNoCache()
   786  	}
   787  }
   788  
   789  // getCommonGuardRedirects checks config and may return a redirect that should be executed before the common checkou actions
   790  func (cc *CheckoutController) getCommonGuardRedirects(ctx context.Context, session *web.Session, decoratedCart *decorator.DecoratedCart) web.Result {
   791  	ctx, span := trace.StartSpan(ctx, "checkout/CheckoutController/getCommonGuardRedirects")
   792  	defer span.End()
   793  
   794  	if cc.redirectToCartOnInvalideCart {
   795  		result := cc.applicationCartService.ValidateCart(ctx, session, decoratedCart)
   796  		if !result.IsValid() {
   797  			cc.logger.WithContext(ctx).Info("StartAction > RedirectToCartOnInvalidCart")
   798  			resp := cc.responder.RouteRedirect("cart.view", nil)
   799  			resp.SetNoCache()
   800  			return resp
   801  		}
   802  	}
   803  	return nil
   804  }
   805  
   806  // checkTermsAndPrivacyPolicy checks if TermsAndConditions and PrivacyPolicy is set as required
   807  // the returned error indicates that the check failed
   808  func (cc *CheckoutController) checkTermsAndPrivacyPolicy(r *web.Request) (bool, error) {
   809  	proceed, _ := r.Form1("proceed")
   810  	termsAndConditions, _ := r.Form1("termsAndConditions")
   811  	privacyPolicy, _ := r.Form1("privacyPolicy")
   812  
   813  	// prepare a minimal slice for error messages
   814  	errorMessages := make([]string, 0, 2)
   815  
   816  	// check for privacy policy if required
   817  	if cc.privacyPolicyRequired && privacyPolicy != "1" && proceed == "1" {
   818  		errorMessages = append(errorMessages, "privacy_policy_required")
   819  	}
   820  
   821  	// check for terms and conditions if required
   822  	if termsAndConditions != "1" {
   823  		errorMessages = append(errorMessages, "terms_and_conditions_required")
   824  	}
   825  
   826  	canProceed := proceed == "1" && (!cc.privacyPolicyRequired || privacyPolicy == "1") && termsAndConditions == "1"
   827  
   828  	if len(errorMessages) == 0 {
   829  		return canProceed, nil
   830  	}
   831  
   832  	return canProceed, errors.New(strings.Join(errorMessages, ", "))
   833  }
   834  
   835  // getViewErrorsFromSessionFlash check if session flash data contains checkout errors, if so return them and remove the flash message from the session
   836  func (cc *CheckoutController) getViewErrorsFromSessionFlash(r *web.Request) (*ViewErrorInfos, bool) {
   837  	flashErrors := r.Session().Flashes(CheckoutErrorFlashKey)
   838  	if len(flashErrors) == 1 {
   839  		flashViewErrorInfos, ok := flashErrors[0].(ViewErrorInfos)
   840  		if ok {
   841  			return &flashViewErrorInfos, true
   842  		}
   843  	}
   844  
   845  	return nil, false
   846  }