Skip to content

[ongoing discussion] Draft for cart operations and other GraphQL improvements #15

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 24 commits into from
Oct 19, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d8e9846
Initial draft for add items to cart GraphQL mutations
paliarush Jul 31, 2018
30ef22f
Update AddGiftCardProductToCart.graphqls
paliarush Jul 31, 2018
dc06523
Eliminated CartItemProduct and CartItemImage. All data can be fetched…
paliarush Aug 2, 2018
d638fa9
Added coupon codes support
paliarush Aug 3, 2018
902e999
Renamed coupon removal mutation
paliarush Aug 3, 2018
43829b3
Reworded "overview" in the readme
paliarush Aug 6, 2018
7d71efc
Proposals for input and output wrappers
paliarush Aug 7, 2018
83aa880
Schema for cart prices
paliarush Aug 8, 2018
c6f7d84
Fixed typos in schema for cart prices
paliarush Aug 8, 2018
47b9607
Added basic fields for cart type
paliarush Aug 8, 2018
bdd70b4
Adjusted cart schema
paliarush Aug 9, 2018
c5aaccb
Adjusted cart schema
paliarush Aug 13, 2018
c87159c
Proposal to change resolver interface return type
paliarush Aug 14, 2018
f2ca044
Proposal to change resolver interface return type
paliarush Aug 14, 2018
10ce77e
Adjusted cart schema
paliarush Aug 15, 2018
5cc770f
Adjusted cart schema
paliarush Aug 16, 2018
b0bc80e
Adjusted cart schema
paliarush Aug 17, 2018
fa08ab1
Proposal for input-output extensibility
paliarush Aug 21, 2018
5a831aa
Proposal for input-output extensibility
paliarush Aug 21, 2018
a940475
Adding configurable products to cart using variant SKU instead of an …
paliarush Sep 7, 2018
299809a
Added schema for cart address operations
paliarush Sep 18, 2018
9635092
Iterated on schema for cart address operations
paliarush Sep 20, 2018
f0e011a
Merge remote-tracking branch 'remotes/origin/master' into graphql-car…
paliarush Oct 19, 2018
ca3a063
Merge remote-tracking branch 'remotes/mainline/master' into graphql-c…
paliarush Oct 19, 2018
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
30 changes: 30 additions & 0 deletions design-documents/graph-ql/coverage/CouponOperations.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type Mutation {
applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput
removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput
}

input ApplyCouponToCartInput {
cart_id: String!
coupon_code: String!
}

type ApplyCouponToCartOutput {
cart: Cart!
}

type Cart {
applied_coupon: AppliedCoupon
}

type AppliedCoupon {
# Wrapper allows for future extension of coupon info
code: String!
}

input RemoveCouponFromCartInput {
cart_id: String!
}

type RemoveCouponFromCartOutput {
cart: Cart
}
49 changes: 49 additions & 0 deletions design-documents/graph-ql/coverage/add-items-to-cart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
**Overview**

As a Magento developer, I need to manipulate the shopping cart via GraphQL so that I can build basic ecommerce experiences for shoppers on the front-end using only GraphQL.
Copy link
Contributor

Choose a reason for hiding this comment

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

for shoppers on the front-end using only GraphQL.

This is a big nitpick, so...sorry!

I think it's super important that we avoid language that couples the concept of GraphQL in Magento to the front-end specifically.

I know it's a small thing, but we want to make sure everyone has the mindset that this is an API for any external consumer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please suggest how to stress, that GraphQL in Magento is intended for store front scenarios only, not for admin ones.

Copy link
Contributor

@DrewML DrewML Aug 3, 2018

Choose a reason for hiding this comment

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

How about:

As a Magento developer, I need to manipulate the shopping cart via GraphQL so that I can programmatically create orders on behalf of a shopper.

Thoughts?


GraphQL needs to provide sufficient mutations (ways to create/update/delete data) for a developer to build out the storefront checkout experience for a shopper.

**Use cases:**
- Both guest and registered shoppers can add new items to cart
- Both guest and registered shoppers can update item qty in cart
- Both guest and registered shoppers can remove items from cart
- Both guest and registered shoppers can update the configuration (for a configurable product) or quantity of a previously added configurable product in cart
- Edit Item link > Product page > Update configuration or qty > Update Cart

**Main decision points:**

- Separate mutations for each product type while adding items to cart. Each operation will be supporting bulk use case
- Uniform interface for guest vs customer
- Separate mutations for each checkout step
- Create empty cart
- Add items to cart
- Set shipment method
- Set payment method
- Set addresses
- Same granularity for updates and removals
- Possibility to combine mutations for checkout steps
- Can create "order in one call" mutation in the future if needed
- Hashed IDs for cart items
- Single input object
- Async nature of the flow must be supported on the client side (via AJAX calls)
Copy link
Contributor

@DrewML DrewML Jul 31, 2018

Choose a reason for hiding this comment

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

Might want to expand on this point more, since all requests from the browser are async (barring sync XMLHTTPRequest, which is deprecated).

