-
Notifications
You must be signed in to change notification settings - Fork 152
[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
Changes from 5 commits
d8e9846
30ef22f
dc06523
d638fa9
902e999
43829b3
7d71efc
83aa880
c6f7d84
47b9607
bdd70b4
c5aaccb
c87159c
f2ca044
10ce77e
5cc770f
b0bc80e
fa08ab1
5a831aa
a940475
299809a
9635092
f0e011a
ca3a063
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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. | ||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By default for cart mutations, or for all mutations? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @akaplya suggested for all mutations. Still under discussion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For a server less architecture as we have in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!] | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't configurable_options be replaced by a single There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we go with
Looks like more work for client app developers. I am probably missing something, please clarify why Thanks for the review! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @paales Can you help by providing the motivation for using 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to add |
||
values: [SelectedCustomizableOptionValue!]! | ||
sort_order: Int! | ||
} | ||
|
||
type SelectedCustomizableOptionValue { | ||
id: Int | ||
label: String! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is |
||
price: CartItemSelectedOptionValuePrice! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need price info? Maybe
|
||
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] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about:
Thoughts?