@faststore/api 1.8.22 → 1.8.23

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.23",
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": "a8958ead23ee3df81fbb78d35f06726e7b290874"
49
49
  }
@@ -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,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')