@faststore/api 1.10.34 → 1.11.3

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.10.34",
3
+ "version": "1.11.3",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -46,5 +46,5 @@
46
46
  "peerDependencies": {
47
47
  "graphql": "^15.6.0"
48
48
  },
49
- "gitHead": "5d59e28c724e389b596e2bb13b1464314eab275f"
49
+ "gitHead": "3e3bfc6bd9aa68ffc170b9979d5e3eeb5e06088e"
50
50
  }
@@ -1,6 +1,12 @@
1
1
  import deepEquals from 'fast-deep-equal'
2
2
 
3
3
  import { md5 } from '../utils/md5'
4
+ import {
5
+ attachmentToPropertyValue,
6
+ getPropertyId,
7
+ VALUE_REFERENCES,
8
+ } from '../utils/propertyValue'
9
+
4
10
  import type {
5
11
  IStoreCart,
6
12
  IStoreOffer,
@@ -13,12 +19,6 @@ import type {
13
19
  OrderFormItem,
14
20
  } from '../clients/commerce/types/OrderForm'
15
21
  import type { Context } from '..'
16
- import {
17
- attachmentToPropertyValue,
18
- getPropertyId,
19
- VALUE_REFERENCES,
20
- } from '../utils/propertyValue'
21
-
22
22
  type Indexed<T> = T & { index?: number }
23
23
 
24
24
  const isAttachment = (value: IStorePropertyValue) =>
@@ -28,7 +28,7 @@ const getId = (item: IStoreOffer) =>
28
28
  [
29
29
  item.itemOffered.sku,
30
30
  item.seller.identifier,
31
- item.price,
31
+ item.price < 0.01 ? 'Gift' : undefined,
32
32
  item.itemOffered.additionalProperty
33
33
  ?.filter(isAttachment)
34
34
  .map(getPropertyId)
@@ -39,7 +39,7 @@ const getId = (item: IStoreOffer) =>
39
39
 
40
40
  const orderFormItemToOffer = (
41
41
  item: OrderFormItem,
42
- index?: number
42
+ index?: number,
43
43
  ): Indexed<IStoreOffer> => ({
44
44
  listPrice: item.listPrice / 100,
45
45
  price: item.sellingPrice / 100,
@@ -55,7 +55,7 @@ const orderFormItemToOffer = (
55
55
  })
56
56
 
57
57
  const offerToOrderItemInput = (
58
- offer: Indexed<IStoreOffer>
58
+ offer: Indexed<IStoreOffer>,
59
59
  ): OrderFormInputItem => ({
60
60
  quantity: offer.quantity,
61
61
  seller: offer.seller.identifier,
@@ -69,14 +69,18 @@ const offerToOrderItemInput = (
69
69
  })),
70
70
  })
71
71
 
72
- const groupById = (offers: IStoreOffer[]): Map<string, IStoreOffer> =>
72
+ const groupById = (offers: IStoreOffer[]): Map<string, IStoreOffer[]> =>
73
73
  offers.reduce((acc, item) => {
74
74
  const id = getId(item)
75
75
 
76
- acc.set(id, acc.get(id) ?? item)
76
+ if (!acc.has(id)) {
77
+ acc.set(id, [])
78
+ }
79
+
80
+ acc.get(id)?.push(item)
77
81
 
78
82
  return acc
79
- }, new Map<string, IStoreOffer>())
83
+ }, new Map<string, IStoreOffer[]>())
80
84
 
81
85
  const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
