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 }