@faststore/api 1.10.34 → 1.11.7

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/dist/index.d.ts CHANGED
@@ -128,9 +128,7 @@ export declare const getResolvers: (options: Options) => {
128
128
  }>;
129
129
  };
130
130
  Mutation: {
131
- validateCart: (_: unknown, { cart: { order } }: {
132
- cart: import("./__generated__/schema").IStoreCart;
133
- }, ctx: import("./platforms/vtex").Context) => Promise<{
131
+ validateCart: (_: unknown, { cart: { order }, session }: import("./__generated__/schema").MutationValidateCartArgs, ctx: import("./platforms/vtex").Context) => Promise<{
134
132
  order: {
135
133
  orderNumber: string;
136
134
  acceptedOffer: Promise<{
@@ -34,6 +34,10 @@ export declare const VtexCommerce: ({ account, environment }: Options, ctx: Cont
34
34
  };
35
35
  checkout: {
36
36
  simulation: (args: SimulationArgs, { salesChannel }?: SimulationOptions) => Promise<Simulation>;
37
+ shippingData: ({ id, body }: {
38
+ id: string;
39
+ body: unknown;
40
+ }) => Promise<OrderForm>;
37
41
  orderForm: ({ id, refreshOutdatedData, channel, }: {
38
42
  id: string;
39
43
  refreshOutdatedData?: boolean | undefined;
@@ -29,6 +29,10 @@ export declare const getClients: (options: Options, ctx: Context) => {
29
29
  };
30
30
  checkout: {
31
31
  simulation: (args: import("./commerce/types/Simulation").SimulationArgs, { salesChannel }?: import("./commerce/types/Simulation").SimulationOptions) => Promise<import("./commerce/types/Simulation").Simulation>;
32
+ shippingData: ({ id, body }: {
33
+ id: string;
34
+ body: unknown;
35
+ }) => Promise<import("./commerce/types/OrderForm").OrderForm>;
32
36
  orderForm: ({ id, refreshOutdatedData, channel, }: {
33
37
  id: string;
34
38
  refreshOutdatedData?: boolean | undefined;
@@ -86,6 +86,7 @@ export interface Product {
86
86
  key: string;
87
87
  value: string;
88
88
  }>;
89
+ releaseDate: string;
89
90
  }
90
91
  interface Image {
91
92
  imageId: string;
@@ -158,9 +158,7 @@ export declare const getResolvers: (_: Options) => {
158
158
  }>;
159
159
  };
160
160
  Mutation: {
161
- validateCart: (_: unknown, { cart: { order } }: {
162
- cart: import("../..").IStoreCart;
163
- }, ctx: Context) => Promise<{
161
+ validateCart: (_: unknown, { cart: { order }, session }: import("../..").MutationValidateCartArgs, ctx: Context) => Promise<{
164
162
  order: {
165
163
  orderNumber: string;
166
164
  acceptedOffer: Promise<{
@@ -1,7 +1,5 @@
1
1
  export declare const Mutation: {
2
- validateCart: (_: unknown, { cart: { order } }: {
3
- cart: import("../../..").IStoreCart;
4
- }, ctx: import("..").Context) => Promise<{
2
+ validateCart: (_: unknown, { cart: { order }, session }: import("../../..").MutationValidateCartArgs, ctx: import("..").Context) => Promise<{
5
3
  order: {
6
4
  orderNumber: string;
7
5
  acceptedOffer: Promise<{
@@ -1,4 +1,4 @@
1
- import type { IStoreCart } from '../../../__generated__/schema';
1
+ import type { MutationValidateCartArgs } from '../../../__generated__/schema';
2
2
  import type { Context } from '..';
3
3
  /**
4
4
  * This resolver implements the optimistic cart behavior. The main idea in here
@@ -13,9 +13,7 @@ import type { Context } from '..';
13
13
  * 3. Update the orderForm in VTEX platform accordingly
14
14
  * 4. If any changes were made, send to the UI the new cart. Null otherwise
15
15
  */
16
- export declare const validateCart: (_: unknown, { cart: { order } }: {
17
- cart: IStoreCart;
18
- }, ctx: Context) => Promise<{
16
+ export declare const validateCart: (_: unknown, { cart: { order }, session }: MutationValidateCartArgs, ctx: Context) => Promise<{
19
17
  order: {
20
18
  orderNumber: string;
21
19
  acceptedOffer: Promise<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/api",
3
- "version": "1.10.34",
3
+ "version": "1.11.7",
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": "0cd611b59da838e4939992888ce60ea768e5e32e"
50
50
  }
@@ -208,6 +208,7 @@ export type MutationSubscribeToNewsletterArgs = {
208
208
 
209
209
  export type MutationValidateCartArgs = {
210
210
  cart: IStoreCart;
211
+ session?: Maybe<IStoreSession>;
211
212
  };
212
213
 
213
214
 
@@ -610,6 +611,8 @@ export type StoreProduct = {
610
611
  offers: StoreAggregateOffer;
611
612
  /** Product ID, such as [ISBN](https://www.isbn-international.org/content/what-isbn) or similar global IDs. */
612
613
  productID: Scalars['String'];
614
+ /** The product's release date. Formatted using https://en.wikipedia.org/wiki/ISO_8601 */
615
+ releaseDate: Scalars['String'];
613
616
  /** Array with review information. */
614
617
  review: Array<StoreReview>;
615
618
  /** Meta tag data. */
@@ -85,6 +85,17 @@ export const VtexCommerce = (
85
85
  }
86
86
  )
87
87
  },
88
+ shippingData: (
89
+ { id, body }: { id: string; body: unknown },
90
+ ): Promise<OrderForm> => {
91
+ return fetchAPI(
92
+ `${base}/api/checkout/pub/orderForm/${id}/attachments/shippingData`,
93
+ {
94
+ ...BASE_INIT,
95
+ body: JSON.stringify(body),
96
+ },
97
+ );
98
+ },
88
99
  orderForm: ({
89
100
  id,
90
101
  refreshOutdatedData = true,
@@ -90,6 +90,7 @@ export interface Product {
90
90
  specificationGroups: SpecificationGroup[]
91
91
  properties: Array<{ name: string; values: string[] }>
92
92
  selectedProperties: Array<{ key: string; value: string }>
93
+ releaseDate: string
93
94
  }
94
95
 
95
96
  interface Image {
@@ -116,4 +116,5 @@ export const StoreProduct: Record<string, Resolver<Root>> & {
116
116
 
117
117
  return [...propertyValueSpecifications, ...propertyValueAttachments]
118
118
  },
119
+ releaseDate: ({ isVariantOf: { releaseDate } }) => releaseDate ?? ''
119
120
  }
@@ -1,11 +1,15 @@
1
1
  import deepEquals from 'fast-deep-equal'
2
2
 
3
3
  import { md5 } from '../utils/md5'
4
+ import { attachmentToPropertyValue, getPropertyId, VALUE_REFERENCES } from '../utils/propertyValue'
5
+
4
6
  import type {
5
- IStoreCart,
7
+ IStoreSession,
6
8
  IStoreOffer,
7
9
  IStoreOrder,
8
10
  IStorePropertyValue,
11
+ Maybe,
12
+ MutationValidateCartArgs,
9
13
  } from '../../../__generated__/schema'
10
14
  import type {
11
15
  OrderForm,
@@ -13,12 +17,6 @@ import type {
13
17
  OrderFormItem,
14
18
  } from '../clients/commerce/types/OrderForm'
15
19
  import type { Context } from '..'
16
- import {
17
- attachmentToPropertyValue,
18
- getPropertyId,
19
- VALUE_REFERENCES,
20
- } from '../utils/propertyValue'
21
-
22
20
  type Indexed<T> = T & { index?: number }
23
21
 
24
22
  const isAttachment = (value: IStorePropertyValue) =>
@@ -28,7 +26,7 @@ const getId = (item: IStoreOffer) =>
28
26
  [
29
27
  item.itemOffered.sku,
30
28
  item.seller.identifier,
31
- item.price,
29
+ item.price < 0.01 ? 'Gift' : undefined,
32
30
  item.itemOffered.additionalProperty
33
31
  ?.filter(isAttachment)
34
32
  .map(getPropertyId)
@@ -39,7 +37,7 @@ const getId = (item: IStoreOffer) =>
39
37
 
40
38
  const orderFormItemToOffer = (
41
39
  item: OrderFormItem,
42
- index?: number
40
+ index?: number,
43
41
  ): Indexed<IStoreOffer> => ({
44
42
  listPrice: item.listPrice / 100,
45
43
  price: item.sellingPrice / 100,
@@ -55,7 +53,7 @@ const orderFormItemToOffer = (
55
53
  })
56
54
 
57
55
  const offerToOrderItemInput = (
58
- offer: Indexed<IStoreOffer>
56
+ offer: Indexed<IStoreOffer>,
59
57
  ): OrderFormInputItem => ({
60
58
  quantity: offer.quantity,
61
59
  seller: offer.seller.identifier,
@@ -69,14 +67,18 @@ const offerToOrderItemInput = (
69
67
  })),
70
68
  })
71
69
 
72
- const groupById = (offers: IStoreOffer[]): Map<string, IStoreOffer> =>
70
+ const groupById = (offers: IStoreOffer[]): Map<string, IStoreOffer[]> =>
73
71
  offers.reduce((acc, item) => {
74
72
  const id = getId(item)
75
73
 
76
- acc.set(id, acc.get(id) ?? item)
74
+ if (!acc.has(id)) {
75
+ acc.set(id, [])
76
+ }
77
+
78
+ acc.get(id)?.push(item)
77
79
 
78
80
  return acc
79
- }, new Map<string, IStoreOffer>())
81
+ }, new Map<string, IStoreOffer[]>())
80
82
 
81
83
  const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
82
84
  const pick = (item: Indexed<IStoreOffer>, index: number) => ({
@@ -96,9 +98,42 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
96
98
  return isSameOrder && orderItemsAreSync
97
99
  }
98
100
 
101
+ const joinItems = (form: OrderForm) => {
102
+ const itemsById = form.items
103
+ .reduce((acc, item) => {
104
+ const id = getId(orderFormItemToOffer(item))
105
+
106
+ if (!acc[id]) {
107
+ acc[id] = []
108
+ }
109
+
110
+ acc[id].push(item)
111
+
112
+ return acc
113
+ }, {} as Record<string, OrderFormItem[]>)
114
+
115
+ return {
116
+ ...form,
117
+ items: Object.values(itemsById).map((items) => {
118
+ const [item] = items
119
+ const quantity = items.reduce((acc, i) => acc + i.quantity, 0)
120
+ const totalPrice = items.reduce(
121
+ (acc, i) => acc + i.quantity * i.sellingPrice,
122
+ 0,
123
+ )
124
+
125
+ return {
126
+ ...item,
127
+ quantity,
128
+ sellingPrice: totalPrice / quantity,
129
+ }
130
+ }),
131
+ }
132
+ }
133
+
99
134
  const orderFormToCart = async (
100
135
  form: OrderForm,
101
- skuLoader: Context['loaders']['skuLoader']
136
+ skuLoader: Context['loaders']['skuLoader'],
102
137
  ) => {
103
138
  return {
104
139
  order: {
@@ -119,7 +154,7 @@ const getOrderFormEtag = ({ items }: OrderForm) => md5(JSON.stringify(items))
119
154
 
120
155
  const setOrderFormEtag = async (
121
156
  form: OrderForm,
122
- commerce: Context['clients']['commerce']
157
+ commerce: Context['clients']['commerce'],
123
158
  ) => {
124
159
  try {
125
160
  const orderForm = await commerce.checkout.setCustomData({
@@ -132,7 +167,7 @@ const setOrderFormEtag = async (
132
167
  return orderForm
133
168
  } catch (err) {
134
169
  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'
170
+ '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
171
  )
137
172
 
138
173
  throw err
@@ -146,7 +181,7 @@ const setOrderFormEtag = async (
146
181
  */
147
182
  const isOrderFormStale = (form: OrderForm) => {
148
183
  const faststoreData = form.customData?.customApps.find(
149
- (app) => app.id === 'faststore'
184
+ (app) => app.id === 'faststore',
150
185
  )
151
186
 
152
187
  const oldEtag = faststoreData?.fields?.cartEtag
@@ -160,6 +195,31 @@ const isOrderFormStale = (form: OrderForm) => {
160
195
  return newEtag !== oldEtag
161
196
  }
162
197
 
198
+ // Returns the regionalized orderForm
199
+ const getOrderForm = async (
200
+ id: string,
201
+ session: Maybe<IStoreSession> | undefined,
202
+ { clients: { commerce } }: Context,
203
+ ) => {
204
+ const orderForm = await commerce.checkout.orderForm({
205
+ id,
206
+ });
207
+
208
+ const shouldUpdateShippingData =
209
+ orderForm.shippingData?.address?.postalCode != session?.postalCode;
210
+
211
+ if (shouldUpdateShippingData) {
212
+ return commerce.checkout.shippingData({
213
+ id: orderForm.orderFormId,
214
+ body: {
215
+ selectedAddresses: [session],
216
+ },
217
+ });
218
+ }
219
+
220
+ return orderForm;
221
+ };
222
+
163
223
  /**
164
224
  * This resolver implements the optimistic cart behavior. The main idea in here
165
225
  * is that we receive a cart from the UI (as query params) and we validate it with
@@ -175,8 +235,8 @@ const isOrderFormStale = (form: OrderForm) => {
175
235
  */
176
236
  export const validateCart = async (
177
237
  _: unknown,
178
- { cart: { order } }: { cart: IStoreCart },
179
- ctx: Context
238
+ { cart: { order }, session }: MutationValidateCartArgs,
239
+ ctx: Context,
180
240
  ) => {
181
241
  const { enableOrderFormSync } = ctx.storage.flags
182
242
  const { orderNumber, acceptedOffer } = order
@@ -186,9 +246,7 @@ export const validateCart = async (
186
246
  } = ctx
187
247
 
188
248
  // Step1: Get OrderForm from VTEX Commerce
189
- const orderForm = await commerce.checkout.orderForm({
190
- id: orderNumber,
191
- })
249
+ const orderForm = await getOrderForm(orderNumber, session, ctx)
192
250
 
193
251
  // Step1.5: Check if another system changed the orderForm with this orderNumber
194
252
  // If so, this means the user interacted with this cart elsewhere and expects
@@ -197,7 +255,9 @@ export const validateCart = async (
197
255
  const isStale = isOrderFormStale(orderForm)
198
256
 
199
257
  if (isStale === true && orderNumber) {
200
- const newOrderForm = await setOrderFormEtag(orderForm, commerce)
258
+ const newOrderForm = await setOrderFormEtag(orderForm, commerce).then(
259
+ joinItems,
260
+ )
201
261
 
202
262
  return orderFormToCart(newOrderForm, skuLoader)
203
263
  }
@@ -206,37 +266,54 @@ export const validateCart = async (
206
266
  // Step2: Process items from both browser and checkout so they have the same shape
207
267
  const browserItemsById = groupById(acceptedOffer)
208
268
  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
269
+ const originItems = Array.from(originItemsById.entries()); // items on the VTEX platform backend
270
+ const browserItems = Array.from(browserItemsById.entries()); // items on the user's browser
211
271
 
212
272
  // 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 {
273
+ const { itemsToAdd, itemsToUpdate } = browserItems
274
+ .reduce(
275
+ (acc, [id, items]) => {
276
+ const maybeOriginItem = originItemsById.get(id)
277
+
278
+ // Adding new items to cart
279
+ if (!maybeOriginItem) {
280
+ items.forEach((item) => acc.itemsToAdd.push(item))
281
+
282
+ return acc
283
+ }
284
+
285
+ // Update existing items
286
+ const [head, ...tail] = maybeOriginItem
287
+ const totalQuantity = items.reduce(
288
+ (acc, curr) => acc + curr.quantity,
289
+ 0,
290
+ )
291
+
292
+ // set total quantity to first item
220
293
  acc.itemsToUpdate.push({
221
- ...maybeOriginItem,
222
- quantity: item.quantity,
294
+ ...head,
295
+ quantity: totalQuantity,
223
296
  })
224
- }
225
297
 
226
- return acc
227
- },
228
- {
229
- itemsToAdd: [] as IStoreOffer[],
230
- itemsToUpdate: [] as IStoreOffer[],
231
- }
232
- )
298
+ // Remove all the rest
299
+ tail.forEach((item) =>
300
+ acc.itemsToUpdate.push({ ...item, quantity: 0 })
301
+ )
302
+
303
+ return acc
304
+ },
305
+ {
306
+ itemsToAdd: [] as IStoreOffer[],
307
+ itemsToUpdate: [] as IStoreOffer[],
308
+ },
309
+ )
233
310
 
234
311
  const itemsToDelete = originItems
235
- .filter((item) => !browserItemsById.has(getId(item)))
236
- .map((item) => ({ ...item, quantity: 0 }))
312
+ .filter(([id]) => !browserItemsById.has(id))
313
+ .flatMap(([, items]) => items.map((item) => ({ ...item, quantity: 0 })))
237
314
 
238
315
  const changes = [...itemsToAdd, ...itemsToUpdate, ...itemsToDelete].map(
239
- offerToOrderItemInput
316
+ offerToOrderItemInput,
240
317
  )
241
318
 
242
319
  if (changes.length === 0) {
@@ -254,6 +331,7 @@ export const validateCart = async (
254
331
  .then((form) =>
255
332
  enableOrderFormSync ? setOrderFormEtag(form, commerce) : form
256
333
  )
334
+ .then(joinItems)
257
335
 
258
336
  // Step5: If no changes detected before/after updating orderForm, the order is validated
259
337
  if (equals(order, updatedOrderForm)) {
@@ -2,7 +2,7 @@ type Mutation {
2
2
  """
3
3
  Checks for changes between the cart presented in the UI and the cart stored in the ecommerce platform. If changes are detected, it returns the cart stored on the platform. Otherwise, it returns `null`.
4
4
  """
5
- validateCart(cart: IStoreCart!): StoreCart
5
+ validateCart(cart: IStoreCart!, session: IStoreSession): StoreCart
6
6
  """
7
7
  Updates a web session with the specified values.
8
8
  """
@@ -62,6 +62,10 @@ type StoreProduct {
62
62
  Array of additional properties.
63
63
  """
64
64
  additionalProperty: [StorePropertyValue!]!
65
+ """
66
+ The product's release date. Formatted using https://en.wikipedia.org/wiki/ISO_8601
67
+ """
68
+ releaseDate: String!
65
69
  }
66
70
 
67
71
  """