82
86
  const pick = (item: Indexed<IStoreOffer>, index: number) => ({
@@ -96,9 +100,42 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
96
100
  return isSameOrder && orderItemsAreSync
97
101
  }
98
102
 
103
+ const joinItems = (form: OrderForm) => {
104
+ const itemsById = form.items
105
+ .reduce((acc, item) => {
106
+ const id = getId(orderFormItemToOffer(item))
107
+
108
+ if (!acc[id]) {
109
+ acc[id] = []
110
+ }
111
+
112
+ acc[id].push(item)
113
+
114
+ return acc
115
+ }, {} as Record<string, OrderFormItem[]>)
116
+
117
+ return {
118
+ ...form,
119
+ items: Object.values(itemsById).map((items) => {
120
+ const [item] = items
121
+ const quantity = items.reduce((acc, i) => acc + i.quantity, 0)
122
+ const totalPrice = items.reduce(
123
+ (acc, i) => acc + i.quantity * i.sellingPrice,
124
+ 0,
125
+ )
126
+
127
+ return {
128
+ ...item,
129
+ quantity,
130
+ sellingPrice: totalPrice / quantity,
131
+ }
132
+ }),
133
+ }
134
+ }
135
+
99
136
  const orderFormToCart = async (
100
137
  form: OrderForm,
101
- skuLoader: Context['loaders']['skuLoader']
138
+ skuLoader: Context['loaders']['skuLoader'],
102
139
  ) => {
103
140
  return {
104
141
  order: {
@@ -119,7 +156,7 @@ const getOrderFormEtag = ({ items }: OrderForm) => md5(JSON.stringify(items))
119
156
 
120
157
  const setOrderFormEtag = async (
121
158
  form: OrderForm,
122
- commerce: Context['clients']['commerce']
159
+ commerce: Context['clients']['commerce'],
123
160
  ) => {
124
161
  try {
125
162
  const orderForm = await commerce.checkout.setCustomData({
@@ -132,7 +169,7 @@ const setOrderFormEtag = async (
132
169
  return orderForm
133
170
  } catch (err) {
134
171
  console.error(
135
- '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'
172
+ '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',
136
173
  )
137
174
 
138
175
  throw err
@@ -146,7 +183,7 @@ const setOrderFormEtag = async (
146
183
  */
147
184
  const isOrderFormStale = (form: OrderForm) => {
148
185
  const faststoreData = form.customData?.customApps.find(
149
- (app) => app.id === 'faststore'
186
+ (app) => app.id === 'faststore',
150
187
  )
151
188
 
152
189
  const oldEtag = faststoreData?.fields?.cartEtag
@@ -176,7 +213,7 @@ const isOrderFormStale = (form: OrderForm) => {
176
213
  export const validateCart = async (
177
214
  _: unknown,
178
215
  { cart: { order } }: { cart: IStoreCart },
179
- ctx: Context
216
+ ctx: Context,
180
217
  ) => {
181
218
  const { enableOrderFormSync } = ctx.storage.flags
182
219
  const { orderNumber, acceptedOffer } = order
@@ -197,7 +234,9 @@ export const validateCart = async (
197
234
  const isStale = isOrderFormStale(orderForm)
198
235
 
199
236
  if (isStale === true && orderNumber) {
200
- const newOrderForm = await setOrderFormEtag(orderForm, commerce)
237
+ const newOrderForm = await setOrderFormEtag(orderForm, commerce).then(
238
+ joinItems,
239
+ )
201
240
 
202
241
  return orderFormToCart(newOrderForm, skuLoader)
203
242
  }
@@ -206,37 +245,54 @@ export const validateCart = async (
206
245
  // Step2: Process items from both browser and checkout so they have the same shape
207
246
  const browserItemsById = groupById(acceptedOffer)
208
247
  const originItemsById = groupById(orderForm.items.map(orderFormItemToOffer))
209
- const browserItems = Array.from(browserItemsById.values()) // items on the user's browser
210
- const originItems = Array.from(originItemsById.values()) // items on the VTEX platform backend
248
+ const originItems = Array.from(originItemsById.entries()); // items on the VTEX platform backend
249
+ const browserItems = Array.from(browserItemsById.entries()); // items on the user's browser
211
250
 
212
251
  // Step3: Compute delta changes
213
- const { itemsToAdd, itemsToUpdate } = browserItems.reduce(
214
- (acc, item) => {
215
- const maybeOriginItem = originItemsById.get(getId(item))
216
-
217
- if (!maybeOriginItem) {
218
- acc.itemsToAdd.push(item)
219
- } else {
252
+ const { itemsToAdd, itemsToUpdate } = browserItems
253
+ .reduce(
254
+ (acc, [id, items]) => {
255
+ const maybeOriginItem = originItemsById.get(id)
256
+
257
+ // Adding new items to cart
258
+ if (!maybeOriginItem) {
259
+ items.forEach((item) => acc.itemsToAdd.push(item))
260
+
261
+ return acc
262
+ }
263
+
264
+ // Update existing items
265
+ const [head, ...tail] = maybeOriginItem
266
+ const totalQuantity = items.reduce(
267
+ (acc, curr) => acc + curr.quantity,
268
+ 0,
269
+ )
270
+
271
+ // set total quantity to first item
220
272
  acc.itemsToUpdate.push({
221
- ...maybeOriginItem,
222
- quantity: item.quantity,
273
+ ...head,
274
+ quantity: totalQuantity,
223
275
  })
224
- }
225
276
 
226
- return acc
227
- },
228
- {
229
- itemsToAdd: [] as IStoreOffer[],
230
- itemsToUpdate: [] as IStoreOffer[],
231
- }
232
- )
277
+ // Remove all the rest
278
+ tail.forEach((item) =>
279
+ acc.itemsToUpdate.push({ ...item, quantity: 0 })
280
+ )
281
+
282
+ return acc
283
+ },
284
+ {
285
+ itemsToAdd: [] as IStoreOffer[],
286
+ itemsToUpdate: [] as IStoreOffer[],
287
+ },
288
+ )
233
289
 
234
290
  const itemsToDelete = originItems
235
- .filter((item) => !browserItemsById.has(getId(item)))
236
- .map((item) => ({ ...item, quantity: 0 }))
291
+ .filter(([id]) => !browserItemsById.has(id))
292
+ .flatMap(([, items]) => items.map((item) => ({ ...item, quantity: 0 })))
237
293
 
238
294
  const changes = [...itemsToAdd, ...itemsToUpdate, ...itemsToDelete].map(
239
- offerToOrderItemInput
295
+ offerToOrderItemInput,
240
296
  )
241
297
 
242
298
  if (changes.length === 0) {
@@ -254,6 +310,7 @@ export const validateCart = async (
254
310
  .then((form) =>
255
311
  enableOrderFormSync ? setOrderFormEtag(form, commerce) : form
256
312
  )
313
+ .then(joinItems)
257
314
 
258
315
  // Step5: If no changes detected before/after updating orderForm, the order is validated
259
316
  if (equals(order, updatedOrderForm)) {