@faststore/api 1.9.6 → 1.9.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/api.cjs.development.js +113 -34
  3. package/dist/api.cjs.development.js.map +1 -1
  4. package/dist/api.cjs.production.min.js +1 -1
  5. package/dist/api.cjs.production.min.js.map +1 -1
  6. package/dist/api.esm.js +113 -34
  7. package/dist/api.esm.js.map +1 -1
  8. package/dist/index.d.ts +5 -4
  9. package/dist/platforms/vtex/clients/commerce/types/Portal.d.ts +1 -1
  10. package/dist/platforms/vtex/index.d.ts +5 -4
  11. package/dist/platforms/vtex/loaders/index.d.ts +1 -1
  12. package/dist/platforms/vtex/loaders/sku.d.ts +1 -2
  13. package/dist/platforms/vtex/resolvers/mutation.d.ts +3 -3
  14. package/dist/platforms/vtex/resolvers/offer.d.ts +1 -1
  15. package/dist/platforms/vtex/resolvers/seo.d.ts +1 -0
  16. package/dist/platforms/vtex/resolvers/validateCart.d.ts +3 -3
  17. package/dist/platforms/vtex/utils/canonical.d.ts +2 -0
  18. package/dist/platforms/vtex/utils/facets.d.ts +2 -0
  19. package/dist/platforms/vtex/utils/orderStatistics.d.ts +4 -0
  20. package/dist/platforms/vtex/utils/sku.d.ts +8 -0
  21. package/package.json +2 -2
  22. package/src/platforms/vtex/clients/commerce/index.ts +1 -1
  23. package/src/platforms/vtex/clients/commerce/types/Portal.ts +1 -1
  24. package/src/platforms/vtex/loaders/sku.ts +3 -16
  25. package/src/platforms/vtex/resolvers/offer.ts +6 -3
  26. package/src/platforms/vtex/resolvers/product.ts +6 -4
  27. package/src/platforms/vtex/resolvers/query.ts +44 -1
  28. package/src/platforms/vtex/resolvers/seo.ts +2 -2
  29. package/src/platforms/vtex/resolvers/validateCart.ts +3 -3
  30. package/src/platforms/vtex/utils/canonical.ts +3 -0
  31. package/src/platforms/vtex/utils/facets.ts +6 -0
  32. package/src/platforms/vtex/utils/orderStatistics.ts +16 -0
  33. package/src/platforms/vtex/utils/sku.ts +26 -0
