Skip to content
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

Merged
merged 24 commits into from
Oct 19, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
59 changes: 59 additions & 0 deletions design-documents/graph-ql/coverage/Cart.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
type Query {
Copy link

@irenelagno irenelagno Sep 19, 2018

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

cart(input: CartQueryInput): CartQueryOutput
}

input CartQueryInput {
cart_id: String!
}

type CartQueryOutput {
cart: Cart
}

type Cart {
id: String!

line_items_count: Int!
items_quantity: Float!

selected_payment_method: CheckoutPaymentMethod
available_payment_methods: [CheckoutPaymentMethod]!

customer: CheckoutCustomer
customer_notes: String

gift_cards_amount_used: Money
applied_gift_cards: [CartGiftCard]

is_multishipping: Boolean!
is_virtual: Boolean!
}

type CheckoutCustomer {
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

@real34 real34 Sep 21, 2018

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.

is_guest: Boolean!
email: String!
prefix: String
first_name: String!
last_name: String!
middle_name: String
suffix: String
gender: GenderEnum
date_of_birth: String
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

Choose a reason for hiding this comment

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

Ok, thank you.

vat_number: String # Do we need it at all on storefront? Do we need more details
Copy link

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.

}

enum GenderEnum {
MALE
FEMALE
}

type CheckoutPaymentMethod {
code: String!
label: String!
balance: Money
sort_order: Int
Copy link
Member

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)?

Copy link
Contributor Author

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.

Copy link
Member

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)

}

type CartGiftCard {
code: String!
}
110 changes: 110 additions & 0 deletions design-documents/graph-ql/coverage/CartAddressOperations.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
type Query {
setBillingAddressOnCart(input: SetBillingAddressOnCartInput): SetBillingAddressOnCartOutput
setShippingAddressesOnCart(input: SetShippingAddressesOnCartInput): SetShippingAddressesOnCartOutput
setShippingMethodsOnCart(input: SetShippingMethodsOnCartInput): SetShippingMethodsOnCartOutput
}

input SetShippingMethodsOnCartInput {
shipping_methods: [ShippingMethodForAddressInput!]!
}

input ShippingMethodForAddressInput {
cart_address_id: String!
shipping_method_code: String!
}

input SetBillingAddressOnCartInput {
customer_address_id: Int
address: CartAddressInput
}

input SetShippingAddressesOnCartInput {
customer_address_id: Int # Can be provided in one-page checkout and is required for multi-shipping checkout
address: CartAddressInput
cart_items: [CartItemQuantityInput!]
}

input CartItemQuantityInput {
cart_item_id: String!
quantity: Float!
}

input CartAddressInput {
firstname: String!

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?

lastname: String!
company: String
street: [String!]!
city: String!
region: String
postcode: String
country_code: String!
telephone: String!
save_in_address_book: Boolean!
}

type SetShippingAddressesOnCartOutput {
cart: Cart!
}

type SetShippingMethodsOnCartOutput {
cart: Cart!
}

type SetBillingAddressOnCartOutput {
cart: Cart!
}

type Cart {
addresses: [CartAddress]!
}

type CartAddress {
firstname: String!

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

lastname: String!
company: String
street: [String!]!
city: String!
region: CartAddressRegion
postcode: String
country: CartAddressCountry!
telephone: String!
address_type: AdressTypeEnum!

selected_shipping_method: CheckoutShippingMethod
available_shipping_methods: [CheckoutShippingMethod]!

items_weight: Float
customer_notes: String
gift_cards_amount_used: Money
applied_gift_cards: [CartGiftCard]

cart_items: [CartItemQuantity]
}

type CartItemQuantity {
cart_item_id: String!
quantity: Float!
}

type CartAddressCountry {
code: String
label: String
}

type CartAddressRegion {
code: String
label: String
}

enum AdressTypeEnum {
SHIPPING
BILLING
}

type CheckoutShippingMethod {

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

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")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

code: String
label: String
free_shipping: Boolean!
error_message: String
# TODO: Add more complex structure for shipping rates
}
55 changes: 55 additions & 0 deletions design-documents/graph-ql/coverage/CartPrices.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
interface CartItemInterface {
prices: CartItemPrices
}

type Cart {
prices: CartPrices
}

type CartAddress {
prices: CartAddressPrices
# additional fields will be added later
}

interface CartPricesInterface {
grand_total: Money

# price display settings should be requested via store config query
subtotal_including_tax: Money
subtotal_excluding_tax: Money

subtotal_with_discount_excluding_tax: Money
discount_tax_compensation: Money #Should we have subtotal with discount including tax instead, is it different from grand_total?

applied_taxes: [CartTaxItem]! # Should include regular taxes and WEEE taxes
applied_discounts: [CartDiscountItem]!
}

type CartItemPrices implements CartPricesInterface {
price_including_tax: Money
price_excluding_tax: Money

custom_price: Money
}

type CartAddressPrices implements CartPricesInterface {

shipping_including_tax: Money
shipping_excluding_tax: Money

shipping_discount: Money # Do we need shipping_with_discount_including_tax?
shipping_discount_tax_compensation: Money
}

type CartPrices implements CartPricesInterface {
}

type CartTaxItem {
amount: Money!
label: String!
}

type CartDiscountItem {
amount: Money!
label: String!
}
34 changes: 34 additions & 0 deletions design-documents/graph-ql/coverage/CouponOperations.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 CartAddress {
applied_coupon: AppliedCoupon
}

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

input RemoveCouponFromCartInput {
cart_id: String!
}

type RemoveCouponFromCartOutput {
cart: Cart
}
45 changes: 45 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,45 @@
**Overview**

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.

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.

- Server-side asynchronous mutations can be supported in the future on framework level in a way similar to Async 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,59 @@
type Mutation {
addBundleProductsToCart(input: AddBundleProductsToCartInput): AddBundleProductsToCartOutput
updateBundleProductsInCart(input: UpdateBundleProductsInCartInput): UpdateBundleProductsInCartOutput
}

input UpdateBundleProductsInCartInput {
cart_id: String!
cartItems: [UpdateBundleProductCartItemInput!]!
}

input UpdateBundleProductCartItemInput {
details: UpdateCartItemDetailsInput!
Copy link

@naydav naydav Sep 25, 2018

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!

bundle_options:[BundleOptionInput!]
customizable_options:[CustomizableOptionInput]
}

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

input BundleProductCartItemInput {
details: CartItemDetailsInput!
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!
}

Loading