flamingo.me/flamingo-commerce/v3@v3.11.0/checkout/application/orderService.go (about) 1 package application 2 3 import ( 4 "context" 5 "encoding/gob" 6 "errors" 7 "fmt" 8 9 "go.opencensus.io/trace" 10 11 "flamingo.me/flamingo/v3/framework/flamingo" 12 "flamingo.me/flamingo/v3/framework/opencensus" 13 "flamingo.me/flamingo/v3/framework/web" 14 "go.opencensus.io/stats" 15 "go.opencensus.io/stats/view" 16 17 "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 paymentDomain "flamingo.me/flamingo-commerce/v3/payment/domain" 22 "flamingo.me/flamingo-commerce/v3/payment/interfaces" 23 priceDomain "flamingo.me/flamingo-commerce/v3/price/domain" 24 ) 25 26 type ( 27 // OrderService defines the order service 28 OrderService struct { 29 logger flamingo.Logger 30 cartService *application.CartService 31 cartReceiverService *application.CartReceiverService 32 deliveryInfoBuilder cart.DeliveryInfoBuilder 33 webCartPaymentGateways map[string]interfaces.WebCartPaymentGateway 34 decoratedCartFactory *decorator.DecoratedCartFactory 35 } 36 37 // PlaceOrderInfo struct defines the data of payments on placed orders 38 PlaceOrderInfo struct { 39 PaymentInfos []PlaceOrderPaymentInfo 40 PlacedOrders placeorder.PlacedOrderInfos 41 ContactEmail string 42 Cart cart.Cart 43 } 44 45 // PlaceOrderPaymentInfo holding payment infos 46 PlaceOrderPaymentInfo struct { 47 Gateway string 48 PaymentProvider string 49 Method string 50 CreditCardInfo *placeorder.CreditCardInfo 51 Amount priceDomain.Price 52 Title string 53 } 54 ) 55 56 const ( 57 // PaymentFlowStandardCorrelationID used as correlationid for the start of the payment (session scoped) 58 PaymentFlowStandardCorrelationID = "checkout" 59 60 // LastPlacedOrderSessionKey is the session key for storing the last placed order 61 LastPlacedOrderSessionKey = "orderservice_last_placed" 62 ) 63 64 var ( 65 // cartValidationFailCount counts validation failures on carts 66 cartValidationFailCount = stats.Int64("flamingo-commerce/checkout/orders/cart_validation_failed", "Count of failures while validating carts", stats.UnitDimensionless) 67 68 // noPaymentSelectionCount counts error for orders without payment selection 69 noPaymentSelectionCount = stats.Int64("flamingo-commerce/checkout/orders/no_payment_selection", "Count of carts without having a selected payment", stats.UnitDimensionless) 70 71 // paymentGatewayNotFoundCount counts errors if payment gateway for selected payment could not be found 72 paymentGatewayNotFoundCount = stats.Int64("flamingo-commerce/checkout/orders/payment_gateway_not_found", "The selected payment gateway could not be found", stats.UnitDimensionless) 73 74 // paymentFlowStatusErrorCount counts errors while fetching payment flow status 75 paymentFlowStatusErrorCount = stats.Int64("flamingo-commerce/checkout/orders/payment_flow_status_error", "Count of failures while fetching payment flow status", stats.UnitDimensionless) 76 77 // orderPaymentFromFlowErrorCount counts errors while fetching payment from flow 78 orderPaymentFromFlowErrorCount = stats.Int64("flamingo-commerce/checkout/orders/order_payment_from_flow_error", "Count of failures while fetching payment from flow", stats.UnitDimensionless) 79 80 // paymentFlowStatusFailedCanceledCount counts orders trying to be placed with payment status either failed or canceled 81 paymentFlowStatusFailedCanceledCount = stats.Int64("flamingo-commerce/checkout/orders/payment_flow_status_failed_canceled", "Count of payments with status failed or canceled", stats.UnitDimensionless) 82 83 // paymentFlowStatusAbortedCount counts orders trying to be placed with payment status aborted 84 paymentFlowStatusAbortedCount = stats.Int64("flamingo-commerce/checkout/orders/payment_flow_status_aborted", "Count of payments with status aborted", stats.UnitDimensionless) 85 86 // placeOrderFailCount counts failed placed orders 87 placeOrderFailCount = stats.Int64("flamingo-commerce/checkout/orders/place_order_failed", "Count of failures while placing orders", stats.UnitDimensionless) 88 89 // placeOrderSuccessCount counts successfully placed orders 90 placeOrderSuccessCount = stats.Int64("flamingo-commerce/checkout/orders/place_order_successful", "Count of successfully placed orders", stats.UnitDimensionless) 91 ) 92 93 func init() { 94 gob.Register(PlaceOrderInfo{}) 95 openCensusViews := map[string]*stats.Int64Measure{ 96 "flamingo-commerce/checkout/orders/cart_validation_failed": cartValidationFailCount, 97 "flamingo-commerce/checkout/orders/no_payment_selection": noPaymentSelectionCount, 98 "flamingo-commerce/checkout/orders/payment_gateway_not_found": paymentGatewayNotFoundCount, 99 "flamingo-commerce/checkout/orders/payment_flow_status_error": paymentFlowStatusErrorCount, 100 "flamingo-commerce/checkout/orders/order_payment_from_flow_error": orderPaymentFromFlowErrorCount, 101 "flamingo-commerce/checkout/orders/payment_flow_status_failed_canceled": paymentFlowStatusFailedCanceledCount, 102 "flamingo-commerce/checkout/orders/payment_flow_status_aborted": paymentFlowStatusAbortedCount, 103 "flamingo-commerce/checkout/orders/place_order_failed": placeOrderFailCount, 104 "flamingo-commerce/checkout/orders/place_order_successful": placeOrderSuccessCount, 105 } 106 107 for name, measure := range openCensusViews { 108 err := opencensus.View(name, measure, view.Sum()) 109 if err != nil { 110 panic(err) 111 } 112 113 stats.Record(context.Background(), measure.M(0)) 114 } 115 } 116 117 // Inject dependencies 118 func (os *OrderService) Inject( 119 logger flamingo.Logger, 120 CartService *application.CartService, 121 CartReceiverService *application.CartReceiverService, 122 DeliveryInfoBuilder cart.DeliveryInfoBuilder, 123 webCartPaymentGatewayProvider interfaces.WebCartPaymentGatewayProvider, 124 decoratedCartFactory *decorator.DecoratedCartFactory, 125 126 ) { 127 os.logger = logger.WithField(flamingo.LogKeyCategory, "checkout.OrderService").WithField(flamingo.LogKeyModule, "checkout") 128 os.cartService = CartService 129 os.cartReceiverService = CartReceiverService 130 os.webCartPaymentGateways = webCartPaymentGatewayProvider() 131 os.deliveryInfoBuilder = DeliveryInfoBuilder 132 os.decoratedCartFactory = decoratedCartFactory 133 } 134 135 // GetPaymentGateway tries to get the supplied payment gateway by code from the registered payment gateways 136 func (os *OrderService) GetPaymentGateway(ctx context.Context, paymentGatewayCode string) (interfaces.WebCartPaymentGateway, error) { 137 _, span := trace.StartSpan(ctx, "checkout/OrderService/GetPaymentGateway") 138 defer span.End() 139 140 gateway, ok := os.webCartPaymentGateways[paymentGatewayCode] 141 if !ok { 142 return nil, errors.New("Payment gateway " + paymentGatewayCode + " not found") 143 } 144 145 return gateway, nil 146 } 147 148 // GetAvailablePaymentGateways returns the list of registered WebCartPaymentGateway 149 func (os *OrderService) GetAvailablePaymentGateways(ctx context.Context) map[string]interfaces.WebCartPaymentGateway { 150 _, span := trace.StartSpan(ctx, "checkout/OrderService/GetAvailablePaymentGateways") 151 defer span.End() 152 153 return os.webCartPaymentGateways 154 } 155 156 // CurrentCartPlaceOrder places the current cart without additional payment processing 157 func (os *OrderService) CurrentCartPlaceOrder(ctx context.Context, session *web.Session, cartPayment placeorder.Payment) (*PlaceOrderInfo, error) { 158 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/CurrentCartPlaceOrder") 159 defer span.End() 160 161 var info *PlaceOrderInfo 162 var err error 163 web.RunWithDetachedContext(ctx, func(placeOrderContext context.Context) { 164 info, err = func() (*PlaceOrderInfo, error) { 165 decoratedCart, err := os.cartReceiverService.ViewDecoratedCart(placeOrderContext, session) 166 if err != nil { 167 os.logger.WithContext(placeOrderContext).Error("OnStepCurrentCartPlaceOrder GetDecoratedCart Error ", err) 168 169 return nil, err 170 } 171 172 return os.placeOrder(placeOrderContext, session, decoratedCart, cartPayment) 173 }() 174 }) 175 176 return info, err 177 } 178 179 func (os *OrderService) placeOrder(ctx context.Context, session *web.Session, decoratedCart *decorator.DecoratedCart, payment placeorder.Payment) (*PlaceOrderInfo, error) { 180 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/placeOrder") 181 defer span.End() 182 183 validationResult := os.cartService.ValidateCart(ctx, session, decoratedCart) 184 if !validationResult.IsValid() { 185 // record cartValidationFailCount metric 186 stats.Record(ctx, cartValidationFailCount.M(1)) 187 os.logger.WithContext(ctx).Warn("Try to place an invalid cart") 188 189 return nil, errors.New("cart is invalid") 190 } 191 192 placedOrderInfos, err := os.cartService.PlaceOrderWithCart(ctx, session, &decoratedCart.Cart, &payment) 193 if err != nil { 194 // record placeOrderFailCount metric 195 stats.Record(ctx, placeOrderFailCount.M(1)) 196 os.logger.WithContext(ctx).Error("Error during place Order:" + err.Error()) 197 198 return nil, errors.New("error while placing the order. please contact customer support") 199 } 200 201 placeOrderInfo := os.preparePlaceOrderInfo(ctx, decoratedCart.Cart, placedOrderInfos, payment) 202 os.storeLastPlacedOrder(ctx, placeOrderInfo) 203 204 // record placeOrderSuccessCount metric 205 stats.Record(ctx, placeOrderSuccessCount.M(1)) 206 207 return placeOrderInfo, nil 208 } 209 210 // CancelOrder cancels an previously placed order and returns the restored cart with the order content 211 func (os *OrderService) CancelOrder(ctx context.Context, session *web.Session, order *PlaceOrderInfo) (*cart.Cart, error) { 212 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/CancelOrder") 213 defer span.End() 214 215 return os.cartService.CancelOrder(ctx, session, order.PlacedOrders, order.Cart) 216 } 217 218 // CancelOrderWithoutRestore cancels an previously placed order 219 func (os *OrderService) CancelOrderWithoutRestore(ctx context.Context, session *web.Session, order *PlaceOrderInfo) error { 220 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/CancelOrderWithoutRestore") 221 defer span.End() 222 223 return os.cartService.CancelOrderWithoutRestore(ctx, session, order.PlacedOrders) 224 } 225 226 // CurrentCartPlaceOrderWithPaymentProcessing places the current cart which is fetched from the context 227 func (os *OrderService) CurrentCartPlaceOrderWithPaymentProcessing(ctx context.Context, session *web.Session) (*PlaceOrderInfo, error) { 228 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/CurrentCartPlaceOrderWithPaymentProcessing") 229 defer span.End() 230 231 var info *PlaceOrderInfo 232 var err error 233 // use a background context from here on to prevent the place order canceled by context cancel 234 web.RunWithDetachedContext(ctx, func(placeOrderContext context.Context) { 235 info, err = func() (*PlaceOrderInfo, error) { 236 // fetch decorated cart either via cache or freshly from cart receiver service 237 decoratedCart, err := os.cartReceiverService.ViewDecoratedCart(placeOrderContext, session) 238 if err != nil { 239 os.logger.WithContext(placeOrderContext).Warn("Cannot create decorated cart from cart") 240 241 return nil, errors.New("cart is invalid") 242 } 243 244 return os.placeOrderWithPaymentProcessing(placeOrderContext, decoratedCart, session) 245 }() 246 }) 247 248 return info, err 249 } 250 251 // CartPlaceOrderWithPaymentProcessing places the cart passed to the function 252 // this function enables clients to pass a cart as is, without the usage of the cartReceiverService 253 func (os *OrderService) CartPlaceOrderWithPaymentProcessing(ctx context.Context, decoratedCart *decorator.DecoratedCart, 254 session *web.Session) (*PlaceOrderInfo, error) { 255 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/CartPlaceOrderWithPaymentProcessing") 256 defer span.End() 257 258 var info *PlaceOrderInfo 259 var err error 260 // use a background context from here on to prevent the place order canceled by context cancel 261 web.RunWithDetachedContext(ctx, func(placeOrderContext context.Context) { 262 info, err = os.placeOrderWithPaymentProcessing(placeOrderContext, decoratedCart, session) 263 }) 264 265 return info, err 266 } 267 268 // CartPlaceOrder places the cart passed to the function 269 // this function enables clients to pass a cart as is, without the usage of the cartReceiverService 270 func (os *OrderService) CartPlaceOrder(ctx context.Context, decoratedCart *decorator.DecoratedCart, payment placeorder.Payment) (*PlaceOrderInfo, error) { 271 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/CartPlaceOrder") 272 defer span.End() 273 274 var info *PlaceOrderInfo 275 var err error 276 web.RunWithDetachedContext(ctx, func(placeOrderContext context.Context) { 277 info, err = os.placeOrder(placeOrderContext, web.SessionFromContext(ctx), decoratedCart, payment) 278 }) 279 280 return info, err 281 } 282 283 // storeLastPlacedOrder stores the last placed order/cart in the session 284 func (os *OrderService) storeLastPlacedOrder(ctx context.Context, info *PlaceOrderInfo) { 285 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/storeLastPlacedOrder") 286 defer span.End() 287 288 session := web.SessionFromContext(ctx) 289 290 _ = session.Store(LastPlacedOrderSessionKey, info) 291 } 292 293 // LastPlacedOrder returns the last placed order/cart if available 294 func (os *OrderService) LastPlacedOrder(ctx context.Context) (*PlaceOrderInfo, error) { 295 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/LastPlacedOrder") 296 defer span.End() 297 298 session := web.SessionFromContext(ctx) 299 300 lastPlacedOrder, found := session.Load(LastPlacedOrderSessionKey) 301 if !found { 302 return nil, nil 303 } 304 305 placeOrderInfo, ok := lastPlacedOrder.(PlaceOrderInfo) 306 if !ok { 307 return nil, errors.New("placeOrderInfo couldn't be received from session") 308 } 309 310 return &placeOrderInfo, nil 311 } 312 313 // HasLastPlacedOrder returns if a order has been previously placed 314 func (os *OrderService) HasLastPlacedOrder(ctx context.Context) bool { 315 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/HasLastPlacedOrder") 316 defer span.End() 317 318 lastPlaced, err := os.LastPlacedOrder(ctx) 319 320 return lastPlaced != nil && err == nil 321 } 322 323 // ClearLastPlacedOrder clears the last placed cart, this can be useful if an cart / order is finished 324 func (os *OrderService) ClearLastPlacedOrder(ctx context.Context) { 325 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/ClearLastPlacedOrder") 326 defer span.End() 327 328 session := web.SessionFromContext(ctx) 329 session.Delete(LastPlacedOrderSessionKey) 330 } 331 332 // LastPlacedOrCurrentCart returns the decorated cart of the last placed order if there is one if not return the current cart 333 func (os *OrderService) LastPlacedOrCurrentCart(ctx context.Context) (*decorator.DecoratedCart, error) { 334 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/LastPlacedOrCurrentCart") 335 defer span.End() 336 337 lastPlacedOrder, err := os.LastPlacedOrder(ctx) 338 if err != nil { 339 os.logger.Warn("couldn't get last placed order:", err) 340 341 return nil, err 342 } 343 344 if lastPlacedOrder != nil { 345 // cart has been placed early use it 346 return os.decoratedCartFactory.Create(ctx, lastPlacedOrder.Cart), nil 347 } 348 349 // cart wasn't placed early, fetch it from service 350 decoratedCart, err := os.cartReceiverService.ViewDecoratedCart(ctx, web.SessionFromContext(ctx)) 351 if err != nil { 352 os.logger.WithContext(ctx).Error("ViewDecoratedCart Error:", err) 353 354 return nil, err 355 } 356 357 return decoratedCart, nil 358 } 359 360 // placeOrderWithPaymentProcessing after generating the decorated cart, the place order flow 361 // is the same for the interface functions, therefore the common flow is placed in this private helper function 362 func (os *OrderService) placeOrderWithPaymentProcessing(ctx context.Context, decoratedCart *decorator.DecoratedCart, 363 session *web.Session) (*PlaceOrderInfo, error) { 364 ctx, span := trace.StartSpan(ctx, "checkout/OrderService/placeOrderWithPaymentProcessing") 365 defer span.End() 366 367 if !decoratedCart.Cart.IsPaymentSelected() { 368 // record noPaymentSelectionCount metric 369 stats.Record(ctx, noPaymentSelectionCount.M(1)) 370 os.logger.WithContext(ctx).Error("cart.checkoutcontroller.submitaction: Error Gateway not in carts PaymentSelection") 371 372 return nil, errors.New("no payment gateway selected") 373 } 374 375 validationResult := os.cartService.ValidateCart(ctx, session, decoratedCart) 376 if !validationResult.IsValid() { 377 // record cartValidationFailCount metric 378 stats.Record(ctx, cartValidationFailCount.M(1)) 379 os.logger.WithContext(ctx).Warn("Try to place an invalid cart") 380 381 return nil, errors.New("cart is invalid") 382 } 383 384 gateway, err := os.GetPaymentGateway(ctx, decoratedCart.Cart.PaymentSelection.Gateway()) 385 if err != nil { 386 // record paymentGatewayNotFoundCount metric 387 stats.Record(ctx, paymentGatewayNotFoundCount.M(1)) 388 os.logger.WithContext(ctx).Error(fmt.Sprintf("cart.checkoutcontroller.submitaction: Error %v Gateway: %v", err, decoratedCart.Cart.PaymentSelection.Gateway())) 389 390 return nil, errors.New("selected gateway not available") 391 } 392 393 flowStatus, err := gateway.FlowStatus(ctx, &decoratedCart.Cart, PaymentFlowStandardCorrelationID) 394 if err != nil { 395 // record paymentFlowStatusErrorCount metric 396 stats.Record(ctx, paymentFlowStatusErrorCount.M(1)) 397 398 return nil, err 399 } 400 401 if flowStatus.Status == paymentDomain.PaymentFlowStatusFailed || flowStatus.Status == paymentDomain.PaymentFlowStatusCancelled { 402 // record paymentFlowStatusFailedCanceledCount metric 403 stats.Record(ctx, paymentFlowStatusFailedCanceledCount.M(1)) 404 os.logger.WithContext(ctx).Info("cart.checkoutcontroller.submitaction: PaymentFlowStatusFailed or PaymentFlowStatusCancelled: Error ", flowStatus.Error) 405 406 return nil, flowStatus.Error 407 } 408 409 if flowStatus.Status == paymentDomain.PaymentFlowStatusAborted { 410 // record paymentFlowStatusAbortedCount metric 411 stats.Record(ctx, paymentFlowStatusAbortedCount.M(1)) 412 os.logger.WithContext(ctx).Info("cart.checkoutcontroller.submitaction: PaymentFlowStatusAborted: Error ", flowStatus.Error) 413 414 return nil, flowStatus.Error 415 } 416 417 cartPayment, err := gateway.OrderPaymentFromFlow(ctx, &decoratedCart.Cart, PaymentFlowStandardCorrelationID) 418 if err != nil { 419 // record orderPaymentFromFlowErrorCount metric 420 stats.Record(ctx, orderPaymentFromFlowErrorCount.M(1)) 421 422 return nil, err 423 } 424 425 placedOrderInfos, err := os.cartService.PlaceOrderWithCart(ctx, session, &decoratedCart.Cart, cartPayment) 426 if err != nil { 427 // record placeOrderFailCount metric 428 stats.Record(ctx, placeOrderFailCount.M(1)) 429 os.logger.WithContext(ctx).Error("Error during place Order: " + err.Error()) 430 431 return nil, err 432 } 433 434 os.logger.WithContext(ctx).Info("Placed Order: ", placedOrderInfos) 435 436 placeOrderInfo := os.preparePlaceOrderInfo(ctx, decoratedCart.Cart, placedOrderInfos, *cartPayment) 437 os.storeLastPlacedOrder(ctx, placeOrderInfo) 438 439 if flowStatus.Status != paymentDomain.PaymentFlowStatusCompleted { 440 err = gateway.ConfirmResult(ctx, &decoratedCart.Cart, cartPayment) 441 if err != nil { 442 os.logger.WithContext(ctx).Error("Error during gateway.ConfirmResult: " + err.Error()) 443 444 return nil, err 445 } 446 } 447 448 // record placeOrderSuccessCount metric 449 stats.Record(ctx, placeOrderSuccessCount.M(1)) 450 451 return placeOrderInfo, nil 452 } 453 454 func (os *OrderService) preparePlaceOrderInfo(ctx context.Context, currentCart cart.Cart, placedOrderInfos placeorder.PlacedOrderInfos, cartPayment placeorder.Payment) *PlaceOrderInfo { 455 _, span := trace.StartSpan(ctx, "checkout/OrderService/preparePlaceOrderInfo") 456 defer span.End() 457 458 email := currentCart.GetContactMail() 459 460 placeOrderInfo := &PlaceOrderInfo{ 461 ContactEmail: email, 462 PlacedOrders: placedOrderInfos, 463 Cart: currentCart, 464 } 465 466 for _, transaction := range cartPayment.Transactions { 467 placeOrderInfo.PaymentInfos = append(placeOrderInfo.PaymentInfos, PlaceOrderPaymentInfo{ 468 Gateway: cartPayment.Gateway, 469 Method: transaction.Method, 470 PaymentProvider: transaction.PaymentProvider, 471 Title: transaction.Title, 472 Amount: transaction.AmountPayed, 473 CreditCardInfo: transaction.CreditCardInfo, 474 }) 475 } 476 477 return placeOrderInfo 478 }