package/dist/index.d.ts CHANGED
@@ -49,6 +49,7 @@ export declare const getResolvers: (options: Options) => {
49
49
  StoreSeo: Record<string, import("./platforms/vtex").Resolver<{
50
50
  title?: string | undefined;
51
51
  description?: string | undefined;
52
+ canonical?: string | undefined;
52
53
  }, unknown, any>>;
53
54
  StoreFacet: Record<string, import("./platforms/vtex").Resolver<import("./platforms/vtex/clients/search/types/FacetSearchResult").Facet, unknown, any>>;
54
55
  StoreFacetValue: Record<string, import("./platforms/vtex").Resolver<import("./platforms/vtex/clients/search/types/FacetSearchResult").FacetValue, unknown, any>>;
@@ -57,7 +58,7 @@ export declare const getResolvers: (options: Options) => {
57
58
  } & {
58
59
  attachmentsValues?: import("./platforms/vtex/clients/commerce/types/OrderForm").Attachment[] | undefined;
59
60
  }> | (import("./platforms/vtex/clients/commerce/types/OrderForm").OrderFormItem & {
60
- product: Promise<import("./platforms/vtex/utils/enhanceSku").EnhancedSku>;
61
+ product: import("./platforms/vtex/utils/enhanceSku").EnhancedSku;
61
62
  }), unknown, any>>;
62
63
  StoreAggregateRating: Record<string, import("./platforms/vtex").Resolver<unknown, unknown, any>>;
63
64
  StoreReview: Record<string, import("./platforms/vtex").Resolver<unknown, unknown, any>>;
@@ -125,8 +126,8 @@ export declare const getResolvers: (options: Options) => {
125
126
  }, ctx: import("./platforms/vtex").Context) => Promise<{
126
127
  order: {
127
128
  orderNumber: string;
128
- acceptedOffer: {
129
- product: Promise<import("./platforms/vtex/utils/enhanceSku").EnhancedSku>;
129
+ acceptedOffer: Promise<{
130
+ product: import("./platforms/vtex/utils/enhanceSku").EnhancedSku;
130
131
  id: string;
131
132
  name: string;
132
133
  detailUrl: string;
@@ -170,7 +171,7 @@ export declare const getResolvers: (options: Options) => {
170
171
  total: number;
171
172
  };
172
173
  attachments: import("./platforms/vtex/clients/commerce/types/OrderForm").Attachment[];
173
- }[];
174
+ }>[];
174
175
  };
175
176
  messages: {
176
177
  text: any;
@@ -5,7 +5,7 @@ export interface CollectionPageType {
5
5
  url: string;
6
6
  title: string;
7
7
  metaTagDescription: string;
8
- pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory';
8
+ pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory' | 'Product';
9
9
  }
10
10
  export interface FallbackPageType {
11
11
  id: null;
@@ -77,6 +77,7 @@ export declare const getResolvers: (_: Options) => {
77
77
  StoreSeo: Record<string, Resolver<{
78
78
  title?: string | undefined;
79
79
  description?: string | undefined;
80
+ canonical?: string | undefined;
80
81
  }, unknown, any>>;
81
82
  StoreFacet: Record<string, Resolver<import("./clients/search/types/FacetSearchResult").Facet, unknown, any>>;
82
83
  StoreFacetValue: Record<string, Resolver<import("./clients/search/types/FacetSearchResult").FacetValue, unknown, any>>;
@@ -85,7 +86,7 @@ export declare const getResolvers: (_: Options) => {
85
86
  } & {
86
87
  attachmentsValues?: import("./clients/commerce/types/OrderForm").Attachment[] | undefined;
87
88
  }> | (import("./clients/commerce/types/OrderForm").OrderFormItem & {
88
- product: Promise<import("./utils/enhanceSku").EnhancedSku>;
89
+ product: import("./utils/enhanceSku").EnhancedSku;
89
90
  }), unknown, any>>;
90
91
  StoreAggregateRating: Record<string, Resolver<unknown, unknown, any>>;
91
92
  StoreReview: Record<string, Resolver<unknown, unknown, any>>;
@@ -153,8 +154,8 @@ export declare const getResolvers: (_: Options) => {
153
154
  }, ctx: Context) => Promise<{
154
155
  order: {
155
156
  orderNumber: string;
156
- acceptedOffer: {
157
- product: Promise<import("./utils/enhanceSku").EnhancedSku>;
157
+ acceptedOffer: Promise<{
158
+ product: import("./utils/enhanceSku").EnhancedSku;
158
159
  id: string;
159
160
  name: string;
160
161
  detailUrl: string;
@@ -198,7 +199,7 @@ export declare const getResolvers: (_: Options) => {
198
199
  total: number;
199
200
  };
200
201
  attachments: import("./clients/commerce/types/OrderForm").Attachment[];
201
- }[];
202
+ }>[];
202
203
  };
203
204
  messages: {
204
205
  text: any;
@@ -1,7 +1,7 @@
1
1
  import type { Context, Options } from '..';
2
2
  export declare type Loaders = ReturnType<typeof getLoaders>;
3
3
  export declare const getLoaders: (options: Options, { clients }: Context) => {
4
- skuLoader: import("dataloader")<import("../utils/facets").SelectedFacet[], import("../utils/enhanceSku").EnhancedSku, import("../utils/facets").SelectedFacet[]>;
4
+ skuLoader: import("dataloader")<string, import("../utils/enhanceSku").EnhancedSku, string>;
5
5
  simulationLoader: import("dataloader")<import("../clients/commerce/types/Simulation").PayloadItem[], import("../clients/commerce/types/Simulation").Simulation, import("../clients/commerce/types/Simulation").PayloadItem[]>;
6
6
  collectionLoader: import("dataloader")<string, import("../clients/commerce/types/Portal").CollectionPageType, string>;
7
7
  };
@@ -2,5 +2,4 @@ import DataLoader from 'dataloader';
2
2
  import type { EnhancedSku } from '../utils/enhanceSku';
3
3
  import type { Options } from '..';
4
4
  import type { Clients } from '../clients';
5
- import type { SelectedFacet } from '../utils/facets';
6
- export declare const getSkuLoader: (_: Options, clients: Clients) => DataLoader<SelectedFacet[], EnhancedSku, SelectedFacet[]>;
5
+ export declare const getSkuLoader: (_: Options, clients: Clients) => DataLoader<string, EnhancedSku, string>;
@@ -4,8 +4,8 @@ export declare const Mutation: {
4
4
  }, ctx: import("..").Context) => Promise<{
5
5
  order: {
6
6
  orderNumber: string;
7
- acceptedOffer: {
8
- product: Promise<import("../utils/enhanceSku").EnhancedSku>;
7
+ acceptedOffer: Promise<{
8
+ product: import("../utils/enhanceSku").EnhancedSku;
9
9
  id: string;
10
10
  name: string;
11
11
  detailUrl: string;
@@ -49,7 +49,7 @@ export declare const Mutation: {
49
49
  total: number;
50
50
  };
51
51
  attachments: import("../clients/commerce/types/OrderForm").Attachment[];
52
- }[];
52
+ }>[];
53
53
  };
54
54
  messages: {
55
55
  text: any;
@@ -4,7 +4,7 @@ import type { ArrayElementType } from '../../../typings';
4
4
  import type { EnhancedSku } from '../utils/enhanceSku';
5
5
  import type { OrderFormItem } from '../clients/commerce/types/OrderForm';
6
6
  declare type OrderFormProduct = OrderFormItem & {
7
- product: Promise<EnhancedSku>;
7
+ product: EnhancedSku;
8
8
  };
9
9
  declare type SearchProduct = ArrayElementType<ReturnType<typeof StoreAggregateOffer.offers>>;
10
10
  declare type Root = SearchProduct | OrderFormProduct;
@@ -2,6 +2,7 @@ import type { Resolver } from '..';
2
2
  declare type Root = {
3
3
  title?: string;
4
4
  description?: string;
5
+ canonical?: string;
5
6
  };
6
7
  export declare const StoreSeo: Record<string, Resolver<Root>>;
7
8
  export {};
@@ -18,8 +18,8 @@ export declare const validateCart: (_: unknown, { cart: { order } }: {
18
18
  }, ctx: Context) => Promise<{
19
19
  order: {
20
20
  orderNumber: string;
21
- acceptedOffer: {
22
- product: Promise<import("../utils/enhanceSku").EnhancedSku>;
21
+ acceptedOffer: Promise<{
22
+ product: import("../utils/enhanceSku").EnhancedSku;
23
23
  id: string;
24
24
  name: string;
25
25
  detailUrl: string;
@@ -63,7 +63,7 @@ export declare const validateCart: (_: unknown, { cart: { order } }: {
63
63
  total: number;
64
64
  };
65
65
  attachments: import("../clients/commerce/types/OrderForm").Attachment[];
66
- }[];
66
+ }>[];
67
67
  };
68
68
  messages: {
69
69
  text: any;
@@ -0,0 +1,2 @@
1
+ import type { Product } from '../clients/search/types/ProductSearchResult';
2
+ export declare const canonicalFromProduct: ({ linkText }: Product) => string;
@@ -13,5 +13,7 @@ export declare const transformSelectedFacet: ({ key, value }: SelectedFacet) =>
13
13
  key: string;
14
14
  value: string;
15
15
  };
16
+ export declare const findSlug: (facets?: SelectedFacet[] | null | undefined) => string | null;
17
+ export declare const findSkuId: (facets?: SelectedFacet[] | null | undefined) => string | null;
16
18
  export declare const findLocale: (facets?: SelectedFacet[] | null | undefined) => string | null;
17
19
  export declare const findChannel: (facets?: SelectedFacet[] | null | undefined) => string | null;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * More info at: https://en.wikipedia.org/wiki/Order_statistic
3
+ */
4
+ export declare const min: <T>(array: T[], cmp: (a: T, b: T) => number) => T;
@@ -0,0 +1,8 @@
1
+ import type { Item } from '../clients/search/types/ProductSearchResult';
2
+ /**
3
+ * This function implements Portal heuristics for returning the best sku for a product.
4
+ *
5
+ * The best sku is the one with the best (cheapest available) offer
6
+ * */
7
+ export declare const pickBestSku: (skus: Item[]) => Item;
8
+ export declare const isValidSkuId: (skuId: string) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/api",
3
- "version": "1.9.6",
3
+ "version": "1.9.7",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -44,5 +44,5 @@
44
44
  "peerDependencies": {
45
45
  "graphql": "^15.6.0"
46
46
  },
47
- "gitHead": "69e237e43d8fc3940fffdd2a9fa8d5ed43f40181"
47
+ "gitHead": "a0c8cbd82dc0d83b4ca9f186fd974e09a2bbd768"
48
48
  }
@@ -1,5 +1,5 @@
1
- import type { Context, Options } from '../../index'
2
1
  import { fetchAPI } from '../fetch'
2
+ import type { Context, Options } from '../../index'
3
3
  import type { Brand } from './types/Brand'
4
4
  import type { CategoryTree } from './types/CategoryTree'
5
5
  import type { OrderForm, OrderFormInputItem } from './types/OrderForm'
@@ -6,7 +6,7 @@ export interface CollectionPageType {
6
6
  url: string
7
7
  title: string
8
8
  metaTagDescription: string
9
- pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory'
9
+ pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory' | 'Product'
10
10
  }
11
11
 
12
12
  export interface FallbackPageType {
@@ -1,26 +1,13 @@
1
1
  import DataLoader from 'dataloader'
2
2
 
3
3
  import { enhanceSku } from '../utils/enhanceSku'
4
- import { BadRequestError, NotFoundError } from '../../errors'
4
+ import { NotFoundError } from '../../errors'
5
5
  import type { EnhancedSku } from '../utils/enhanceSku'
6
6
  import type { Options } from '..'
7
7
  import type { Clients } from '../clients'
8
- import type { SelectedFacet } from '../utils/facets'
9
8
 
10
9
  export const getSkuLoader = (_: Options, clients: Clients) => {
11
- const loader = async (facetsList: readonly SelectedFacet[][]) => {
12
- const skuIds = facetsList.map((facets) => {
13
- const maybeFacet = facets.find(({ key }) => key === 'id')
14
-
15
- if (!maybeFacet) {
16
- throw new BadRequestError(
17
- 'Error while loading SKU. Needs to pass an id to selected facets'
18
- )
19
- }
20
-
21
- return maybeFacet.value
22
- })
23
-
10
+ const loader = async (skuIds: readonly string[]) => {
24
11
  const { products } = await clients.search.products({
25
12
  query: `sku:${skuIds.join(';')}`,
26
13
  page: 0,
@@ -47,7 +34,7 @@ export const getSkuLoader = (_: Options, clients: Clients) => {
47
34
  return skus
48
35
  }
49
36
 
50
- return new DataLoader<SelectedFacet[], EnhancedSku>(loader, {
37
+ return new DataLoader<string, EnhancedSku>(loader, {
51
38
  maxBatchSize: 99, // Max allowed batch size of Search API
52
39
  })
53
40
  }
@@ -11,7 +11,7 @@ import type { ArrayElementType } from '../../../typings'
11
11
  import type { EnhancedSku } from '../utils/enhanceSku'
12
12
  import type { OrderFormItem } from '../clients/commerce/types/OrderForm'
13
13
 
14
- type OrderFormProduct = OrderFormItem & { product: Promise<EnhancedSku> }
14
+ type OrderFormProduct = OrderFormItem & { product: EnhancedSku }
15
15
  type SearchProduct = ArrayElementType<
16
16
  ReturnType<typeof StoreAggregateOffer.offers>
17
17
  >
@@ -96,13 +96,16 @@ export const StoreOffer: Record<string, Resolver<Root>> = {
96
96
 
97
97
  return null
98
98
  },
99
- itemOffered: async (root) => {
99
+ itemOffered: (root) => {
100
100
  if (isSearchItem(root)) {
101
101
  return root.product
102
102
  }
103
103
 
104
104
  if (isOrderFormItem(root)) {
105
- return { ...(await root.product), attachmentsValues: root.attachments }
105
+ return {
106
+ ...root.product,
107
+ attachmentsValues: root.attachments,
108
+ }
106
109
  }
107
110
 
108
111
  return null
@@ -1,3 +1,4 @@
1
+ import { canonicalFromProduct } from '../utils/canonical'
1
2
  import { enhanceCommercialOffer } from '../utils/enhanceCommercialOffer'
2
3
  import { bestOfferFirst } from '../utils/productStock'
3
4
  import { slugify } from '../utils/slugify'
@@ -41,9 +42,10 @@ export const StoreProduct: Record<string, Resolver<Root>> & {
41
42
  name: ({ isVariantOf, name }) => name ?? isVariantOf.productName,
42
43
  slug: ({ isVariantOf: { linkText }, itemId }) => getSlug(linkText, itemId),
43
44
  description: ({ isVariantOf: { description } }) => description,
44
- seo: ({ isVariantOf: { description, productName } }) => ({
45
- title: productName,
46
- description,
45
+ seo: ({ isVariantOf }) => ({
46
+ title: isVariantOf.productName,
47
+ description: isVariantOf.description,
48
+ canonical: canonicalFromProduct(isVariantOf),
47
49
  }),
48
50
  brand: ({ isVariantOf: { brand } }) => ({ name: brand }),
49
51
  breadcrumbList: ({
@@ -85,7 +87,7 @@ export const StoreProduct: Record<string, Resolver<Root>> & {
85
87
  aggregateRating: () => ({}),
86
88
  offers: (root) =>
87
89
  root.sellers
88
- .flatMap((seller) =>
90
+ .map((seller) =>
89
91
  enhanceCommercialOffer({
90
92
  offer: seller.commertialOffer,
91
93
  seller,
@@ -1,8 +1,11 @@
1
+ import { NotFoundError, BadRequestError } from '../../errors'
1
2
  import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
2
3
  import { enhanceSku } from '../utils/enhanceSku'
3
4
  import {
4
5
  findChannel,
5
6
  findLocale,
7
+ findSkuId,
8
+ findSlug,
6
9
  transformSelectedFacet,
7
10
  } from '../utils/facets'
8
11
  import { SORT_MAP } from '../utils/sort'
@@ -16,12 +19,15 @@ import type {
16
19
  } from '../../../__generated__/schema'
17
20
  import type { CategoryTree } from '../clients/commerce/types/CategoryTree'
18
21
  import type { Context } from '../index'
22
+ import { isValidSkuId, pickBestSku } from '../utils/sku'
19
23
 
20
24
  export const Query = {
21
25
  product: async (_: unknown, { locator }: QueryProductArgs, ctx: Context) => {
22
26
  // Insert channel in context for later usage
23
27
  const channel = findChannel(locator)
24
28
  const locale = findLocale(locator)
29
+ const id = findSkuId(locator)
30
+ const slug = findSlug(locator)
25
31
 
26
32
  if (channel) {
27
33
  mutateChannelContext(ctx, channel)
@@ -33,9 +39,46 @@ export const Query = {
33
39
 
34
40
  const {
35
41
  loaders: { skuLoader },
42
+ clients: { commerce, search },
36
43
  } = ctx
37
44
 
38
- return skuLoader.load(locator)
45
+ try {
46
+ const skuId = id ?? slug?.split('-').pop() ?? ''
47
+
48
+ if (!isValidSkuId(skuId)) {
49
+ throw new Error('Invalid SkuId')
50
+ }
51
+
52
+ const sku = await skuLoader.load(skuId)
53
+
54
+ return sku
55
+ } catch (err) {
56
+ if (slug == null) {
57
+ throw new BadRequestError('Missing slug or id')
58
+ }
59
+
60
+ const route = await commerce.catalog.portal.pagetype(`${slug}/p`)
61
+
62
+ if (route.pageType !== 'Product' || !route.id) {
63
+ throw new NotFoundError(`No product found for slug ${slug}`)
64
+ }
65
+
66
+ const {
67
+ products: [product],
68
+ } = await search.products({
69
+ page: 0,
70
+ count: 1,
71
+ query: `product:${route.id}`,
72
+ })
73
+
74
+ if (!product) {
75
+ throw new NotFoundError(`No product found for id ${route.id}`)
76
+ }
77
+
78
+ const sku = pickBestSku(product.items)
79
+
80
+ return enhanceSku(sku, product)
81
+ }
39
82
  },
40
83
  collection: (_: unknown, { slug }: QueryCollectionArgs, ctx: Context) => {
41
84
  const {
@@ -1,10 +1,10 @@
1
1
  import type { Resolver } from '..'
2
2
 
3
- type Root = { title?: string; description?: string }
3
+ type Root = { title?: string; description?: string; canonical?: string }
4
4
 
5
5
  export const StoreSeo: Record<string, Resolver<Root>> = {
6
6
  title: ({ title }) => title ?? '',
7
7
  description: ({ description }) => description ?? '',
8
+ canonical: ({ canonical }) => canonical ?? '',
8
9
  titleTemplate: () => '',
9
- canonical: () => '',
10
10
  }
@@ -96,16 +96,16 @@ const equals = (storeOrder: IStoreOrder, orderForm: OrderForm) => {
96
96
  return isSameOrder && orderItemsAreSync
97
97
  }
98
98
 
99
- const orderFormToCart = (
99
+ const orderFormToCart = async (
100
100
  form: OrderForm,
101
101
  skuLoader: Context['loaders']['skuLoader']
102
102
  ) => {
103
103
  return {
104
104
  order: {
105
105
  orderNumber: form.orderFormId,
106
- acceptedOffer: form.items.map((item) => ({
106
+ acceptedOffer: form.items.map(async (item) => ({
107
107
  ...item,
108
- product: skuLoader.load([{ key: 'id', value: item.id }]), // TODO: add channel
108
+ product: await skuLoader.load(item.id), // TODO: add channel
109
109
  })),
110
110
  },
111
111
  messages: form.messages.map(({ text, status }) => ({
@@ -0,0 +1,3 @@
1
+ import type { Product } from '../clients/search/types/ProductSearchResult'
2
+
3
+ export const canonicalFromProduct = ({ linkText }: Product) => `/${linkText}/p`
@@ -34,6 +34,12 @@ export const transformSelectedFacet = ({ key, value }: SelectedFacet) => {
34
34
  }
35
35
  }
36
36
 
37
+ export const findSlug = (facets?: Maybe<SelectedFacet[]>) =>
38
+ facets?.find((x) => x.key === 'slug')?.value ?? null
39
+
40
+ export const findSkuId = (facets?: Maybe<SelectedFacet[]>) =>
41
+ facets?.find((x) => x.key === 'id')?.value ?? null
42
+
37
43
  export const findLocale = (facets?: Maybe<SelectedFacet[]>) =>
38
44
  facets?.find((x) => x.key === 'locale')?.value ?? null
39
45
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * More info at: https://en.wikipedia.org/wiki/Order_statistic
3
+ */
4
+
5
+ // O(n) search to find the max of an array
6
+ export const min = <T>(array: T[], cmp: (a: T, b: T) => number) => {
7
+ let best = 0
8
+
9
+ for (let curr = 1; curr < array.length; curr++) {
10
+ if (cmp(array[best], array[curr]) > 0) {
11
+ best = curr
12
+ }
13
+ }
14
+
15
+ return array[best]
16
+ }
@@ -0,0 +1,26 @@
1
+ import { min } from './orderStatistics'
2
+ import { bestOfferFirst } from './productStock'
3
+ import type { Item } from '../clients/search/types/ProductSearchResult'
4
+
5
+ /**
6
+ * This function implements Portal heuristics for returning the best sku for a product.
7
+ *
8
+ * The best sku is the one with the best (cheapest available) offer
9
+ * */
10
+ export const pickBestSku = (skus: Item[]) => {
11
+ const offersBySku = skus.flatMap((sku) =>
12
+ sku.sellers.map((seller) => ({
13
+ offer: seller.commertialOffer,
14
+ sku,
15
+ }))
16
+ )
17
+
18
+ const best = min(offersBySku, ({ offer: o1 }, { offer: o2 }) =>
19
+ bestOfferFirst(o1, o2)
20
+ )
21
+
22
+ return best.sku
23
+ }
24
+
25
+ export const isValidSkuId = (skuId: string) =>
26
+ skuId !== '' && !Number.isNaN(Number(skuId))