-
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
[ongoing discussion] Draft for cart operations and other GraphQL improvements #15
Conversation
quantity: Float! | ||
configurable_options:[ConfigurableOptionInput!]! | ||
customizable_options:[CustomizableOptionInput!] | ||
} |
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.
Can't configurable_options be replaced by a single simple_sku: Sku!
field?
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.
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!
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.
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 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.
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.
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.
@@ -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. |
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.
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.
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.
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:
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?
- 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 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.
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.
Asynchronous server side implementation is not approved yet. The schema may change, if it will be approved.
|
||
**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 comment
The 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 comment
The 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 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.
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.
The decision is to have Synchronous implementation by default. Asynchronous implementation may be added later on the framework level as it was for REST.
- 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 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.
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.
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.
qty: Float! | ||
product: CartItemProduct! | ||
image: CartItemImage! | ||
prices: CartItemPrices! |
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/where are the calculated cartItem prices like: prices with(out) taxes? Or the applied discounts?
name: String! | ||
type: String! | ||
# Does it make sense to provide several basic fields (with potentially better performance) in addition to the ful product object? | ||
details: ProductInterface! |
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 should depend on the client so I am in favour of adding it to the cartItem. I guess that most clients will just query for some fields and not for the entire ProductInterface
(not sure if it matters for performance).
|
||
type CartItemImage { | ||
alt_text: String! | ||
thumbnail: ProductMediaGalleryEntriesContent! |
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.
Probably out of scope the current PR scope but would be really nice to have the absolute url here.
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.
I think URL is already part of ProductInterface
, what if we remove cart item image and just leave reference to ProductInterface
?
type Cart { | ||
id: String | ||
items: [CartItemInterface] | ||
} |
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.
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
.
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.
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.
|
||
**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 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.
… using ProductInterface
code: String! | ||
} | ||
|
||
input CancelCouponForCartInput { |
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.
suggest "remove coupon from cart", as in "RemoveCouponFromCartInput", "RemoveCouponFromCartOutput"
} | ||
|
||
type CartAddress { | ||
pices: CartAddressTotals |
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.
Is this the CartAddressPrices? Also, note the small typo, the field name should be prices
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.
Why plural?
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.
I think plural makes sense because there are multiple price types stored in this field.
Do you strongly believe it should be singular in this case?
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.
Not really, but there's this small inconsistency where you have a plural to designate lists but here is a single object. Why don't name it totals?
@@ -0,0 +1,57 @@ | |||
interface CartItemInterface { | |||
prices: CartItemTotals |
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.
Where is the CartItemTotals defined?
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.
Good catch, I called the type CartItemTotals
first and then renamed it to CartItemPrices
, forgot to change here.
} | ||
|
||
type Cart { | ||
prices: CartTotals |
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.
Where is the CartTotals defined?
suffix: String | ||
gender: GenderEnum | ||
date_of_birth: String | ||
vat_number: String # Do we need it at all on storefront? Do we need more details |
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.
The presence of a vat_number can influence the rendering in many ways, so I'd say yes.
- Improved extensibility for cart item mutations - Added cart item removal and update schemas
postcode: String | ||
country_code: String! | ||
telephone: String! | ||
fax: String |
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.
do we really need fax anymore? I would get rid of it if we can do it without breaking things
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.
Done
} | ||
|
||
input AddShippingAddressToCartInput { | ||
address_id: Int # Can be provided in one-page checkout and is required for multi-shipping checkout |
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.
Have we accounted for multishipping scenarios here? I don't yet see how we tie cart items with shipping addresses, in case there are multiple
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.
Done
addShippingAddressToCart(input: AddShippingAddressToCartInput): AddShippingAddressToCartOutput | ||
} | ||
|
||
input AddBillingAddressToCartInput { |
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.
Suggestion: rename to setBillingAddressOnCart (to show that there can only be one)
input AddShippingAddressToCartInput { | ||
address_id: Int # Can be provided in one-page checkout and is required for multi-shipping checkout | ||
address: CartAddressInput | ||
shipping_method_code: String # Should be on address level for multi-shipping checkout |
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.
Needs to be a separate mutation
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.
Done
} | ||
|
||
input CartAddressInput { | ||
firstname: String! |
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 are we going to add customer address attributes?
} | ||
|
||
type CartAddress { | ||
firstname: String! |
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 are we going to set information about customer address attributes
BILLING | ||
} | ||
|
||
type CheckoutShippingMethod { |
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.
I think we also need to add information about the error message, it can be displayed if in the system configuration we choose carriers/dhl/showmethod = 1
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.
case code/label become optional if error message is displayed (setting "show method if not applicable")
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.
Done
|
||
type CheckoutShippingMethod { | ||
code: String! | ||
label: String! |
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.
Also, online shipping methods can have different rates, how are we going to display this information?
type CheckoutPaymentMethod { | ||
code: String! | ||
label: String! | ||
# Do we need more info from quote_payment DB table? |
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.
need to add "amount" or "balance" for things like store credit, gift card, reward pts
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.
Done
type CheckoutPaymentMethod { | ||
code: String! | ||
label: String! | ||
# Do we need more info from quote_payment DB table? |
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.
Add sort order
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.
Done
type CheckoutPaymentMethod { | ||
code: String! | ||
label: String! | ||
# Do we need more info from quote_payment DB table? |
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.
handle payment method-specific field such as "instructions", "payable to", "send check to", etc
type CheckoutPaymentMethod { | ||
code: String! | ||
label: String! | ||
# Do we need more info from quote_payment DB table? |
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.
consider "terms and conditions" and "billing agreements"
type CheckoutPaymentMethod { | ||
code: String! | ||
label: String! | ||
# Do we need more info from quote_payment DB table? |
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.
consider gift messaging, gift wrapping, gift receipt
@@ -0,0 +1,58 @@ | |||
type Query { |
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 add information
- about shipping address if we add an item from gift registry;
- how can we set gift messages, gift wrapping, printed cart information;
- how can we set checkout agreements
<extension_attributes for="Magento\Quote\Api\Data\PaymentInterface"> <!-- This is needed to provide type hint to serializer --> <attribute code="agreement_ids" type="string[]" /> </extension_attributes>
- Also, if graphQL be compatible with B2B edition
is_virtual: Boolean! | ||
} | ||
|
||
type CheckoutCustomer { |
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.
It would be great to use polymorphism here.
interface CheckoutCustomer {
# the mandatory fields
}
type GuestCheckoutCustomer implements CheckoutCustomer {
# …
middle_name: String
}
type LoggedInCheckoutCustomer implements CheckoutCustomer {
# …
middle_name: String!
}
React components could then require different fragments depending on the type
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.
Good idea in general. I don't understand though, why middle_name
should be required for the registered customer. The only difference I see right now is that we can eliminate is_guest
field in this way.
Please clarify which exact fields should be different between guest and customer implementations.
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.
I agree that the middle_name
is not a good example. I meant user information mandatory upon registration, such as email
for instance.
The idea of having such a structure, is to open rooms for extension in the shop. For instance in a B2B context, a LoggedInCheckoutCustomer
may have a sales_representative: SalesPerson!
attached which would not be available for guests.
Developers would then only extend the LoggedInCheckoutCustomer
type, making implementation more explicit.
middle_name: String | ||
suffix: String | ||
gender: GenderEnum | ||
date_of_birth: String |
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.
Are there any plans for a scalar Date
type in the core?
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.
We do not have plans for that, but I know that this topic was discussed in scope of Webonyx project: webonyx/graphql-php#228
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.
Ok, thank you.
code: String! | ||
label: String! | ||
balance: Money | ||
sort_order: Int |
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.
Is there a use case for exposing this value (vs having sort params in queries returning this type)?
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.
We thought of a use case when the client app needs to honor sort order of available payment methods during rendering.
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.
I think this pattern is better to be delegated to the server logic. By default available_payment_methods
would return sorted values.
If a use case where methods should be sorted differently, it could lead to a different signature with an optional param:
available_payment_methods(sortDesc: Boolean)
} | ||
|
||
input UpdateBundleProductCartItemInput { | ||
details: UpdateCartItemDetailsInput! |
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.
Maybe it will be better to use data
instead of details
data: CartItemInput!
|
||
type SelectedCustomizableOptionValue { | ||
id: Int | ||
label: String! |
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.
What is label
for SelectedCustomizableOptionValue
?
Is the value entered / selected by the user?
type SelectedCustomizableOption { | ||
id: Int! | ||
label: String! | ||
type: String! |
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.
Do we need to add Required
flag value?
type SelectedCustomizableOptionValue { | ||
id: Int | ||
label: String! | ||
price: CartItemSelectedOptionValuePrice! |
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.
Do we need price info?
Maybe id
and value
will be enough for the response about selected options
{
"id": 14,
"value": "test",
}
} | ||
|
||
input UpdateSimpleProductCartItemInput { | ||
details: UpdateCartItemDetailsInput! |
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.
Why do we need one more level like UpdateCartItemDetailsInput
Now request is
cartItems: [
{
data: {
sku: "product_dynamic_24494",
qty: 1
},
customizable_options: [
{id: 5, value: "test"},
]
}
]
Can we use next?
cartItems: [
{
sku: "product_dynamic_24494",
qty: 1,
customizable_options: [
{id: 5, value: "test"},
]
}
]
``
Merged the initial document. |
For the context see recording of previous GraphQL community meeting.
GraphQL project wiki: https://github.com/magento/graphql-ce/wiki