Maybe add some additional language around this clarifying that it creates a "job" of sorts, which helps imply that the results of the mutation will be some object representing a task that will be finished some time in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Asynchronous server side implementation is not approved yet. The schema may change, if it will be approved.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's important to include why there is a desire for these mutations to be async. When we chatted briefly yesterday, I got the impression that it's because this could be a long-running operation, and we don't want to keep the request open. Is that accurate?

It's just worth making the justification clear, because a concept of a "job" that needs to be polled (or a subscription) pushes some complexity to the consumer of the API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not necessarily long-running, may be just a lot of concurrent requests during peak hours. Decision on the async server side is not made yet.



**Open questions:**

- Do we want to implement server-side asynchronous mutations by default?
Copy link
Contributor

Choose a reason for hiding this comment

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

By default for cart mutations, or for all mutations?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@akaplya suggested for all mutations. Still under discussion.

Copy link

Choose a reason for hiding this comment

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

For a server less architecture as we have in CIF this might not work OOTB.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The decision is to have Synchronous implementation by default. Asynchronous implementation may be added later on the framework level as it was for REST.


**Proposed schema for adding items to cart:**

- [AddSimpleProductToCart](AddSimpleProductToCart.graphqls)
- [AddBundleProductToCart](AddBundleProductToCart.graphqls)
- [AddConfigurableProductToCart](AddConfigurableProductToCart.graphqls)
- [AddDownloadableProductToCart](AddDownloadableProductToCart.graphqls)
- [AddGiftCardProductToCart](AddGiftCardProductToCart.graphqls)
- [AddGroupedProductToCart](AddGroupedProductToCart.graphqls)
- [AddVirtualProductToCart](AddVirtualProductToCart.graphqls)


**My Account area impacted:**
- Cart
- Minicart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
type Mutation {
addBundleProductsToCart(input: AddBundleProductsToCartInput): AddBundleProductsToCartOutput
}

input AddBundleProductsToCartInput {
cart_id: String!
cartItems: [BundleProductCartItemInput!]!
}

input BundleProductCartItemInput {
sku: String!
quantity: Float!
bundle_options:[BundleOptionInput!]!
customizable_options:[CustomizableOptionInput!]
}

input BundleOptionInput {
id: Int!
quantity: Float!
value: [String!]!
}

type AddBundleProductsToCartOutput {
cart: Cart!
}

type BundleCartItem implements CartItemInterface {
customizable_options: [SelectedCustomizableOption]!
bundle_options: [SelectedBundleOption!]!
}

type SelectedBundleOption {
id: Int!
label: String!
type: String!
# No quantity here even though it is set on option level in the input
values: [SelectedBundleOptionValue!]!
sort_order: Int!
}

