Skip to content

Alternative checkout flow proposal #192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Sep 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions design-documents/checkout/alternative-checkout-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# Alternative checkout flow

In the scope of work on GraphQL and storefront APIs, we have an opportunity to improve the design and features of storefront checkout.

The purpose of this document is to discuss possible alternatives to current Magento checkout flow which might be introduced as an alternative API.

The cart is just a container for the items the user wants to purchase. In the proposed flow the cart is created as soon as the user adds the product to cart. The data from the cart entity should be enough to render mini-cart and cart pages (with totals estimation). Taxes and other adjustments are not calculated at these steps.

The quote, on the other hand, contains a full break down of all adjustments calculated. It provides the user with the total he has to pay for the items in the cart.

It is possible to split cart items into separate quotes. This can be done based on shipping addresses or shipping sources.

Each quote is used to create a separate order, the quote will have time to live (TTL) based on different factors like coupon expiration time, stock availability, available store credit, etc. Multiple payment methods (with independent billing address each) can be selected during each order creation.

This proposal describes multiple approaches for the checkout flow.

## Current checkout flow

This section describes current checkout flow.

![Current checkout flow](img/alternative-checkout-flow.png)

1. Products rendered from Catalog. An item is added to the cart.
2. The cart calls the catalog product’s repository to get product details.
3. The view cart action calls a totals collector to estimate totals on the quote.
4. “Proceed to Checkout” starts a one-page checkout flow, where a customer can specify shipping and billing addresses, choose a payment solution and place an order.
5. When the customer enters or selects the shipping address, shipping rates are estimated, which triggers whole totals calculation.
6. On the Select Payment Method step, the customer can specify a billing address, choose a payment method, and apply discounts and gift cards.
7. Actions like applying a customer balance, discounts, gift cards trigger the recalculation of the totals.
8. The payment review step also contains summary details like items, shipping method, totals.

Most of the actions like page reloading, retrieving shipping rates, applying discounts, gift cards, etc. triggers whole cart/quote recalculation.

The different quote's calculators, like Cart Price Rule calculator, change quote object and totals and behavior might be unpredictable as different calculators can operate with the same data.

## Uni-directional checkout flow

The flow assumes that each component like Cart, Quote, Shipping, etc. will receive all needed details to perform operations. This approach allows resolving and reducing a list of dependencies between modules.

![Checkout data flow](img/checkout-data-flow.png)

The Cart will depend on Catalog. Quote will have a knowledge about PIM, Shipping Rates estimator, Coupons, Store Credit and other services for totals calculation. Cart will provide data for Quote. Quote will provide data for Order. Order will not have any knowledge about Catalog, Quote, Cart.

![One-directional checkout flow](img/alternative-checkout-flow-2.png)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the diagram we have separate total calculation for cart and quote. Considering that a lot of totals will be the same, can we have unified way for totals calculation?

Copy link
Contributor

@YevSent YevSent Sep 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totals won't be the same. In most cases, cart totals contain items summary and some adjustments like free item, the quote totals - shipping rate, discounts, taxes, customer balance, gift cards. But, in general, I don't see restrictions to don't use it for cart re-calculation, as calculation should be used for different scenarios like B2B quotes.

The current shopping cart contains summary estimation block (with shipping, taxes, etc.) but actually it's not a shopping cart but a quote estimation as right now we don't have a separation to cart and quote.


1. An item that contains all needed attributes (like SKU, quantity, regular price and options) is added to the cart.
2. The cart calls the totals estimator for basic items price calculation. This calculation is needed only for a summary representation in the shopping cart. It does not make sense to check prices from PIM or Catalog.
3. The “Proceed to Checkout” triggers the creation of a Quote entity. It does not contain an estimate of the totals.
4. After the customer specifies a shipping method, the shipping rates estimator provides all available shipping methods for this address. If a customer wants to specify multiple shipping addresses, then multiple quotes will be created (one per shipping address) and a list of available shipping methods will be provided for all addresses.
5. On the select payment method step, a customer chooses the payment method, specifies a billing address while Magento applies discounts and gift cards, calculates customer balance, etc.
6. After a payment method is chosen and all possible price adjustments are specified, the summary can be reloaded dynamically based on changes for each item.
7. The summary block includes quote items, shipping addresses and other quote/multi-quote details.
8. The change of any or all price adjustments triggers a new quote creation and totals recalculation.
9. The Quotes Estimator triggers totals calculation. Each calculator gets all the needed data, like the actual shipping rate, product prices, tax rules, the current state of customer balance, etc.

