@faststore/api 1.8.22 → 1.8.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/api",
3
- "version": "1.8.22",
3
+ "version": "1.8.25",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -45,5 +45,5 @@
45
45
  "peerDependencies": {
46
46
  "graphql": "^15.6.0"
47
47
  },
48
- "gitHead": "8feb71286b28d2308d4b11b37dbb62c2d5c76838"
48
+ "gitHead": "da2e248b5446eea91fe96e96e6f7028d2ab79c45"
49
49
  }
@@ -11,7 +11,7 @@ export type Scalars = {
11
11
  Float: number;
12
12
  };
13
13
 
14
- /** Shopping cart identification input. */
14
+ /** Shopping cart input. */
15
15
  export type IStoreCart = {
16
16
  /** Order information, including `orderNumber` and `acceptedOffer`. */
17
17
  order: IStoreOrder;
@@ -43,7 +43,7 @@ export type IStoreOffer = {
43
43
  export type IStoreOrder = {
44
44
  /** Array with information on each accepted offer. */
45
45
  acceptedOffer: Array<IStoreOffer>;
46
- /** Order shopping cart ID, also known as `orderFormId`. */
46
+ /** ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#). */
47
47
  orderNumber: Scalars['String'];
48
48
  };
49
49
 
@@ -53,13 +53,13 @@ export type IStoreOrganization = {
53
53
  identifier: Scalars['String'];
54
54
  };
55
55
 
56
- /** Product input. */
56
+ /** Product input. Products are variants within product groups, equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on. */
57
57
  export type IStoreProduct = {
58
58
  /** Array of product images. */
59
59
  image: Array<IStoreImage>;
60
60
  /** Product name. */
61
61
  name: Scalars['String'];
62
- /** Stock Keeping Unit ID. */
62
+ /** Stock Keeping Unit. Merchant-specific ID for the product. */
63
63
  sku: Scalars['String'];
64
64
  };
65
65
 
@@ -350,7 +350,7 @@ export type StoreOrder = {
350
350
  __typename?: 'StoreOrder';
351
351
  /** Array with information on each accepted offer. */
352
352
  acceptedOffer: Array<StoreOffer>;
353
- /** Order shopping cart ID, also known as `orderFormId`. */
353
+ /** ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#). */
354
354
  orderNumber: Scalars['String'];
355
355
  };
356
356
 
@@ -389,7 +389,7 @@ export type StorePerson = {
389
389
  id: Scalars['String'];
390
390
  };
391
391
 
392
- /** Product information. */
392
+ /** Product information. Products are variants within product groups, equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on. */
393
393
  export type StoreProduct = {
394
394
  __typename?: 'StoreProduct';
395
395
  /** Array of additional properties. */
@@ -412,13 +412,13 @@ export type StoreProduct = {
412
412
  name: Scalars['String'];
413
413
  /** Aggregate offer information. */
414
414
  offers: StoreAggregateOffer;
415
- /** Product ID. */
415
+ /** Product ID, such as [ISBN](https://www.isbn-international.org/content/what-isbn) or similar global IDs. */
416
416
  productID: Scalars['String'];
417
417
  /** Array with review information. */
418
418
  review: Array<StoreReview>;
419
419
  /** Meta tag data. */
420
420
  seo: StoreSeo;
421
- /** Stock Keeping Unit ID. */
421
+ /** Stock Keeping Unit. Merchant-specific ID for the product. */
422
422
  sku: Scalars['String'];
423
423
  /** Corresponding collection URL slug, with which to retrieve this entity. */
424
424
  slug: Scalars['String'];
@@ -442,12 +442,12 @@ export type StoreProductEdge = {
442
442
  node: StoreProduct;
443
443
  };
444
444
 
445
- /** Product group information. */
445
+ /** Product group information. Product groups are catalog entities that may contain variants. They are equivalent to VTEX [Products](https://help.vtex.com/en/tutorial/what-is-a-product--2zrB2gFCHyQokCKKE8kuAw#), whereas each variant is equivalent to a VTEX [SKU](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on. */
446
446
  export type StoreProductGroup = {
447
447
  __typename?: 'StoreProductGroup';
448
448
  /** Array of additional properties. */
449
449
  additionalProperty: Array<StorePropertyValue>;
450
- /** Array of variants related to product group. */
450
+ /** Array of variants related to product group. Variants are equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). */
451
451
  hasVariant: Array<StoreProduct>;
452
452
  /** Product group name. */
453
453
  name: Scalars['String'];
@@ -103,6 +103,26 @@ export const VtexCommerce = (
103
103
  }
104
104
  )
105
105
  },
106
+ setCustomData: ({
107
+ id,
108
+ appId,
109
+ key,
110
+ value,
111
+ }: {
112
+ id: string
113
+ appId: string
114
+ key: string
115
+ value: string
116
+ }): Promise<OrderForm> => {
117
+ return fetchAPI(
118
+ `${base}/api/checkout/pub/orderForm/${id}/customData/${appId}/${key}`,
119
+ {
120
+ ...BASE_INIT,
121
+ body: JSON.stringify({ value }),
122
+ method: 'PUT',
123
+ }
124
+ )
125
+ },
106
126
  region: async ({
107
127
  postalCode,
108
128
  country,
@@ -136,7 +136,7 @@ export interface OrderForm {
136
136
  giftRegistryData: any | null
137
137
  openTextField: any | null
138
138
  invoiceData: any | null
139
- customData: any | null
139
+ customData: OrderFormCustomData | null
140
140
  itemMetadata: {
141
141
  items: MetadataItem[]
142
142
  }
@@ -149,6 +149,14 @@ export interface OrderForm {
149
149
  itemsOrdination: any | null
150
150
  }
151
151
 
152
+ export interface OrderFormCustomData {
153
+ customApps: Array<{
154
+ fields: Record<string, string>
155
+ id: string
156
+ major: number
157
+ }>
158
+ }
159
+
152
160
  export interface OrderFormMarketingData {
153
161
  utmCampaign?: string
154
162
  utmMedium?: string
@@ -25,6 +25,11 @@ export interface Options {
25
25
  // Default sales channel to use for fetching products
26
26
  channel: string
27
27
  hideUnavailableItems: boolean
28
+ flags?: FeatureFlags
29
+ }
30
+
31
+ interface FeatureFlags {
32
+ enableOrderFormSync?: boolean
28
33
  }
29
34
 
30
35
  export interface Context {
@@ -38,6 +43,7 @@ export interface Context {
38
43
  * */
39
44
  storage: {
40
45
  channel: Required<Channel>
46
+ flags: FeatureFlags
41
47
  }
42
48
  headers: Record<string, string>
43
49
  }
@@ -68,6 +74,7 @@ const Resolvers = {
68
74
  export const getContextFactory = (options: Options) => (ctx: any): Context => {
69
75
  ctx.storage = {
70
76
  channel: ChannelMarshal.parse(options.channel),
77
+ flags: options.flags ?? {},
71
78
  }
72
79
  ctx.clients = getClients(options, ctx)
73
80
  ctx.loaders = getLoaders(options, ctx)
@@ -1,5 +1,6 @@
1
1
  import { enhanceCommercialOffer } from '../utils/enhanceCommercialOffer'
2
2
  import { bestOfferFirst } from '../utils/productStock'
3
+ import { slugify } from '../utils/slugify'
3
4
  import type { EnhancedCommercialOffer } from '../utils/enhanceCommercialOffer'
4
5
  import type { Resolver } from '..'
5
6
  import type { PromiseType } from '../../../typings'
@@ -38,11 +39,13 @@ export const StoreProduct: Record<string, Resolver<Root>> & {
38
39
  return {
39
40
  itemListElement: [
40
41
  ...categories.reverse().map((categoryPath, index) => {
41
- const categoryNames = categoryPath.split('/')
42
+ const splitted = categoryPath.split('/')
43
+ const name = splitted[splitted.length - 2]
44
+ const item = splitted.map(slugify).join('/')
42
45
 
43
46
  return {
44
- name: categoryNames[categoryNames.length - 2],
45
- item: categoryPath.toLowerCase(),
47
+ name,
48
+ item,
46
49
  position: index + 1,
47
50
  }
48
51
  }),
@@ -1,14 +1,15 @@
1
1
  import deepEquals from 'fast-deep-equal'
2
2
 
3
+ import { md5 } from '../utils/md5'
3
4
  import type {
4
- IStoreOrder,
5
5
  IStoreCart,
6
6
  IStoreOffer,
7
+ IStoreOrder,
7
8
  } from '../../../__generated__/schema'
8
9
  import type {
9
10
  OrderForm,
10
- OrderFormItem,
11
11
  OrderFormInputItem,
12
+ OrderFormItem,
12
13
  } from '../clients/commerce/types/OrderForm'
13
14
  import type { Context } from '..'
14
15
 
@@ -69,6 +70,70 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
69
70
  return isSameOrder && orderItemsAreSync
70
71
  }
71
72
 
73
+ const orderFormToCart = (
74
+ form: OrderForm,
75
+ skuLoader: Context['loaders']['skuLoader']
76
+ ) => {
77
+ return {
78
+ order: {
79
+ orderNumber: form.orderFormId,
80
+ acceptedOffer: form.items.map((item) => ({
81
+ ...item,
82
+ product: skuLoader.load([{ key: 'id', value: item.id }]), // TODO: add channel
83
+ })),
84
+ },
85
+ messages: form.messages.map(({ text, status }) => ({
86
+ text,
87
+ status: status.toUpperCase(),
88
+ })),
89
+ }
90
+ }
91
+
92
+ const getOrderFormEtag = ({ items }: OrderForm) => md5(JSON.stringify(items))
93
+
94
+ const setOrderFormEtag = async (
95
+ form: OrderForm,
96
+ commerce: Context['clients']['commerce']
97
+ ) => {
98
+ try {
99
+ const orderForm = await commerce.checkout.setCustomData({
100
+ id: form.orderFormId,
101
+ appId: 'faststore',
102
+ key: 'cartEtag',
103
+ value: getOrderFormEtag(form),
104
+ })
105
+
106
+ return orderForm
107
+ } catch (err) {
108
+ console.error(
109
+ 'Error while setting custom data to orderForm.\n Make sure to add the following custom app to the orderForm: \n{"fields":["cartEtag"],"id":"faststore","major":1}.\n More info at: https://developers.vtex.com/vtex-rest-api/docs/customizable-fields-with-checkout-api'
110
+ )
111
+
112
+ throw err
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Checks if cartEtag stored on customData is up to date
118
+ * @description If cartEtag is not up to date, this means that
119
+ * another system changed the cart, like Checkout UI or Order Placed
120
+ */
121
+ const isOrderFormStale = (form: OrderForm) => {
122
+ const faststoreData = form.customData?.customApps.find(
123
+ (app) => app.id === 'faststore'
124
+ )
125
+
126
+ const oldEtag = faststoreData?.fields?.cartEtag
127
+
128
+ if (oldEtag == null) {
129
+ return true
130
+ }
131
+
132
+ const newEtag = getOrderFormEtag(form)
133
+
134
+ return newEtag !== oldEtag
135
+ }
136
+
72
137
  /**
73
138
  * This resolver implements the optimistic cart behavior. The main idea in here
74
139
  * is that we receive a cart from the UI (as query params) and we validate it with
@@ -87,6 +152,7 @@ export const validateCart = async (
87
152
  { cart: { order } }: { cart: IStoreCart },
88
153
  ctx: Context
89
154
  ) => {
155
+ const { enableOrderFormSync } = ctx.storage.flags
90
156
  const { orderNumber, acceptedOffer } = order
91
157
  const {
92
158
  clients: { commerce },
@@ -98,6 +164,19 @@ export const validateCart = async (
98
164
  id: orderNumber,
99
165
  })
100
166
 
167
+ // Step1.5: Check if another system changed the orderForm with this orderNumber
168
+ // If so, this means the user interacted with this cart elsewhere and expects
169
+ // to see this new cart state instead of what's stored on the user's browser.
170
+ if (enableOrderFormSync === true) {
171
+ const isStale = isOrderFormStale(orderForm)
172
+
173
+ if (isStale === true && orderNumber) {
174
+ const newOrderForm = await setOrderFormEtag(orderForm, commerce)
175
+
176
+ return orderFormToCart(newOrderForm, skuLoader)
177
+ }
178
+ }
179
+
101
180
  // Step2: Process items from both browser and checkout so they have the same shape
102
181
  const browserItemsById = groupById(acceptedOffer)
103
182
  const originItemsById = groupById(orderForm.items.map(orderFormItemToOffer))
@@ -139,28 +218,22 @@ export const validateCart = async (
139
218
  }
140
219
 
141
220
  // Step4: Apply delta changes to order form
142
- const updatedOrderForm = await commerce.checkout.updateOrderFormItems({
143
- id: orderForm.orderFormId,
144
- orderItems: changes,
145
- })
221
+ const updatedOrderForm = await commerce.checkout
222
+ // update orderForm items
223
+ .updateOrderFormItems({
224
+ id: orderForm.orderFormId,
225
+ orderItems: changes,
226
+ })
227
+ // update orderForm etag so we know last time we touched this orderForm
228
+ .then((form) =>
229
+ enableOrderFormSync ? setOrderFormEtag(form, commerce) : form
230
+ )
146
231
 
147
232
  // Step5: If no changes detected before/after updating orderForm, the order is validated
148
233
  if (equals(order, updatedOrderForm)) {
149
234
  return null
150
235
  }
151
236
 
152
- // Step6: There were changes, convert orderForm to StoreOrder
153
- return {
154
- order: {
155
- orderNumber: updatedOrderForm.orderFormId,
156
- acceptedOffer: updatedOrderForm.items.map((item) => ({
157
- ...item,
158
- product: skuLoader.load([{ key: 'id', value: item.id }]), // TODO: add channel
159
- })),
160
- },
161
- messages: updatedOrderForm.messages.map(({ text, status }) => ({
162
- text,
163
- status: status.toUpperCase(),
164
- })),
165
- }
237
+ // Step6: There were changes, convert orderForm to StoreCart
238
+ return orderFormToCart(updatedOrderForm, skuLoader)
166
239
  }
@@ -0,0 +1,4 @@
1
+ import crypto from 'crypto'
2
+
3
+ export const md5 = (payload: string) =>
4
+ crypto.createHash('md5').update(payload).digest('hex')
@@ -27,7 +27,7 @@ type StoreCart {
27
27
  }
28
28
 
29
29
  """
30
- Shopping cart identification input.
30
+ Shopping cart input.
31
31
  """
32
32
  input IStoreCart {
33
33
  """
@@ -3,7 +3,7 @@ Information of a specific order.
3
3
  """
4
4
  type StoreOrder {
5
5
  """
6
- Order shopping cart ID, also known as `orderFormId`.
6
+ ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#).
7
7
  """
8
8
  orderNumber: String!
9
9
  """
@@ -17,7 +17,7 @@ Offer input.
17
17
  """
18
18
  input IStoreOrder {
19
19
  """
20
- Order shopping cart ID, also known as `orderFormId`.
20
+ ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#).
21
21
  """
22
22
  orderNumber: String!
23
23
  """
@@ -1,5 +1,5 @@
1
1
  """
2
- Product information.
2
+ Product information. Products are variants within product groups, equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on.
3
3
  """
4
4
  type StoreProduct {
5
5
  """
@@ -19,7 +19,7 @@ type StoreProduct {
19
19
  """
20
20
  name: String!
21
21
  """
22
- Product ID.
22
+ Product ID, such as [ISBN](https://www.isbn-international.org/content/what-isbn) or similar global IDs.
23
23
  """
24
24
  productID: String!
25
25
  """
@@ -39,7 +39,7 @@ type StoreProduct {
39
39
  """
40
40
  offers: StoreAggregateOffer!
41
41
  """
42
- Stock Keeping Unit ID.
42
+ Stock Keeping Unit. Merchant-specific ID for the product.
43
43
  """
44
44
  sku: String!
45
45
  """
@@ -65,11 +65,11 @@ type StoreProduct {
65
65
  }
66
66
 
67
67
  """
68
- Product input.
68
+ Product input. Products are variants within product groups, equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on.
69
69
  """
70
70
  input IStoreProduct {
71
71
  """
72
- Stock Keeping Unit ID.
72
+ Stock Keeping Unit. Merchant-specific ID for the product.
73
73
  """
74
74
  sku: String!
75
75
  """
@@ -1,9 +1,9 @@
1
1
  """
2
- Product group information.
2
+ Product group information. Product groups are catalog entities that may contain variants. They are equivalent to VTEX [Products](https://help.vtex.com/en/tutorial/what-is-a-product--2zrB2gFCHyQokCKKE8kuAw#), whereas each variant is equivalent to a VTEX [SKU](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#). For example, you may have a **Shirt** product group with associated products such as **Blue shirt size L**, **Green shirt size XL** and so on.
3
3
  """
4
4
  type StoreProductGroup {
5
5
  """
6
- Array of variants related to product group.
6
+ Array of variants related to product group. Variants are equivalent to VTEX [SKUs](https://help.vtex.com/en/tutorial/what-is-an-sku--1K75s4RXAQyOuGUYKMM68u#).
7
7
  """
8
8
  hasVariant: [StoreProduct!]!
9
9
  """