type SelectedBundleOptionValue {
id: Int!
label: String!
quantity: Float! # Quantity is displayed on option value level, while is set on option level
price: CartItemSelectedOptionValuePrice!
sort_order: Int!
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
type Mutation {
addConfigurableProductsToCart(input: AddConfigurableProductsToCartInput): AddConfigurableProductsToCartOutput
}

input AddConfigurableProductsToCartInput {
cart_id: String!
cartItems: [ConfigurableProductCartItemInput!]!
}

input ConfigurableProductCartItemInput {
sku: String!
quantity: Float!
configurable_options:[ConfigurableOptionInput!]!
customizable_options:[CustomizableOptionInput!]
}
Copy link

Choose a reason for hiding this comment

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

Can't configurable_options be replaced by a single simple_sku: Sku! field?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we go with simple_sku, then the client on its end will have to map selected options combination to the SKU:

  • Get a map during product request (option1, option2, optionN => simple SKU, ...)
  • Calculate current simple SKU based on selected options
  • Send simple SKU along with add product to cart request

Looks like more work for client app developers. I am probably missing something, please clarify why simple_sku is preferable.

Thanks for the review!

Copy link

Choose a reason for hiding this comment

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

I think there already is some form of simple_sku mapping in the frontend, especially if a frontend developer wishes to fetch images from the simple product etc. Or are there other strategies in that regard?

Copy link
Contributor

Choose a reason for hiding this comment

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

@paales Can you help by providing the motivation for using simple_sku in lieu of sending along options?

AFAICT, it seems like, in any situation you'd be adding an item to a cart, you'd always have a list of the options (since the shopper needs to select them). So it feels like needing to track a simple_sku on the client-side might be work that is unnecessary. Granted, it's very little extra work, but I'm curious what the motivation would be for that > options list.

Copy link

Choose a reason for hiding this comment

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

In the past I've always found it much easier to supply a sku rather than a list of options that map to a simple product sku. I agree with @paales that usually the sku is already known on the frontend.


input ConfigurableOptionInput {
id: Int!
value: Int!
}

type AddConfigurableProductsToCartOutput {
cart: Cart!
}

type ConfigurableCartItem implements CartItemInterface {
customizable_options: [SelectedCustomizableOption]!
configurable_options: [SelectedConfigurableOption!]!
}

type SelectedConfigurableOption {
id: Int!
option_label: String!
value_id: Int!
value_label: String!
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
type Mutation {
addDownloadableProductsToCart(input: AddDownloadableProductsToCartInput): AddDownloadableProductsToCartOutput
}

input AddDownloadableProductsToCartInput {
cart_id: String!
cartItems: [DownloadableProductCartItemInput!]!
}

input DownloadableProductCartItemInput {
sku: String!
quantity: Int!
downloadable_links: [DownloadableLinksInput!]
customizable_options:[CustomizableOptionInput!]
}

input DownloadableLinksInput {
id: [Int!]!
}

type AddDownloadableProductsToCartOutput {
cart: Cart!
}

type DownloadableCartItem implements CartItemInterface {
links_label: String!
links: [DownloadableCartItemLink!]!
configurable_options: [SelectedConfigurableOption!]!
}

type DownloadableCartItemLink {
id: Int!
label: String!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type Mutation {
addGiftCardProductsToCart(input: AddGiftCardProductsToCartInput): AddGiftCardProductsToCartOutput
}

input AddGiftCardProductsToCartInput {
cart_id: String!
cartItems: [GiftCardProductCartItemInput!]!
}

input GiftCardProductCartItemInput {
sku: String!
quantity: Float!
sender_name: String!
recepient_name: String!
amount: Money!
message: String
customizable_options:[CustomizableOptionInput!]
}

type AddGiftCardProductsToCartOutput {
cart: Cart!
}

type GiftCardCartItem implements CartItemInterface {
sender_name: String!
recepient_name: String!
amount: Money!
message: String
customizable_options: [SelectedCustomizableOption]!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type Mutation {
addGroupedProductsToCart(input: AddGroupedProductsToCartInput): AddGroupedProductsToCartOutput
}

input AddGroupedProductsToCartInput {
cart_id: String!
cartItems: [GroupedProductCartItemInput!]!
}

input GroupedProductCartItemInput {
sku: String!
quantity: Float!
# the difference from simple products is that grouped products do not support customizable options
}

type AddGroupedProductsToCartOutput {
cart: Cart!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
type Mutation {
addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput
}

input AddSimpleProductsToCartInput {
cart_id: String!
cartItems: [SimpleProductCartItemInput!]!
}

input SimpleProductCartItemInput {
sku: String!
quantity: Float!
customizable_options:[CustomizableOptionInput!]
}

input CustomizableOptionInput {
id: Int!
value: String!
}

type AddSimpleProductsToCartOutput {
cart: Cart!
}

type Cart {
id: String
items: [CartItemInterface]
}
Copy link

Choose a reason for hiding this comment

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

Missing the cart totals, currency, updated dates and all the other information. I know that this is not in the scope but my suggestion is to add all these data to cart definition ASAP to make things clear. Btw how can a client be sure that the cart he is currently updating is the last one? The use case here is when the same cart is updated by the end user and also by a back office.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The cart is updated by ID and there is only one active cart per customer at a time. If there are conflicting writes to the same cart (from store front and from the admin), then the last one wins. This should not happen in normal use cases.


interface CartItemInterface @typeResolver(class: "Magento\\CatalogCheckoutGraphQl\\Model\\CartItemInterfaceTypeResolverComposite") {
id: Int!
qty: Float!
product: ProductInterface!
prices: CartItemPrices!
Copy link

Choose a reason for hiding this comment

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

How/where are the calculated cartItem prices like: prices with(out) taxes? Or the applied discounts?

}

type CartItemPrices {
price: Money!
subtotal: Money!
}

type SimpleCartItem implements CartItemInterface {
customizable_options: [SelectedCustomizableOption]
}

type SelectedCustomizableOption {
id: Int!
label: String!
type: String!
Copy link

@naydav naydav Sep 26, 2018

Choose a reason for hiding this comment

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

Do we need to add Required flag value?

values: [SelectedCustomizableOptionValue!]!
sort_order: Int!
}

type SelectedCustomizableOptionValue {
id: Int
label: String!
Copy link

Choose a reason for hiding this comment

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

What is label for SelectedCustomizableOptionValue ?
Is the value entered / selected by the user?

price: CartItemSelectedOptionValuePrice!
Copy link

@naydav naydav Sep 26, 2018

Choose a reason for hiding this comment

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

Do we need price info?

Maybe id and value will be enough for the response about selected options

{
   "id": 14,
   "value": "test",                    
 }

sort_order: Int!
}

type CartItemSelectedOptionValuePrice {
value: Float!
units: String!
type: PriceTypeEnum!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
type Mutation {
# for now this mutation is identical to addSimpleProductsToCart and exists as a syntax sugar. Also it allows product type based customizations
addVirtualProductsToCart(input: AddVirtualProductsToCartInput): AddVirtualProductsToCartOutput
}

input AddVirtualProductsToCartInput {
cart_id: String!
cartItems: [VirtualProductCartItemInput!]!
}

input VirtualProductCartItemInput {
sku: String!
quantity: Float!
customizable_options:[CustomizableOptionInput!]
}

type AddVirtualProductsToCartOutput {
cart: Cart!
}

# Custom cart item type can be used to customize rendering when there are no physical producs available, e.g. skip shipping
type VirtualCartItem implements CartItemInterface {
customizable_options: [SelectedCustomizableOption]
}