The `Quotes Estimator` will be the main entry point to create a quote based on the provided input and the `Totals Collector` will provide totals calculation based on the provided quote object and the configuration. The `Shipping Rates Estimator` will be agnostic to the quote object and will provide shipping rates based on input data like shipping origin, shipping destination, items dimensions. The unified input data would allow using the same `Shipping Rates Estimator` for RMA, order estimated delivery, etc. Also, the estimator will support rates retrieving only for a specific shipping method to reduce a number of calls to other shipping carriers (the current implementation gets rates from all configured carriers).

The following sequence diagram shows how the place order will look like according to the proposed solution:

![Place Order Flow](img/alternative-checkout-place-order.png)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We recalculate totals on the place order again. As I understand this is for the case when coupon balance, store credit or gift card balance may change. Are we going to recalculate shipping rates and taxes too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All totals will be recalculated based on configuration and provided data including taxes, discounts, gift cards, customer balance, line items, shipping rate, etc. The shipping rate will be recalculated only for the selected shipping method, it doesn't make sense to go through all available carriers because the quote already has the selected shipping method.


The [GraphQL](https://graphql.org/learn/queries/) allows combining multiple queries/mutations in the same HTTP request which reduce the number of communications between client and backed applications. According to the proposed flow, the requests like getting available shipping and payment methods in some cases can be merged into a single request. Also, depending on how the checkout flow is built, requests to select a shipping method, payment, applying a discount, customer balance can be combined, which allows to do not re-calculate quote totals for each operation and do it only once for final input data.

## Data Flow

1. When Quote is created?
* When a customer clicks `Proceed to Checkout`
2. Cart properties:
* Line Items:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can more information on how different product types would be represented be added to the proposal?
Currently a product in the quote is represented by one or more quote items. Each quote item can have one or more quote item options to store relevant data.
These options are used for many different things, starting from enabling reorders to linking of related quote items over additional data to display on line items to many different types of customizations.
Because they are so heavily used I would like to read a little on how those problems would be solved with this proposal

* SKU
* Selected options (custom/configurable)
* Quantity
* Regular price
* Price
3. Quote factory arguments: Quote
* !Line Items
* Dimensions (weight & size) (based on LineItems)
* Shipping (requited for physical products)
* Address
* Selected Shipping Method
* Billing address (required for virtual products). Need use cases
* Coupons
* Gift cards
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gift cards, coupons and store credits may be moved to the payment step.

* Store credit
* Cart rules (calculated by Applicable catalog rules calculator)
* Customer
4. Totals calculator: Totals
* QuoteArgumentDTO
* PreviousTotals
5. Quote
* Line Items
* Dimensions
* Shipping address
* Price Adjustments
* Line Items Totals
* Totals
* Adjustments
* Taxes
* Cart rule discounts
* Coupons
* Gift cards
* Store credit
* Taxes
* Customer
6. Place Order: Order
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we allow creating multiple quotas based on the single cart (quote segregation based on addresses), from my standpoint it makes sense to make order placement as an atomic operation which allows submitting multiple quotes. It runs us in another question - multiple payments handling together. but this also has a sense. Orders with different destination mean addresses may have different process flow which is fine. For example, the multi-tenant installation will require such functionality.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second this, quotes split by shipping address should still be payable with a single payment during checkout. This has been a major limitation of the current design and is one of the reasons so few merchants utilize the multi address checkout. Having to do separate payments for each order makes the process incompatible with most current payment processors.

* Quote
* PaymentMethod with Billing Addresses

## Quote creation flow

A quote should be created from Cart after a customer clicks `Proceed to Checkout`, on the API level it will be a separate service contract like `QuoteEstimator::create`. After the quote is created, it became an immutable object and only totals can be recalculated (the totals will be represented as a separate object). If a list of items should be changed, when a new quote object should be created. Quote will live until TTL expires or an order is successfully placed.

![Quote Calculation](img/alternative-quote-calculation.png)

## Totals calculation improvements

The current approach for quote calculation has multiple drawbacks like changes in the quote object, a quote totals collector is difficult to customize, a complicated logic to define the order of totals calculation (tax before/after discount, discount/tax rules for shipping, etc.), additional calls to 3rd party systems for shipping rate prices updating.

The proposed solution assumes that a quote will be an immutable object and will operate with totals list, each calculator will create add new totals object based on totals list and the order of each calculation can be changed in runtime. As one of the benefits of the proposed approached - the list of calculators and their order can be visualized for better calculation understanding.

Let's consider the calculation of the totals might look like.

![Totals Calculation](img/totals-calculation-pipeline.png)

Each calculator receives Quote DTO and Totals List, calculates totals, creates new Totals DTO with the calculated amount and adds it to Totals List. This approach allows to do not change a quote object, have a defined interface for totals and change the order of calculation. Magento provides multiple configurations to change the order of calculation, for example, discount can be applied before shipping amount or after.

Copy link

@Vinai Vinai Aug 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One common customization for the current totals calculation is that custom discounts are implemented by adjusting prior totals, e.g. the subtotal, from a later total model. If the totals are immutable, there should be an option for total models to return a changed list of totals, e.g. remove or replace total models that where calculated by prior. Each total model then can be immutable, but can be replaced or removed from the TotalsListInterface as needed by customizations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I clearly understand your point but totals calculators will be created in runtime according to the configuration and priority and each calculator has isApplicable method (the customization can disable needed calculator, add a new one) which decides if calculator should be executed or not, for example, it does not make sense to execute a shipping calculator for the quote only with virtual products.

A custom discount calculator can be added in the same way as standard calculators with own priority and grand total calc will automatically collect all amounts and adjustments.

![Totals Calculation 2](img/totals-calculation-pipeline-2.png)

As the list of calculators and their order depend not only on configuration but also on such factors like a presence of shipping address (gift cards, virtual, downloadable products do not require shipping address) the calculations pipeline should be built-in runtime.

![Totals Calc Sequence Diagram](img/totals-calc-sequence-diagram.png)

And the following UML class diagram represents needed classes and interfaces.

![UML class diagram](img/totals-calc-class-diagram.png)

All client code will work with totals via `TotalsListInterface`, which will contain all calculated totals, also it provides a possibility to get all totals as list of objects.

All totals will be persisted as JSON structure into the database which allows having dynamic totals structure for different entities like quote, order, invoice, credit memo without increasing the list of database columns. For example, the current `quote` database table contains 28 fields related to different types of totals like price with/without tax, used customer balance, amounts with/without base currency, etc. The new structure would allow to store all totals in one field. The currency exchange rate will be stored instead of duplication of amounts for base and display currencies. As all calculations are happen only in base currency, the display currency is used for amount representation on storefront.

The example of totals presentation:

```json
{
"amount":1002,
"currency":"USD",
"display_currency":"EUR",
"currency_exchange_rate":0.92,
"is_applicable":true,
"code":"totals_list",
"totals":[
{
"code":"subtotal",
"amount":902,
"is_applicable":true
},
{
"code":"shipping",
"amount":100,
"is_applicable":true
},
{
"code":"discount",
"amount":0,
"is_applicable":false
},
{
"code":"grand_total",
"amount":1002,
"is_applicable":true
}
]
}
```

The next schema represents relations between totals and other entities like quote, order, invoice, credit memo.

![Totals Database ER diagram](img/totals-db-er.png)

The usage of UUID would allow removing dependency to the entity type and make entity/totals storage more agnostic.

![Totals Database ER + UUID diagram](img/totals-er-uuid.png)

### Totals Calculation Priority

The current implementation of totals calculation allows to specify the priority of calculators execution:
- 100 Subtotal
- 190 Gift Wrapping
- 200 Subtotal Tax
- 225 Weee (FPT)
- 300 Discount
- 350 Shipping
- 375 Shipping Tax
- 400 Shipping Discount
- 425 Gift Wrapping Tax
- 450 Tax
- 460 Weee (FPT) Tax
- 475 Gift Wrapping After Tax
- 550 Grand Total
- 700 Gift Card Account
- 750 Customer Balance
- 1000 Reward

The last three calculators `Gift Card Account`, `Customer Balance`, `Reward` have incorrect priority because they adjust `Grand Total` which should be last in the list.

### Data for calculation

A quote has multiple parts of totals which should recalculated each time like:

- Discount - includes expiration time, availability, reservation0
- Store Credit - includes balance availability
- Shipping rate price - actual price for a product delivery
- Gift Card - includes expiration time, number of usages and balance availability

There is still an open question if catalog prices should be re-calculated for an active quote.

In the scope of Services Isolation project, the calculation of the totals can be implemented as a separate service.

## Use cases

The proposed solution must be verified against use cases described in [this document](alternative-checkout-flow/use-cases.md).

## Open questions

The current implementation assumes that quote should be recalculated on any change like cart price rule, gift card, etc. Also, the quote is recalculated before place order and each calculator makes additional communication to other components like Catalog, Shipping Carrier to get the latest updates.

There are still open questions related to the proposed solutions:

- When and which data should be validated (coupons expiration, store credit/rewards points availability, gift card balance, actual shipping rates)?
- Do we guarantee that product prices in the quote always actual during quote TTL?
- How the reservation mechanism should be implemented (a customer wants to apply the coupon code/store credit/rewards balance for multiple quotes)?
- Should quote TTL be fixed or dynamically calculated in runtime?
- Do we want to have only one `Add to Cart` entry-point for multiple product types?
- Should shipping address be a part of the Cart? (No)
- Should we migrate to integer representation of amounts to avoid one-cent issues and support zero-decimal currencies out-of-box?
- The current shopping cart contains actions which do not belong to it like shipping estimation, coupon and gift cards.
- Do we want to use some kind of signature for quote and totals to do not re-calculate totals on every page loading?

## Summary

The main benefits of the proposed changes are the following:

- Clear boundaries between Cart and Quote components
- Multi-address checkout out-of-box
- Support of immutable multi-quote flow
- Unified interface via `ToalsListInterface` for totals calculation
- One-directional flow allows reducing communication between components
- Possibility to separate quotes based on stock availability
- Improving API customizability and extensibility
- Performance improvements based on reducing dependencies between components and their communication
- Simplified and unified totals storage
39 changes: 39 additions & 0 deletions design-documents/checkout/alternative-checkout-flow/use-cases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
## Checkout use cases

The following use cases must be used as a check list to verify the alternative checkout design. The list may be extended when additional use cases are identified.

- Based on multiple-shipping address, different stocks, etc. we can estimate multiple quotes at the same time
- The Cart it's just a storage for items and might have only totals estimation
- Quote and cart will have own TTL
- The totals calculation is concentrated into a separate independent component
- Cart might receive updates about Catalog price changes
- Digital products (skipped shipping information and method)
- Physical products
- Buying products for someone (shipping and billing are different)
- Guest checkout
- Do not check user context in the server, this should be solved on the controller level
- Localize currency selector, zip-code, region etc based on the selected country
- Unified storefront-backend checkout field validation (excluding the one which requires access to the storage)
- Use some config which will be used for field validation rules in resolvers/JS/etc
- Cart summary on the “Order success” page, including Guest checkout
- Tier prices in cart (just rely on tier prices from catalog API)
- Independent totals calculation for quote and cart
- Avoid surprise cost increase on the later checkout stages, be as transparent as possible on every step
- Include default shipping estimate, maybe tax estimate based on geolocation?
- Make it clear that taxes are not included into cart estimate
- Give note about taxes/shipping being calculated at the next steps
- Cart price rules should be calculated during checkout, not just before placing the order
- Give an option to hide coupon code field by default (replace with link to get the field)
- Provide applied coupon name in the quote totals summary
- Provide shipping time estimate
- Secure payment logo (lock image)
- Redirect to cart after product was added to cart, display up-sells (by default)
- Address autocomplete service (based on street complete other fields) via server-side call to google
- Display currency vs base currency (store exchange rate instead of set of prices in display currency)
- Enter coupon on the last step, just before placing order. Store credit and rewards just before payment method selection
- Store pickup shipping method
- Multi quote (each quote is converted into a separate order)
- Push notifications do not make sense since checkout is usually pretty quick. The chance of price changing during checkout is slim

Questions:
- Are there any legal restrictions on when to display tax in checkout
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design-documents/checkout/img/totals-db-er.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design-documents/checkout/img/totals-er-uuid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.