flamingo.me/flamingo-commerce/v3@v3.11.0/cart/Readme.md (about) 1 # Cart Module 2 3 The cart module is one of the main modules in Flamingo Commerce. It offers: 4 5 * *domain layer*: 6 * domain models for carts, deliveries and their items. 7 * cartservices: the secondary ports required for modifying the cart. 8 * orderservice: the secondary port called when placing the cart as order 9 * support for multiple deliveries 10 * support for multipayment 11 * *application layer* useful application services, that should be used to get and modify the carts. That also includes a transparent session based cart cache to cache carts in cases where updating and reading the cart (e.g. against an external API) is too slow. 12 * *interface layer* Controllers and Actions to render the carrt pages. Also a flexible to use Ajax API that can be used to modify the cart. 13 * *infrastructure layer* 14 * Sample adapter for the secondary ports that maneges the cart in memory. 15 * Sample adapter that will log a json file with every for placing an order 16 17 There will be additional Flamingo modules that provide adapters for the secondary ports against common e-commerce APIs like Magento 2. 18 The cart module and its services are used by the checkout module. 19 20 ## Usage 21 22 ### Configurations 23 24 For all possible configurations you can check the `module.go` (CueConfig function) 25 As always you can also dump the current configuration with the "config" Flamingo command. 26 27 Here is a typical configuration 28 ```yaml 29 commerce.cart: 30 # enable the secondary adapters for the cart services. (e.g. for testing or development mode) 31 useInMemoryCartServiceAdapters: true 32 # enable the cache 33 enableCartCache: true 34 # set the default delivery code that is used if no other is given 35 defaultDeliveryCode: "delivery" 36 ``` 37 38 ## Domain Model Details 39 40 ### Cart Aggregate 41 42 Represents the Cart with PaymentInfos, DeliveryInfos and its Items: 43 44  45 46 ### Immutable cart / Updating the cart 47 * The "Cart" aggregate in the Domain Model is a complex object that should be used as a pure **immutable value object**: 48 * Never change it directly! 49 * Only read from it 50 * The Cart is only **modified by Commands** send to a CartBehaviour Object 51 * If you want to retrieve or change a cart - **ONLY work with the application services**. This will ensure that the correct cache is used 52 53 54 ### About Delivery 55 56 In order to support Multidelivery the cart cannot directly have Items attached, instead the Items belong to a Delivery. 57 58 That also means when adding Items to the cart you need to specify the delivery with a "code". 59 60 In cases where you only need one Delivery this can be configured as default and will be added on the fly for you. 61 62 #### DeliveryInfo 63 64 DeliveryInfo represents the information about which delivery method should be used and what delivery location should be used. 65 66 A DeliveryInfo has: 67 * a `code` that should identify a Delivery unique under the cart. It's up to you what code you want. You may want to follow the conventions used by the Default `DeliveryInfoBuilder` 68 * a `workflow` - that is used to be able to differentiate between different fulfillment workflows (e.g. pickup or delivery) 69 * a `method` - used to specify details for the delivery. It's up for the project what you want to use. E.g. use it to differentiate between `standard` and `express` 70 * a `deliverylocation` - A deliverylocation can be an address, but also a location defined by a code (e.g. such as a collection point). 71 72 The DeliveryInfo object is normally completed with all required infos during the checkout using the DeliveryInfoUpdateCommand 73 74 ##### Optional Port: DeliveryInfoBuilder 75 76 The DeliveryInfoBuilder interface defines an interface that builds initial `DeliveryInfo` for a cart. 77 78 The `DefaultDeliveryInfoBuilder` that is part of the package should be ok for most cases, it simply takes the passed `deliverycode` and builds an initial `DeliveryInfo` object. 79 The code used by the `DefaultDeliveryInfoBuilder` should be speaking for that reason and is used to initially create the `DeliveryInfo`: 80 81 The convention used by this default builder is as follow: `WORKFLOW_LOCATIONTYPE_LOCATIONCODE_METHOD_anythingelse` 82 83 Valid codes are: 84 * `delivery` (default) 85 * DeliveryInfo to have the item (home) delivered 86 * `pickup_store_LOCATIONCODE` 87 * DeliveryInfo to pickup the item in a (in)store pickup location 88 * `pickup_collection_LOCATIONCODE` 89 * DeliveryInfo to pickup the item in a special pickup location (central collection point) 90 91 92 ### CartItem details 93 94 There are special properties that require some explanations: 95 96 * `SourceId`: Optional represents a location that should be used to fulfill this item. 97 This can be the code of a certain warehouse or even the code of a retail store (if the item should be picked(sourced) from that location) 98 * There is a SourcingService interface - that allows you to register the logic of how to decide on the `SourceId` 99 100 ### Decorated Cart 101 102 If you need all the product information at hand - use the Decorated Cart - its decorating the cart with references to the product (dependency product package) 103 104 ```graphviz 105 digraph hierarchy { 106 size="5,5" 107 node[shape=record,style=filled,fillcolor=gray95] 108 edge[dir=both, arrowtail=diamond, arrowhead=open] 109 110 decoratedCart[label = "{decoratedCart||...}"] 111 decoratedItem[label = "{decoratedItem||...}"] 112 113 product[label = "{BasicProduct|+ ID\n|...}"] 114 115 cart[label = "{Cart|+ ID\n|...}"] 116 item[label = "{Item|ID\nPrice\nMarketplaceCode\nVariantMarketPlaceCode\nPrice\nQty|...}"] 117 118 decoratedCart->cart[arrowtail=none] 119 decoratedItem->item[arrowtail=odiamond] 120 121 decoratedItem->product[arrowtail="none"] 122 decoratedCart->decoratedItem 123 124 cart->item[] 125 126 } 127 128 ``` 129 130 ## Details about Price fields 131 132 Make sure you read the product package details about prices. 133 134 The cart needs to show prices with their taxes and additional cart discounts and all different subtotals etc. 135 What you want to show depends on the project, the type of shop (B2B or B2C), the discount logic and the implemented calculation details of the underlying cartservice implementation. 136 137 Some of the prices you may want to show are "calculable" on the fly (in this cases they are offered as methods) - but some highly depend on the tax and discount logic and they need to have the correct values set by the underlying cartservice implementation. 138 139 ### Cart invariants 140 141 * While the Flamingo price model (which is used) can calculate exact prices internal, we need "payable prices" to be set in the cart. This is to allow consistent adding and subtracting of prices and still always get a price that is payable. 142 143 * All sums - also the cart grand total is calculated and can be "tracked" back to the item row prices. 144 145 In order to get consistent sub totals in the cart, the cart model needs certain invariants to be matched: 146 * Item level: RowPriceNet + TotalTaxAmount = RowPriceGross 147 * Item level: TotalDiscount <= RowPriceGross 148 * All main prices need to have same currency (that may be extended later but currently it is a constraint) 149 150 ### About Tax calculation in general 151 152 It makes sense to understand the details of tax calculations - so please take the following example of a cart with 2 items: 153 * item1 net price 14,71 154 * item2 net price 10,18 155 * and a 19% VAT (G.S.T) 156 157 There are two principal ways of tax calculations (called vertical and horizontal) 158 * vertical: the final tax is calculated from the sum of all rounded item taxes: 159 * item1 +19% gross price rounded: 17,50 (tax 2,79) 160 * item2 +19% gross price rounded: 12,11 (tax 1,93) 161 * = GrandTotal = 29,61 / = totalTax: 4,72 162 * => SubTotalNet = 24,89 163 * Often preferred for B2C, when the item prices are shown as gross prices first. 164 * Pro: Easier to calculate 165 * Con: rounding errors may sum up in big orders 166 167 * horizontal: The final tax is calculated from the sum of all net prices of the item and then rounded 168 * => SubTotalNet: 24,89 => +19% rounded GrandTotal: 29,62 / included Tax: 4,73 169 * Often preferred for B2B or when the prices shown to the customer are as net prices at first. 170 171 * In both cases the tax might be calculated from a given net price or a given gross price (see product package config `commerce.product.priceIsGross`). 172 173 * discounts are normally subtracted before tax calculation 174 175 * At least in germany the law doesn't force one algorithm over the other. In this cart module it is up to the cart behaviour implementation what algorithm it uses to fill the tax. 176 177 178 ### Cartitems - price fields and method 179 180 The Key with "()" in the list are methods and it is assumed as an invariant, that all prices in an item have the same currency. 181 182 | Key | Desc | Math Invariants | 183 |--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| 184 | SinglePriceGross | Single price of product (gross) (was SinglePrice) | | 185 | SinglePriceNet | Net price (excl. taxes) for the product | | 186 | Qty | Quantity | | 187 | RowPriceGross | Price incl. taxes for the whole Qty of products (was RowTotal) | SinglePriceGross * Qty | 188 | RowPriceGrossWithDiscount | Price incl. taxes with deducted discounts for the whole Qty of products. This is in most cases the final price for the customer to pay. | RowPriceGross-TotalDiscountAmount() | 189 | RowPriceGrossWithItemRelatedDiscount | Price incl. taxes with deducted item related discounts for the whole Qty of products | RowPriceGross-ItemRelatedDiscountAmount() | 190 | RowPriceNet | Price excl. taxes for the whole Qty of products | SinglePriceNet * Qty | 191 | RowPriceNetWithDiscount | The discounted net price for the whole Qty of products | | 192 | RowPriceNetWithItemRelatedDiscount | price excl. taxes with deducted item related discounts for the whole Qty of products | | 193 | RowTaxes | Collection of all taxes applied for the given Qty of products | | 194 | TotalTaxAmount() | sum of all applied taxes for the whole Qty of products | RowPriceGross-RowPriceNet | 195 | AppliedDiscounts | List with the applied Discounts for this Item (There are ItemRelated Discounts and Discounts that are not ItemRelated (CartRelated). However it is important to know that at the end all DiscountAmounts are applied to an item (to make refunding logic easier later) | | 196 | TotalDiscountAmount | Sum of all applied discounts (aka the savings for the customer) | Sum of AppliedDiscounts (ItemRelatedDiscountAmount + NonItemRelatedDiscountAmount) | 197 | ItemRelatedDiscountAmount | Sum of all itemrelated Discounts, e.g. promo due to product attribute | | 198 | NonItemRelatedDiscountAmount | Sum of non-itemrelated Discounts, e.g. a general promo | | 199 200 [comment]: <> (use https://www.tablesgenerator.com/markdown_tables to update the table) 201 202 ### Delivery - price fields and method 203 204 | Key | Desc | Math | 205 |---------------------------------|----------------------------------------------------------------------------------|------------------------------------------------------------------| 206 | GrandTotal | The final amount that need to be paid by the customer (Gross) | SubTotalGross + ShippingItem.PriceGross + TotalDiscountAmount | 207 | SubTotalGross | Sum of items RowPriceGross (without shipping/discounts) | | 208 | SubTotalGrossWithDiscounts | Sum of row gross prices reduced by the applied discounts | SubTotalGross() + SumSubTotalDiscountAmount() | 209 | SubTotalNetWithDiscounts | Sum of row net prices reduced by the net value of the applied discounts | SubTotalNet() + SumSubTotalDiscountAmount() | 210 | SubTotalNet | Sum of items RowPriceNet | | 211 | SumRowTaxes() | List of the sum of the different RowTaxes (of cart items) | | 212 | SumTotalTaxAmount() | Sum of all applied item taxes including shipping taxes | | 213 | TotalDiscountAmount | Sum off all discounts affecting the delivery (on cart items and shipping) | SumSubTotalDiscountAmount + shippingItem.AppliedDiscounts.Sum() | 214 | SubTotalDiscountAmount | Sum of all cart items discounts (without shipping) | Sum of items TotalDiscountAmount | 215 | NonItemRelatedDiscountAmount | Sum of discounts that are not related to the item (including shipping discounts) | | 216 | ItemRelatedDiscountAmount | Sum of discounts that are related to the item (including shipping discounts) | | 217 218 ### Cart - price fields and method 219 220 | Key | Desc | Math | 221 |---------------------------------|----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------| 222 | GrandTotal | The final amount that need to be paid by the customer (gross) | SubtotalGrossWithDiscounts + SumShippingGrossWithDiscounts + Sum(Totalitems) | 223 | Totalitems | List of (additional) Totalitems. Each have a certain type - you may want to show some of them in the frontend. | | 224 | SumShippingNet | Sum of all shipping costs | Sum of all deliveries shipping items PriceNet | 225 | SumShippingNetWithDiscounts | Sum of all shipping costs with all shipping discounts | SumShippingNet + Sum of all applied shipping discounts | 226 | SumShippingGross | Sum of all shipping costs including tax | Sum of all deliveries shipping items PriceGross | 227 | SumShippingGrossWithDiscounts | Sum of all shipping costs with all shipping discounts including tax | Sum of shipping items PriceGrossWithDiscounts | 228 | SubTotalGross | Sum of all delivery subtotals (without shipping / discounts) | Sum of deliveries SubTotalGross | 229 | SubTotalNet | Sum of all delivery net subtotals (without shipping / discounts) | Sum of deliveries SubTotalNet | 230 | SumTaxes() | The total taxes of the cart - as list of Tax | | 231 | SumTotalTaxAmount() | The overall Tax of cart | | 232 | SubTotalGrossWithDiscounts | Sum of row gross prices reduced by the applied discounts | Sum of deliveries SubTotalGrossWithDiscounts | 233 | SubTotalNetWithDiscounts | Sum of row net prices reduced by the net value of the applied discounts | Sum of deliveries SubTotalNetWithDiscounts | 234 | TotalDiscountAmount | Sum of all discounts (incl. shipping) | Sum of deliveries TotalDiscountAmount | 235 | NonItemRelatedDiscountAmount | Sum of discounts that are not related to the item (including shipping discounts) | Sum of deliveries NonItemRelatedDiscountAmount | 236 | ItemRelatedDiscountAmount | Sum of discounts that are related to the item (including shipping discounts) | Sum of deliveries ItemRelatedDiscountAmount | 237 | TotalGiftCardAmount | The part of GrandTotal which is paid by gift cards | | 238 | GrandTotalWithGiftCards | The final amount with the applied gift cards subtracted. If there are no gift cards, equal to GrandTotal. | GrandTotal - SumAppliedGiftCards | 239 | GetVoucherSavings() | Returns the sum of Totalitems of type voucher | | 240 241 ### Typical B2C vs B2B usecases 242 243 B2C use cases: 244 * The item price is with all fees (gross). The discount will be reduced from the price and all the fees, duty and net price will be calculated from this. 245 * Typically you want to show per row: 246 * SinglePriceGross 247 * Qty 248 * RowPriceGross 249 * RowPriceGrossWithDiscount 250 251 * The cart normally shows: 252 * SubTotalGross 253 * Carttotals (non taxable extra lines on cart level) 254 * Shipping 255 * The included Total Tax in Cart (SumTaxes) 256 * GrandTotal (is what the customer needs to pay at the end including all fees) 257 258 B2B use cases: 259 * The item price is without fees (net). The discount will be reduced and then the fees will be added to get the gross price. You probably do want to show per row: 260 * SinglePriceNet 261 * RowPriceNet 262 * RowPriceNetWithDiscount 263 264 * The cart then normally shows: 265 * SubTotalNet 266 * Carttotals (non taxable extra lines on cart level) 267 * Shipping 268 * The included Total Tax in Cart (SumTaxes) 269 * GrandTotal (is what the customer need to pay at the end inkl all fees) 270 271 ### Building a Cart with its deliveries and items 272 273 The domain concept is that the cart is returned and updated by the "Cartservice" and the underlying modify behaviour, which is the main secondary port of the package that needs to be implemented (see below). 274 All calculations for the public price fields must be done by the modify behaviour implementation. 275 276 ### About charges 277 278 If you have read the sections above you know about the different prices that are available at item, delivery and cart level and how they are calculated. 279 280 There is something else that this cart model supports - we call it "charges". All the price amounts mentioned in the previous chapters represents the value of the items in the carts default currency. 281 282 However this value need to be paid - when paying the value it can be that: 283 - customer wants to pay with different payment methods (e.g. 50% of the value with PayPal and the rest with credit card) 284 - also the value can be paid in a different currency 285 286 287 The desired split of charges is saved on the cart with the "UpdatePaymentSelection" command. 288 If you dont need the full flexibility of the charges, than you will simply always pay one charge that matches the grand total of your cart. 289 Use the factory `NewDefaultPaymentSelection` for this, which also supports gift cards out of the box. 290 291 The PaymentSelection supports the [Idempotency Key pattern](https://stripe.com/blog/idempotency), the `DefaultPaymentSelection` will generate a new random UUID v4 during creation. 292 In cases of a payment error (e.g. aborted by customer / general error) the Idempotency Key needs to be regenerated to avoid a loop and enable the customer to retry the payment. 293 The PaymentSelection therefore offers a `GenerateNewIdempotencyKey()` function, which should also called during generation of the PaymentSelection. 294 295 If you want to use the feature it is important to know how the cart charge split should be generated: 296 297 1. the product that is in the cart might require that his price is paid in certain charges. An example for this is products that need to be paid in miles. 298 2. the customer might want to select a split by himself 299 300 You can use the factory on the decorated cart to get a valid PaymentSelection based on the two facts 301 302 It is also important to note that changes to the shopping cart may affect an existing PaymentSelection. We therefore recommend that you validate PaymentSelection after each shopping cart transaction. 303 304 ## Domain - Secondary Ports 305 306 ### Must Have Secondary Ports 307 308 **GuestCartService, CustomerCartService (and ModifyBehavior)** 309 310 `GuestCartService` and `CustomerCartService` are the two interfaces that act as secondary ports. 311 They need to be implemented and registered: 312 313 ```go 314 injector.Bind((*cart.GuestCartService)(nil)).To(infrastructure.YourAdapter{}) 315 injector.Bind((*cart.CustomerCartService)(nil)).To(infrastructure.YourAdapter{}) 316 ``` 317 318 Most of the cart modification methods are part of the `ModifyBehaviour` interface - if you look at the secondary ports you will see, that they need to return an (initialized) implementation of the 319 `ModifyBehaviour` interface - so in fact this interface needs to be implemented when writing an adapter as well. 320 321 **in-memory cart adapter** 322 There is a "DefaultCartBehaviour" implementation as part of the package. It allows basic cart operations with a cart that is stored in memory. 323 Since the cart storage is not persisted in any way we currently recommend the usage only for demo / testing. 324 325 The in memory adapter supports custom gift card / voucher logic by implementing the `GiftCardHandler` and `VoucherHandler` interfaces. 326 327 **PlaceOrderService** 328 329 There is also a `PlaceOrderService` interface as secondary port. 330 Implement an adapter for it to define what should happen in case the cart is placed. 331 332 There is a `EmailAdapter` implementation as part of the package, that sends out the content of the cart as mail. 333 334 #### Optional Port: CartValidator 335 336 The CartValidator interface defines an interface to validate the cart. 337 338 If you want to register an implementation, it will be used to pass the validation results to the web view. 339 Also the cart validator will be used by the checkout - to make sure only valid carts can be placed as order. 340 341 #### Optional Port: ItemValidator 342 343 ItemValidator defines an interface to validate an item **BEFORE** it is added to the cart. 344 345 If an Item is not valid according to the result of the registered *ItemValidator* it will **not** be added to the cart. 346 347 ### Store "any" data on the cart 348 349 This package offers also a flexible way to store any additional objects on the cart: 350 351 See this example: 352 353 ```go 354 type ( 355 // FlightData value object 356 FlightData struct { 357 Direction string 358 FlightNumber string 359 } 360 ) 361 362 var ( 363 // need to implement the cart interface AdditionalDeliverInfo 364 _ cart.AdditionalDeliverInfo = new(FlightData) 365 ) 366 367 func (f *FlightData) Marshal() ([]byte, error) { 368 return json.Marshal(f) 369 } 370 371 func (f *FlightData) Unmarshal(data []byte) error { 372 return json.Unmarshal(data, f) 373 } 374 375 376 // Helper for storing additional data 377 func StoreFlightData(duc *cart.DeliveryInfoUpdateCommand, flight *FlightData) ( error) { 378 if flight == nil { 379 return nil 380 } 381 return duc.SetAdditional("flight",flight) 382 } 383 384 // Helper for getting stored data: 385 func GetStoredFlightData(d cart.DeliveryInfo) (*FlightData, error) { 386 flight := new(FlightData) 387 err := d.LoadAdditionalInfo("flight",flight) 388 if err != nil { 389 return nil,err 390 } 391 return flight, nil 392 } 393 ``` 394 395 ## Application Layer 396 397 Offers the following services: 398 399 * CartReceiverService: 400 * Responsible to get the current users cart. This is either a GuestCart or a CustomerCart 401 * Interacts with the local CartCache (if enabled) 402 * CartService 403 * All manipulation actions should go over this service (!) 404 * Interacts with the local CartCache (if enabled) 405 406 Example Sequence for AddToCart Application Services to 407 408  409 410 ### RestrictionService 411 412 The Restriction Service provides a port for implementing product restrictions. By using Dingo multibinding to `cart.MaxQuantityRestrictor`, 413 you can add your own restriction to the service. The Restriction Service is called during cart add / update item. 414 415 The `Restrict` function returns a `RestrictionResult` containing information about the restriction. This `RestrictionResult` specifies whether a restriction applies, 416 the maximum allowed quantity and the remaining difference in relation to the current cart. 417 418 The Service itself consolidates the results of all bound restrictors and returns the most restricting result. 419 420 ### Event Handling 421 422 Event Handling is mainly concerned with the transformation of a guest shopping cart into a customer shopping cart. 423 Various options are available for this. By default, the content of both shopping carts is merged. 424 425 The following strategies can be set via the config `commerce.cart.mergeStrategy`: 426 * `merge` (default): Merge the content of the guest and customer cart 427 * `replace`: Replace the customer cart with the guest cart content 428 * `none`: Don't do anything, guest cart is lost during customer sign-in. 429 430 ## A typical Checkout "Flow" 431 432 A checkout package would use the cart package for adding information to the cart, typically that would involve: 433 434 * Checkout might want to update Items: 435 * Set sourceId (Sourcing logic) on an item and then call `CartBehaviour.UpdateItem(item,itemId)` 436 437 * Updating DeliveryInformation by calling `CartBehaviour.UpdateDeliveryInfo()` 438 * (for updating ShippingAddress, WishDate, ...) 439 440 * Optional updating Purchaser Infos by calling `CartBehaviour.UpdatePurchaser()` 441 442 * Finish by calling `CartService.PlaceOrder(CartPayment)` 443 * CartPayment is an object, which holds the information which Payment is used for which item 444 445 ## Interface Layer 446 447 ### Cart Controller 448 449 The main Cart Controller expects the following templates by default: 450 451 * checkout/cart 452 * checkout/carterror 453 454 The templates get the following variables passed: 455 456 * DecoratedCart 457 * CartValidationResult 458 459 ### Cart template function 460 461 Use the `getCart` template function to get the cart. 462 Use the `getDecoratedCart` template function to get the decorated cart. 463 464 ```pug 465 - 466 var cart = getCart() 467 var decoratedCart = getDecoratedCart() 468 var currentCount = decoratedCart.cart.getCartTeaser.itemCount 469 ``` 470 471 ### Cart Ajax API 472 473 There are also of course ajax endpoints, that can be used to interact with the cart directly from your browser and the javascript functionality of your template. 474 To get an idea of all endpoints, have a look at the module.go, especially the apiRoutes method where endpoints are handled. 475 476 477 ### GraphQL 478 479 The module exposes most of its functionality also via GraphQL, have a look at the [schema](interfaces/graphql/schema.graphql) to see all available querys / mutations.