[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/itemMetadataPriceTableItem.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import type { Logger } from '@vtex/api'
import { isEmpty, path } from 'ramda'
import { getItemChoiceType, CHOICE_TYPES } from '../../utils/attachments'
import { logDegradedSearchError } from './utils'

interface Params {
  compositionItem: CompositionItem
  simulationPayload: {
    marketingData: Record<string, string>
    countryCode: string
  }
  items: SearchMetadataItem[]
  parent: SearchMetadataItem
  assemblyOption: AssemblyOption
}

// Generate a valid simulation list with this child, considering its a single item type
const getSimulationPayloadItemsForSingleFromTree = (
  parent: SearchMetadataItem,
  childId: string,
  seller: string,
  parentAssemblyBinding: string
) => {
  return [
    { id: parent.id, quantity: 1, seller: parent.seller },
    {
      id: childId,
      seller,
      quantity: 1,
      parentItemIndex: 0,
      parentAssemblyBinding,
    },
  ]
}

// Generate a valid simulation list with this child, considering its a TOGGLE item type
const getSimulationPayloadItemsForToggleFromTree = (
  parent: SearchMetadataItem,
  childId: string,
  seller: string,
  assemblyOption: AssemblyOption,
  parentBasicTree: PayloadItem[]
) => {
  const parentPayloadItem = {
    id: parent.id,
    quantity: 1,
    seller: parent.seller,
  }
  const basicChildItem = {
    id: childId,
    seller,
    quantity: 1,
    parentItemIndex: 0,
    parentAssemblyBinding: assemblyOption.id,
  }
  const siblings = parentBasicTree.filter(
    ({ parentAssemblyBinding }) => parentAssemblyBinding === assemblyOption.id
  )
  const siblingCount = siblings.length
  // See if we can add this item, considering the group max quantity
  if (siblingCount < assemblyOption.composition!.maxQuantity) {
    return [parentPayloadItem, basicChildItem]
  }

  // If there are siblings on the basic tree, remove it and add this child
  const brotherToBeRemoved = siblings.find(item => {
    const brotherComposition = assemblyOption.composition!.items.find(
      comp => comp.id === item.id
    )
    return brotherComposition!.minQuantity === 0
  })
  if (!brotherToBeRemoved) {
    return []
  }
  const withoutBrother = parentBasicTree.filter(
    ({ id, parentAssemblyBinding }) =>
      id !== brotherToBeRemoved.id &&
      parentAssemblyBinding !== brotherToBeRemoved.parentAssemblyBinding
  )
  return [parentPayloadItem, ...withoutBrother, basicChildItem]
}

// Generate a valid simulation list with this child, considering its a MULTIPLE item type
const getSimulationPayloadItemsForMultipleFromTree = (
  parent: SearchMetadataItem,
  assemblyOption: AssemblyOption,
  parentBasicTree: PayloadItem[],
  childCompositionItem: CompositionItem
) => {
  const parentPayloadItem = {
    id: parent.id,
    quantity: 1,
    seller: parent.seller,
  }

  const basicItemQuantity = childCompositionItem.minQuantity || 1
  const familyMinimum = assemblyOption.composition!.minQuantity
  const basicChildItem = {
    id: childCompositionItem.id,
    seller: childCompositionItem.seller,
    quantity: basicItemQuantity,
    parentItemIndex: 0,
    parentAssemblyBinding: assemblyOption.id,
  }

  // Corner case that can be easily solved: when a single child can fill alone the whole group
  if (childCompositionItem.maxQuantity >= familyMinimum) {
    return [
      parentPayloadItem,
      { ...basicChildItem, quantity: familyMinimum || basicItemQuantity },
    ]
  }

  const siblings = parentBasicTree.filter(
    ({ parentAssemblyBinding, parentItemIndex }) =>
      parentAssemblyBinding === assemblyOption.id && parentItemIndex === 0
  )

  const siblingCount = siblings.reduce((sum, sib) => sum + sib.quantity, 0)
  const siblingsAllowed = assemblyOption.composition!.maxQuantity - siblingCount
  if (basicItemQuantity <= siblingsAllowed) {
    return [parentPayloadItem, ...siblings, basicChildItem]
  }

  let currentSiblingCount = siblingCount
  // We will iterate through the childs siblings and remove the minimum we should
  for (const brother of siblings) {
    const brotherComposition = assemblyOption.composition!.items.find(
      comp => comp.id === brother.id
    )
    const oldBrotherQuantity = brother.quantity
    brother.quantity = brotherComposition!.minQuantity
    currentSiblingCount -= oldBrotherQuantity - brotherComposition!.minQuantity
    const currentSiblingsAllowed =
      assemblyOption.composition!.maxQuantity - currentSiblingCount
    if (currentSiblingsAllowed >= basicItemQuantity) {
      // We can now break because we can add out chuld item safely
      break
    }
  }

  const nonEmptySiblings = siblings.filter(({ quantity }) => quantity > 0)
  return [parentPayloadItem, ...nonEmptySiblings, basicChildItem]
}

const simulateAndGetPrice = async (
  payload: SimulationPayload,
  checkout: Context['clients']['checkout'],
  itemId: string,
  assemblyId: string,
  logger: Logger,
) => {
  try {
    const simulation = await checkout.simulation(payload)
    const childInTree = simulation.items.find(
      item => itemId === item.id && assemblyId === item.parentAssemblyBinding
    )
    return childInTree ? childInTree.sellingPrice : 0
  } catch (e) {
    logDegradedSearchError(logger, {
      service: 'Checkout simulation',
      error: `Checkout simulation API returned an error for item ${itemId}.
        Simulation will be skipped for this item and the price of priceTable may be incorrect.`,
      errorStack: e,
    })
    return 0
  }
}

const getSimulationPayloadItems = (
  assemblyOption: AssemblyOption,
  compositionItem: CompositionItem,
  parent: SearchMetadataItem,
  fatherBasicTree: PayloadItem[]
) => {
  const assemblyType = getItemChoiceType(assemblyOption)

  if (assemblyType === CHOICE_TYPES.SINGLE) {
    return getSimulationPayloadItemsForSingleFromTree(
      parent,
      compositionItem.id,
      compositionItem.seller,
      assemblyOption.id
    )
  }
  if (assemblyType === CHOICE_TYPES.TOGGLE) {
    return getSimulationPayloadItemsForToggleFromTree(
      parent,
      compositionItem.id,
      compositionItem.seller,
      assemblyOption,
      fatherBasicTree
    )
  }
  return getSimulationPayloadItemsForMultipleFromTree(
    parent,
    assemblyOption,
    fatherBasicTree,
    compositionItem
  )
}

export const resolvers = {
  PriceTableItem: {
    assemblyId: path(['assemblyOption', 'id']),
    id: path(['compositionItem', 'id']),
    price: async (
      { parent, simulationPayload, assemblyOption, compositionItem }: Params,
      _: any,
      { clients: { checkout }, vtex: { logger } }: Context
    ) => {
      const { id, priceTable } = compositionItem
      const { marketingData, countryCode } = simulationPayload
      const payload = {
        country: countryCode,
        isCheckedIn: false,
        priceTables: [priceTable],
        ...(isEmpty(marketingData) ? {} : { marketingData }),
      }

      const parentBasicTree = [
        { id: parent.id, seller: parent.seller, quantity: 1 },
      ] as PayloadItem[]

      for (const fatherAssemblyOption of parent.assemblyOptions) {
        const assemblyId = fatherAssemblyOption.id
        if (!fatherAssemblyOption.composition) {
          continue
        }
        const groupMinimum = fatherAssemblyOption.composition!.minQuantity
        const itemsInitialsSum = fatherAssemblyOption.composition!.items.reduce(
          (sum, item) => sum + item.initialQuantity,
          0
        )
        // The basic tree will not contain children of this group
        if (groupMinimum !== itemsInitialsSum) {
          continue
        }
        for (const compItem of fatherAssemblyOption.composition!.items) {
          if (compItem.initialQuantity > 0) {
            parentBasicTree.push({
              id: compItem.id,
              quantity: compItem.initialQuantity,
              seller: compItem.seller,
              parentItemIndex: 0,
              parentAssemblyBinding: assemblyId,
            })
          }
        }
      }

      const itemInTree = parentBasicTree.find(
        item =>
          id === item.id && assemblyOption.id === item.parentAssemblyBinding
      )
      // If item is already in tree, just simulate and return the price
      if (itemInTree) {
        return simulateAndGetPrice(
          { ...payload, items: parentBasicTree },
          checkout,
          id,
          assemblyOption.id,
          logger,
        )
      }

      // If not in tree, include it, then simulate and get price
      const newSimulationPayloadItems = getSimulationPayloadItems(
        assemblyOption,
        compositionItem,
        parent,
        parentBasicTree
      )
      return simulateAndGetPrice(
        { ...payload, items: newSimulationPayloadItems },
        checkout,
        id,
        assemblyOption.id,
        logger,
      )
    },
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/skuSpecification.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
export const resolvers = {
  SkuSpecification: {
    values: (skuSpecification: SkuSpecification) => {
      const fieldId = skuSpecification.field.id
      return skuSpecification.values.map(value => ({
        ...value,
        fieldId,
      }))
    },
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/index.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { NotFoundError, UserInputError, createMessagesLoader } from '@vtex/api'
import { pathOr } from 'ramda'

import {
  buildAttributePath,
  convertOrderBy,
} from '../../commons/compatibility-layer'
import { getWorkspaceSearchParamsFromStorage } from '../../routes/workspaceSearchParams'
import {
  fetchAutocompleteSuggestions,
  fetchCorrection,
  fetchSearchSuggestions,
  fetchTopSearches,
} from '../../services/autocomplete'
import { fetchBanners } from '../../services/banners'
import { fetchFacets } from '../../services/facets'
import {
  ProductArgs,
  ProductIdentifier,
  ProductsByIdentifierArgs,
  resolveProduct,
  resolveProductsByIdentifier
} from '../../services/product'
import { fetchProductSearch } from '../../services/productSearch'
import { fetchAppSettings } from '../../services/settings'
import { AdvertisementOptions, FacetsInput, ProductSearchInput, ProductsInput, SuggestionProductsArgs } from '../../typings/Search'
import { shouldTranslateToTenantLocale } from '../../utils/i18n'
import { resolvers as assemblyOptionResolvers } from './assemblyOption'
import { resolvers as autocompleteResolvers } from './autocomplete'
import { resolvers as brandResolvers } from './brand'
import { resolvers as categoryResolvers } from './category'
import { MAP_VALUES_SEP, PATH_SEPARATOR } from './constants'
import { resolvers as discountResolvers } from './discount'
import { resolvers as itemMetadataResolvers } from './itemMetadata'
import { resolvers as itemMetadataPriceTableItemResolvers } from './itemMetadataPriceTableItem'
import { resolvers as itemMetadataUnitResolvers } from './itemMetadataUnit'
import { getSearchMetaData } from './modules/metadata'
import { toCompatibilityArgs } from './newURLs'
import { resolvers as offerResolvers } from './offer'
import { resolvers as productResolvers } from './product'
import { resolvers as productPriceRangeResolvers } from './productPriceRange'
import { resolvers as recommendationResolvers } from './recommendation'
import { resolvers as searchBreadcrumbResolvers } from './searchBreadcrumb'
import { resolvers as skuResolvers } from './sku'
import { resolvers as skuSpecificationResolver } from './skuSpecification'
import { resolvers as skuSpecificationFieldResolver } from './skuSpecificationField'
import { resolvers as skuSpecificationValueResolver } from './skuSpecificationValue'
import {
  SearchCrossSellingTypes,
  getMapAndPriceRangeFromSelectedFacets,
  getShippingOptionsFromSelectedFacets,
  validMapAndQuery,
} from './utils'

enum CrossSellingInput {
  view = 'view',
  buy = 'buy',
  similars = 'similars',
  viewAndBought = 'viewAndBought',
  suggestions = 'suggestions',
  accessories = 'accessories',
}

enum CrossSellingGroupByInput {
  PRODUCT = 'PRODUCT',
  NONE = 'NONE',
}

// Legacy search format is our search with path?map=c,c,specificationFilter
// Where it has specificationFilters and all segments in path are mapped in `map` querystring
const isLegacySearchFormat = ({
  query,
  map,
}: {
  query: string
  map?: string
}) => {
  if (!map) {
    return false
  }

  return map.split(MAP_VALUES_SEP).length === query.split(PATH_SEPARATOR).length
}

export const getCompatibilityArgs = async <T extends QueryArgs>(
  ctx: Context,
  args: T
) => {
  const {
    clients: { vbase, search },
  } = ctx

  const compatArgs = isLegacySearchFormat(args)
    ? args
    : await toCompatibilityArgs(vbase, search, args)

  const formattedArgs = compatArgs ? { ...args, ...compatArgs } : args

  // VTEX search does not understand brand/category/department and fails with error:
  //  Query contains something not suited for search
  formattedArgs.map = formattedArgs.map
    ?.replace(/brand/g, 'b')
    ?.replace(/category(-[0-9]*)?/g, 'c')

  return formattedArgs
}

interface ProductRecommendationArg {
  identifier?: ProductIdentifier
  type?: CrossSellingInput
  groupBy?: CrossSellingGroupByInput
}

const inputToSearchCrossSelling = {
  [CrossSellingInput.buy]: SearchCrossSellingTypes.whoboughtalsobought,
  [CrossSellingInput.view]: SearchCrossSellingTypes.whosawalsosaw,
  [CrossSellingInput.similars]: SearchCrossSellingTypes.similars,
  [CrossSellingInput.viewAndBought]: SearchCrossSellingTypes.whosawalsobought,
  [CrossSellingInput.accessories]: SearchCrossSellingTypes.accessories,
  [CrossSellingInput.suggestions]: SearchCrossSellingTypes.suggestions,
}

/**
 * There is an URL pattern in VTEX where the number of mapSegments doesn't match the number of querySegments. This function deals with these cases.
 * Since this should not be a search concern, this function will be removed soon.
 *
 * @param {Context} ctx
 * @param {QueryArgs} args
 * @returns
 */
const getCompatibilityArgsFromSelectedFacets = async (
  ctx: Context,
  args: QueryArgs
) => {
  const { selectedFacets, query } = args

  if (!selectedFacets || selectedFacets.length === 0 || !query) {
    return args
  }

  const [map, priceRange] = getMapAndPriceRangeFromSelectedFacets([
    ...selectedFacets,
  ])

  args.map = map

  if (isLegacySearchFormat({ query: args.query, map: args.map })) {
    return args
  }

  const compatibilityArgs = (await getCompatibilityArgs(ctx, args)) as QueryArgs

  const mapSegments = compatibilityArgs.map!.split(MAP_VALUES_SEP)
  const querySegments = compatibilityArgs.query!.split(PATH_SEPARATOR)

  args.selectedFacets = mapSegments.map((map, index) => {
    return {
      key: map,
      value: querySegments[index],
    }
  })

  if (priceRange) {
    args.selectedFacets.push({
      key: 'priceRange',
      value: priceRange,
    })
  }

  return args
}

const translateToStoreDefaultLanguage = async (
  ctx: Context,
  term: string
): Promise<string> => {
  const {
    clients,
    state,
    vtex: { locale: from, tenant },
  } = ctx

  const { locale: to } = tenant!

  if (!shouldTranslateToTenantLocale(ctx)) {
    // Do not translate if string already in correct language
    return term
  }

  if (!state.messagesTenantLanguage) {
    state.messagesTenantLanguage = createMessagesLoader(clients, to)
  }

  return state.messagesTenantLanguage!.load({
    from: from!,
    content: term,
  })
}

const noop = () => {}

// Does prefetching and warms up cache for up to the 10 first elements of a search, so if user clicks on product page
const searchFirstElements = (
  products: SearchProduct[],
  from: number | null = 0,
  search: Context['clients']['search']
) => {
  if (from !== 0 || from == null) {
    // We do not want this for pages other than the first
    return
  }

  products
    .slice(0, Math.min(10, products.length))
    .forEach((product) =>
      search
        .productById(product.productId, undefined, undefined, false)
        .catch(noop)
    )
}

export const fieldResolvers = {
  ...autocompleteResolvers,
  ...brandResolvers,
  ...categoryResolvers,
  ...itemMetadataResolvers,
  ...itemMetadataUnitResolvers,
  ...itemMetadataPriceTableItemResolvers,
  ...offerResolvers,
  ...discountResolvers,
  ...productResolvers,
  ...recommendationResolvers,
  ...skuResolvers,
  ...skuSpecificationResolver,
  ...skuSpecificationFieldResolver,
  ...skuSpecificationValueResolver,
  ...assemblyOptionResolvers,
  ...productPriceRangeResolvers,
  ...searchBreadcrumbResolvers,
}

const getTranslatedSearchTerm = async (
  query: SearchArgs['query'],
  map: SearchArgs['map'],
  ctx: Context
) => {
  if (!query || !map || !shouldTranslateToTenantLocale(ctx)) {
    return query
  }

  const ftSearchIndex = map.split(',').findIndex((m) => m === 'ft')

  if (ftSearchIndex === -1) {
    return query
  }

  const queryArray = query.split('/')
  const queryUnit = queryArray[ftSearchIndex]
  const translated = await translateToStoreDefaultLanguage(ctx, queryUnit)
  const queryTranslated = [
    ...queryArray.slice(0, ftSearchIndex),
    translated,
    ...queryArray.slice(ftSearchIndex + 1),
  ]

  return queryTranslated.join('/')
}

const buildSpecificationFiltersAsFacets = (
  specificationFilters: string[]
): SelectedFacet[] => {
  return specificationFilters.map((specificationFilter: string) => {
    const [key, value] = specificationFilter.split(':')

    return { key, value }
  })
}

const buildCategoriesAndSubcategoriesAsFacets = (
  categories: string
): SelectedFacet[] => {
  const categoriesAndSubcategories = categories.split('/')

  return categoriesAndSubcategories.map((c: string) => {
    return { key: 'c', value: c }
  })
}

const buildSelectedFacets = (args: SearchArgs) => {
  const selectedFacets: SelectedFacet[] = []

  if (args.priceRange) {
    selectedFacets.push({ key: 'priceRange', value: args.priceRange })
  }

  if (args.category) {
    selectedFacets.push(
      ...buildCategoriesAndSubcategoriesAsFacets(args.category)
    )
  }

  if (args.collection) {
    selectedFacets.push({ key: 'productClusterIds', value: args.collection })
  }

  if (args.specificationFilters) {
    selectedFacets.push(
      ...buildSpecificationFiltersAsFacets(args.specificationFilters)
    )
  }

  return selectedFacets
}

const defaultAdvertisementOptions: AdvertisementOptions = {
  showSponsored: false,
  sponsoredCount: 3,
  repeatSponsoredProducts: true,
}

export const queries = {
  autocomplete: async (
    _: any,
    args: { maxRows: number; searchTerm?: string },
    ctx: Context
  ) => {
    const {
      clients: { search },
    } = ctx

    if (!args.searchTerm) {
      throw new UserInputError('No search term provided')
    }

    const translatedTerm = await translateToStoreDefaultLanguage(
      ctx,
      args.searchTerm
    )

    const { itemsReturned } = await search.autocomplete({
      maxRows: args.maxRows,
      searchTerm: translatedTerm,
    })

    return {
      cacheId: args.searchTerm,
      itemsReturned,
    }
  },
  facets: async (_: any, args: FacetsInput, ctx: any) => {
    const [shippingOptions, facets] = getShippingOptionsFromSelectedFacets(
      args.selectedFacets
    )

    args.selectedFacets = facets

    args = (await getCompatibilityArgsFromSelectedFacets(
      ctx,
      args
    )) as FacetsInput

    const { selectedFacets } = args

    return fetchFacets(ctx, { args, selectedFacets, shippingOptions })
  },

  product: async (_: any, rawArgs: ProductArgs, ctx: Context) => {
    const product = await resolveProduct(ctx, rawArgs)

    if (!product) {
      const identifier = rawArgs?.identifier || {
        field: 'slug',
        value: rawArgs.slug!,
      }

      throw new NotFoundError(
        `No product was found with requested ${
          identifier.field
        } ${JSON.stringify({ identifier })}`
      )
    }

    return product
  },

  products: async (_: any, args: ProductsInput, ctx: Context) => {
    const {
      clients: { intelligentSearchApi },
    } = ctx

    const {
      to,
      orderBy,
      advertisementOptions = defaultAdvertisementOptions,
    } = args

    if (to && to > 2500) {
      throw new UserInputError(
        `The maximum value allowed for the 'to' argument is 2500`
      )
    }

    const selectedFacets: SelectedFacet[] = buildSelectedFacets(args)
    const workspaceSearchParams = await getWorkspaceSearchParamsFromStorage(ctx)

    const biggyArgs = {
      ...advertisementOptions,
      ...args,
      sort: convertOrderBy(orderBy),
      ...workspaceSearchParams,
    }

    // unnecessary field. It's is an object and breaks the @vtex/api cache
    delete biggyArgs.selectedFacets

    const result = await intelligentSearchApi.productSearch(
      biggyArgs,
      buildAttributePath(selectedFacets),
      args.shippingOptions
    )

    if (ctx.vtex.tenant) {
      ctx.translated = result.translated
    }

    return result.products
  },

  productsByIdentifier: async (
    _: any,
    args: ProductsByIdentifierArgs,
    ctx: Context
  ) => {
    const products = await resolveProductsByIdentifier(ctx, args)

    if (products.length > 0) {
      return products
    }

    throw new NotFoundError(
      `No products were found with requested ${args.field}`
    )
  },

  productSearch: async (_: any, args: ProductSearchInput, ctx: any) => {
    const [shippingOptions, facets] = getShippingOptionsFromSelectedFacets(
      args.selectedFacets
    )

    args.selectedFacets = facets

    args = (await getCompatibilityArgsFromSelectedFacets(
      ctx,
      args
    )) as ProductSearchInput

    if (!validMapAndQuery(args.query, args.map)) {
      ctx.vtex.logger.warn({
        message: 'Invalid map or query',
        query: args.query,
        map: args.map,
      })
    }

    const { selectedFacets } = args

    return fetchProductSearch(ctx, args, selectedFacets, shippingOptions)
  },

  sponsoredProducts: async (_: any, args: ProductSearchInput, ctx: any) => {
    const [shippingOptions, facets] = getShippingOptionsFromSelectedFacets(
      args.selectedFacets
    )

    args.selectedFacets = facets

    args = (await getCompatibilityArgsFromSelectedFacets(
      ctx,
      args
    )) as ProductSearchInput

    if (!validMapAndQuery(args.query, args.map)) {
      ctx.vtex.logger.warn({
        message: 'Invalid map or query',
        query: args.query,
        map: args.map,
      })
    }

    const { intelligentSearchApi } = ctx.clients
    const { selectedFacets, fullText } = args

    const workspaceSearchParams = await getWorkspaceSearchParamsFromStorage(ctx)

    const biggyArgs: { [key: string]: any } = {
      ...args,
      query: fullText,
      sort: convertOrderBy(args.orderBy),
      ...args.options,
      ...workspaceSearchParams,
    }

    // unnecessary field. It's is an object and breaks the @vtex/api cache
    delete biggyArgs.selectedFacets

    const result = await intelligentSearchApi.sponsoredProducts(
      { ...biggyArgs },
      buildAttributePath(selectedFacets),
      shippingOptions
    )

    if (ctx.vtex.tenant && !args.productOriginVtex) {
      ctx.translated = result.translated
    }

    return result
  },

  productRecommendations: async (
    _: any,
    { identifier, type, groupBy }: ProductRecommendationArg,
    ctx: Context
  ) => {
    if (identifier == null || type == null) {
      throw new UserInputError('Wrong input provided')
    }

  const { shouldUseNewPDPEndpoint } = await fetchAppSettings(ctx)
    const searchType = inputToSearchCrossSelling[type]
    let productId = identifier.value

    if (identifier.field !== 'id') {
      const product = await queries.product(_, { identifier }, ctx)

      productId = product!.productId
    }

    const groupByProduct =
      groupBy === CrossSellingGroupByInput.PRODUCT ? true : false

    if (shouldUseNewPDPEndpoint) {
      ctx.translated = true
    }

    const products = await ctx.clients.search.crossSelling(
      productId,
      searchType,
      groupByProduct,
      shouldUseNewPDPEndpoint ? ctx.vtex.locale : undefined
    )

    searchFirstElements(products, 0, ctx.clients.search)
    // We add a custom cacheId because these products are not exactly like the other products from search apis.
    // Each product is basically a SKU and you may have two products in response with same ID but each one representing a SKU.

    return products.map((product) => {
      const skuId = pathOr('', ['items', '0', 'itemId'], product)

      return {
        ...product,
        cacheId: `${product.linkText}-${skuId}`,
      }
    })
  },

  searchMetadata: async (_: any, args: SearchMetadataArgs, ctx: Context) => {
    if (args.selectedFacets) {
      const { maps, queries } = args.selectedFacets.reduce(
        (acc, { key, value }) => {
          if (key !== 'region-id') {
            acc.maps.push(key)
            acc.queries.push(value)
          }

          return acc
        },
        { maps: [] as string[], queries: [] as string[] }
      )

      const map = maps.join(',')
      const query = queries.join('/')

      args.map = map
      args.query = args.query || query || undefined
    }

    const query = await getTranslatedSearchTerm(
      args.query ?? '',
      args.map ?? '',
      ctx
    )

    const translatedArgs = {
      ...args,
      query,
    }

    const compatibilityArgs = await getCompatibilityArgs<SearchArgs>(
      ctx,
      translatedArgs as SearchArgs
    )

    return getSearchMetaData(_, compatibilityArgs, ctx)
  },
  topSearches: (_: any, __: any, ctx: Context) => {
    return fetchTopSearches(ctx)
  },
  autocompleteSearchSuggestions: (
    _: any,
    args: { fullText: string },
    ctx: Context
  ) => {
    return fetchAutocompleteSuggestions(ctx, args.fullText)
  },
  productSuggestions: async (
    _: any,
    args: SuggestionProductsArgs,
    ctx: Context
  ) => {
    const {
      clients: { intelligentSearchApi },
    } = ctx

    const workspaceSearchParams = await getWorkspaceSearchParamsFromStorage(ctx)
    const selectedFacets: SelectedFacet[] =
      args.facetKey && args.facetValue
        ? [{ key: args.facetKey, value: args.facetValue }]
        : []

    const { advertisementOptions = defaultAdvertisementOptions } = args

    const biggyArgs: { [key: string]: any } = {
      ...advertisementOptions,
      ...args,
      query: args.fullText,
      from: 0,
      to: args.count ? args.count - 1 : 4,
      sort: convertOrderBy(args.orderBy),
      allowRedirect: false, // When there is a redirect, no product is returned.
      ...workspaceSearchParams,
    }

    // unnecessary field. It's is an object and breaks the @vtex/api cache
    delete biggyArgs.selectedFacets

    const result = await intelligentSearchApi.productSearch(
      biggyArgs,
      buildAttributePath(selectedFacets),
      args.shippingOptions
    )

    if (ctx.vtex.tenant && !args.productOriginVtex) {
      ctx.translated = result.translated
    }

    return {
      ...result,
      count: result.recordsFiltered,
    }
  },
  banners: (
    _: any,
    args: { fullText: string; selectedFacets: SelectedFacet[] },
    ctx: Context
  ) => {
    return fetchBanners(ctx, args)
  },
  correction: (_: any, args: { fullText: string }, ctx: Context) => {
    return fetchCorrection(ctx, args.fullText)
  },
  searchSuggestions: (_: any, args: { fullText: string }, ctx: Context) => {
    return fetchSearchSuggestions(ctx, args.fullText)
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/assemblyOption.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
export const resolvers = {
  AssemblyOption: {
    inputValues: (
      { inputValues }: AssemblyOption,
    ): InputValue[] => {
      return Object.keys(inputValues).reduce<InputValue[]>((acc, label) => {
        const inputValue = inputValues[label]
        const type = defineInputValueType(inputValue)

        const maxLength = type === InputValueType.TEXT
          ? inputValue.maximumNumberOfCharacters : undefined

        const domain = type === InputValueType.OPTIONS
          ? inputValue.domain
          : undefined

        const defaultValue = type === InputValueType.OPTIONS
          ? inputValue.domain[0]
          : type === InputValueType.BOOLEAN
            ? stringToBoolean(inputValue.domain[0])
            : ''

        acc.push({
          label,
          type,
          maxLength,
          defaultValue,
          domain,
        })

        return acc
      }, [])
    }
  },
}

function defineInputValueType(inputValue: RawInputValue): InputValueType {
  const domain = inputValue.domain

  // If domain is something like ['true', 'false'] or ['false', 'true']
  if (
    domain && domain.length === 2 &&
    domain.indexOf('true') !== -1 && domain.indexOf('false') !== -1
  ) {
    return InputValueType.BOOLEAN
  }

  // If domain is something like ['Sans serif', 'Roman']
  if (domain && domain.length > 0) {
    return InputValueType.OPTIONS
  }

  return InputValueType.TEXT
}

function stringToBoolean(value: string) {
  return value === 'true'
    ? true
    : false
}

interface InputValue {
  label: string
  maxLength?: number
  type: InputValueType
  defaultValue: string|boolean
  domain?: string[]
}

enum InputValueType {
  TEXT = 'TEXT',
  BOOLEAN = 'BOOLEAN',
  OPTIONS = 'OPTIONS',
}
```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/offer.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { propOr } from 'ramda'

const InstallmentsCriteria = {
  ALL: 'ALL',
  MAX: 'MAX',
  MIN: 'MIN',
  MAX_WITHOUT_INTEREST: 'MAX_WITHOUT_INTEREST',
  MAX_WITH_INTEREST: 'MAX_WITH_INTEREST',
}

const gte = (a: number, b: number) => a > b
const lte = (a: number, b: number) => a < b

const includesCaseInsensitive = (list: string[], item: string) => {
  const normalizedList = list.map(j => String(j).toUpperCase())

  return normalizedList.indexOf(String(item).toUpperCase()) > -1
}

export const resolvers = {
  Offer: {
    Installments: (
      { Installments }: CommertialOffer,
      {
        criteria,
        rates,
        excludedPaymentSystems,
        includedPaymentSystems,
      } : {
        criteria?: string
        rates?: boolean
        excludedPaymentSystems?: string[]
        includedPaymentSystems?: string[]
      }
    ) => {
      if (criteria === InstallmentsCriteria.ALL || Installments.length === 0) {
        return Installments
      }

      /** TODO: Transforms arguments for backwards-compatibility. Should be cleaned up in the future */
      if (criteria === InstallmentsCriteria.MAX_WITH_INTEREST || criteria === InstallmentsCriteria.MAX_WITHOUT_INTEREST){
        rates = criteria === InstallmentsCriteria.MAX_WITHOUT_INTEREST
        criteria = InstallmentsCriteria.MAX
      }

      let filteredInstallments = !rates
        ? Installments
        : Installments.filter(({ InterestRate }) => !InterestRate)

      if (includedPaymentSystems) {
        filteredInstallments = filteredInstallments.filter(
          ({ PaymentSystemName }) =>
            includesCaseInsensitive(includedPaymentSystems, PaymentSystemName)
        )
      }

      if (excludedPaymentSystems) {
        filteredInstallments = filteredInstallments.filter(
          ({ PaymentSystemName }) =>
            (includesCaseInsensitive(excludedPaymentSystems, PaymentSystemName) === false))
      }

      const compareFunc = criteria === InstallmentsCriteria.MAX ? gte : lte
      const value = filteredInstallments.reduce(
        (acc, currentValue) =>
          compareFunc(
            currentValue.NumberOfInstallments,
            acc.NumberOfInstallments
          )
            ? currentValue
            : acc,
        filteredInstallments[0]
      )
      return [value]
    },
    teasers: (offer: CommertialOffer) => {
      return offer.teasers ?? offer.Teasers ?? []
    },
    giftSkuIds: propOr([], 'GiftSkuIds'),
    gifts: async ({ GiftSkuIds }: CommertialOffer, _: any, ctx: Context) => {
      if (GiftSkuIds.length === 0) {
        return []
      }

      const catalogGiftProducts = await ctx.clients.search.productsBySku(GiftSkuIds).catch(() => null)

      if (!catalogGiftProducts || catalogGiftProducts.length === 0) {
        return []
      }

      const giftProducts = GiftSkuIds.map(skuId => {
        let giftSku = null
        let giftProduct = null

        for (const currGiftProduct of catalogGiftProducts) {
          const currGiftSku = currGiftProduct.items.find(item => item.itemId === skuId)
          if (currGiftSku) {
            giftSku = currGiftSku
            giftProduct = currGiftProduct
            break
          }
        }
        if (!giftProduct || !giftSku) {
          return null
        }
        const {
          productName,
          brand,
          linkText,
          description,
        } = giftProduct
        return {
          productName,
          brand,
          linkText,
          description,
          skuName: giftSku?.nameComplete ?? '',
          images: giftSku?.images.map(({ imageLabel, imageUrl, imageText }) => ({
            imageUrl,
            imageLabel,
            imageText,
          })) ?? [],
        }
      })
      const filteredGiftProducts = giftProducts.filter(Boolean)

      return filteredGiftProducts
    },
    discountHighlights: (offer: CommertialOffer) => {
      return offer.DiscountHighLight || offer.discountHighlights || []
    },
    spotPrice: (offer: CommertialOffer) => {
      if (offer.spotPrice) {
        return offer.spotPrice
      }
      const sellingPrice = offer.Price
      const spotPrice: number | undefined = offer.Installments.find(({NumberOfInstallments, Value}) => {
        return (NumberOfInstallments === 1 && Value < sellingPrice)
      })?.Value;
      return spotPrice || sellingPrice
    },
    taxPercentage: (offer: CommertialOffer) => {
      if (!offer.Price) {
        return 0
      }

      return offer.Tax / offer.Price
    }
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/productPriceRange.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
interface Root {
  offers: CommertialOffer[]
}

const getMaxAndMinForAttribute = (
  offers: CommertialOffer[],
  attribute: 'Price' | 'ListPrice'
) => {
  return offers.reduce(
    (acc, currentOffer) => {
      const highPrice =
        currentOffer[attribute] > acc.highPrice
          ? currentOffer[attribute]
          : acc.highPrice
      const lowPrice =
        currentOffer[attribute] < acc.lowPrice && currentOffer[attribute] > 0
          ? currentOffer[attribute]
          : acc.lowPrice
      return { highPrice, lowPrice }
    },
    { highPrice: 0, lowPrice: Infinity }
  )
}

export const resolvers = {
  ProductPriceRange: {
    sellingPrice: ({ offers }: Root) => {
      return getMaxAndMinForAttribute(offers, 'Price')
    },
    listPrice: ({ offers }: Root) => {
      return getMaxAndMinForAttribute(offers, 'ListPrice')
    },
  },
  PriceRange: {
    lowPrice: ({ lowPrice }: any) => lowPrice !== Infinity ? lowPrice : null
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/productPriceRange.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { resolvers } from './product'
import { resolvers as productPriceResolvers } from './productPriceRange'
import { mockContext } from '../../__mocks__/helpers'
import { productPriceRange } from '../../__mocks__/productPriceRange'
import { clone } from 'ramda'

describe('tests related to ProductPriceRange type', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    mockContext.vtex.platform = 'vtex'
  })

  test('gets correct value for product with many skus and prices', () => {
    const { offers } = resolvers.Product.priceRange(
      productPriceRange as any
    ) as { offers: CommertialOffer[] }

    const sellingPrice = productPriceResolvers.ProductPriceRange.sellingPrice({
      offers,
    })
    expect(sellingPrice).toMatchObject({ highPrice: 100, lowPrice: 20 })

    const listPrice = productPriceResolvers.ProductPriceRange.listPrice({
      offers,
    })

    expect(listPrice).toMatchObject({ highPrice: 150, lowPrice: 30 })
  })
  test('works for products with many sellers', () => {
    const cloneProduct = clone(productPriceRange)
    cloneProduct.items[0].sellers.push({
      sellerId: '99',
      commertialOffer: {
        Price: 10,
        ListPrice: 160,
        AvailableQuantity: 10,
      },
    } as any)

    const { offers } = resolvers.Product.priceRange(cloneProduct as any) as {
      offers: CommertialOffer[]
    }
    const sellingPrice = productPriceResolvers.ProductPriceRange.sellingPrice({
      offers,
    })
    expect(sellingPrice).toMatchObject({ highPrice: 100, lowPrice: 10 })

    const listPrice = productPriceResolvers.ProductPriceRange.listPrice({
      offers,
    })

    expect(listPrice).toMatchObject({ highPrice: 160, lowPrice: 30 })
  })

  test('ignores sellers with no availability', () => {
    const cloneProduct = clone(productPriceRange)
    cloneProduct.items[0].sellers.push({
      sellerId: '99',
      commertialOffer: {
        Price: 10,
        ListPrice: 10,
        AvailableQuantity: 0,
      },
    } as any)

    const { offers } = resolvers.Product.priceRange(cloneProduct as any) as {
      offers: CommertialOffer[]
    }
    const sellingPrice = productPriceResolvers.ProductPriceRange.sellingPrice({
      offers,
    })
    expect(sellingPrice).toMatchObject({ highPrice: 100, lowPrice: 20 })

    const listPrice = productPriceResolvers.ProductPriceRange.listPrice({
      offers,
    })

    expect(listPrice).toMatchObject({ highPrice: 150, lowPrice: 30 })
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/product.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
/* eslint-disable jest/no-mocks-import */
/* eslint-disable jest/no-identical-title */

import {
  getBindingLocale,
  mockContext,
  resetContext,
} from '../../__mocks__/helpers'
import { getProduct } from '../../__mocks__/product'
import productSpecs from '../../__mocks__/productSpecs'
import { resolvers } from './product'

describe('tests related to product resolver', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    resetContext()
  })
  describe('categoryTree resolver', () => {
    it('ensure that VTEX account never calls the category tree search API', async () => {
      const searchProduct = getProduct()

      await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )
      expect(mockContext.clients.search.category).toBeCalledTimes(2)
      expect(mockContext.clients.search.categories).toBeCalledTimes(0)
    })

    it('get correct main category tree for product with only one tree', async () => {
      const searchProduct = getProduct()

      await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )
      expect(mockContext.clients.search.category).toBeCalledTimes(2)
      expect(mockContext.clients.search.category.mock.calls[0][0]).toBe(25)
      expect(mockContext.clients.search.category.mock.calls[1][0]).toBe(10)
    })

    it('get correct main category tree for product with more than one tree', async () => {
      const categoriesIds = [
        '/101/101003/101003009/',
        '/101/101003/',
        '/101/',
        '/101/101019/101019004/',
        '/101/101019/',
        '/103/103023/103023003/',
        '/103/103023/',
        '/103/',
      ]

      const searchProduct = getProduct({
        categoriesIds,
        categoryId: '101003009',
      })

      await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )
      expect(mockContext.clients.search.category).toBeCalledTimes(3)
      expect(mockContext.clients.search.category.mock.calls[0][0]).toBe(101)
      expect(mockContext.clients.search.category.mock.calls[1][0]).toBe(101003)
      expect(mockContext.clients.search.category.mock.calls[2][0]).toBe(
        101003009
      )
    })

    it('ensure that GC account calls the category tree API', async () => {
      const searchProduct = getProduct()

      mockContext.vtex.platform = 'gocommerce'
      mockContext.vtex.logger = { info: jest.fn() } as any
      mockContext.clients.search.categories.mockImplementation(() => [
        {
          id: '10',
          name: 'a',
          url: 'a',
          children: [],
        },
        {
          id: '25',
          name: 'a',
          url: 'a',
          children: [],
        },
      ])
      const result = await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )

      expect(mockContext.clients.search.category).toBeCalledTimes(0)
      expect(mockContext.clients.search.categories).toBeCalledTimes(1)
      expect(mockContext.clients.search.categories.mock.calls[0][0]).toBe(2) // ensure maximum level was correct
      expect(result!).toHaveLength(2)
    })

    it('if categoryId does not match any id in categoriesIds, find biggest tree', async () => {
      const searchProduct = getProduct()

      searchProduct.categoryId = '1'
      searchProduct.categoriesIds = ['/2064927469/', '/2064927469/630877787/']
      await resolvers.Product.categoryTree(
        searchProduct as any,
        {},
        mockContext as any
      )
      expect(mockContext.clients.search.category).toBeCalledTimes(2)
      expect(mockContext.clients.search.category.mock.calls[0][0]).toBe(
        2064927469
      )
      expect(mockContext.clients.search.category.mock.calls[1][0]).toBe(
        630877787
      )
    })
  })

  it('if custom cacheId is given, it should be returned', async () => {
    const searchProduct = getProduct()
    const cacheAsSlug = await resolvers.Product.cacheId(searchProduct as any)

    expect(cacheAsSlug).toBe(searchProduct.linkText)

    searchProduct.cacheId = 'CUSTOM_CACHE'

    const customCache = await resolvers.Product.cacheId(searchProduct as any)

    expect(customCache).toBe('CUSTOM_CACHE')
  })

  it('productClusters should be properly formatted', () => {
    const product = getProduct()

    product.productClusters = {
      '140': 'Casual Footwear',
      '195': 'All products',
      '201': 'Third',
    }
    const result = resolvers.Product.productClusters(product as any)

    expect(result).toMatchObject([
      { id: '140', name: 'Casual Footwear' },
      { id: '195', name: 'All products' },
      { id: '201', name: 'Third' },
    ])
  })

  it('productClusters should not break if value is null', () => {
    const product = getProduct()

    product.productClusters = null as any
    const result = resolvers.Product.productClusters(product as any)

    expect(result).toMatchObject([])
  })

  it('clusterHighlights should be properly formatted', () => {
    const product = getProduct()

    product.clusterHighlights = {
      '140': 'Casual Footwear',
      '195': 'All products',
      '201': 'Third',
    }
    const result = resolvers.Product.clusterHighlights(product as any)

    expect(result).toMatchObject([
      { id: '140', name: 'Casual Footwear' },
      { id: '195', name: 'All products' },
      { id: '201', name: 'Third' },
    ])
  })

  it('clusterHighlights should not break if value is null', () => {
    const product = getProduct()

    product.clusterHighlights = null as any
    const result = resolvers.Product.clusterHighlights(product as any)

    expect(result).toMatchObject([])
  })

  it('clusterHighlights should be properly formatted', () => {
    const product = getProduct()

    product.clusterHighlights = {
      '140': 'Casual Footwear',
      '195': 'All products',
      '201': 'Third',
    }
    const result = resolvers.Product.clusterHighlights(product as any)

    expect(result).toMatchObject([
      { id: '140', name: 'Casual Footwear' },
      { id: '195', name: 'All products' },
      { id: '201', name: 'Third' },
    ])
  })

  it('clusterHighlights should not break if value is null', () => {
    const product = getProduct()

    product.clusterHighlights = null as any
    const result = resolvers.Product.clusterHighlights(product as any)

    expect(result).toMatchObject([])
  })

  describe('linkText resolver', () => {
    it('linkText with binding with different locales', async () => {
      const product = getProduct()

      mockContext.vtex.binding.locale = 'fr-FR'
      mockContext.clients.rewriter.getRoute.mockImplementationOnce(
        (id: string, type: string, bindingId: string) =>
          Promise.resolve(`/${id}-${type}-${bindingId}-${getBindingLocale()}/p`)
      )
      const result = await resolvers.Product.linkText(
        product as any,
        {},
        mockContext as any
      )

      expect(result).toBe('16-product-abc-fr-FR')
    })
    it('linkText for same binding language', async () => {
      const product = getProduct()
      const result = await resolvers.Product.linkText(
        product as any,
        {},
        mockContext as any
      )

      expect(result).toBe(product.linkText)
    })
  })

  describe('specificationGroups resolver', () => {
    it('specifications groups should have expected format but not translated if same locale as tenant', async () => {
      const product = getProduct()
      const result = (await resolvers.Product.specificationGroups(
        product as any,
        {},
        mockContext as any
      )) as any

      expect(result[0]).toMatchObject({
        name: 'Tamanho',
        specifications: [{ name: 'Numero do calçado', values: ['35'] }],
      })
      expect(result[1]).toMatchObject({
        name: 'allSpecifications',
        specifications: [{ name: 'Numero do calçado', values: ['35'] }],
      })
    })

    it('specifications groups should have expected format and translated values', async () => {
      mockContext.vtex.locale = 'fr-FR'
      const product = getProduct()

      mockContext.clients.search.filtersInCategoryFromId.mockImplementationOnce(
        () => [
          {
            Name: 'Numero do calçado',
            FieldId: 'specification-Id',
          },
        ]
      )
      const result = (await resolvers.Product.specificationGroups(
        product as any,
        {},
        mockContext as any
      )) as any

      expect(result[0]).toMatchObject({
        name: 'Tamanho (((16))) <<<pt-BR>>> [[[original]]]',
        specifications: [
          {
            name: 'Numero do calçado (((specification-Id))) <<<pt-BR>>> [[[original]]]',
            values: ['35 (((specification-Id))) <<<pt-BR>>> [[[original]]]'],
          },
        ],
      })
      expect(result[1]).toMatchObject({
        name: 'allSpecifications (((16))) <<<pt-BR>>> [[[original]]]',
        specifications: [
          {
            name: 'Numero do calçado (((specification-Id))) <<<pt-BR>>> [[[original]]]',
            values: ['35 (((specification-Id))) <<<pt-BR>>> [[[original]]]'],
          },
        ],
      })
    })

    it('should filter specifications that are not visible', async () => {
      const specificationGroups = await resolvers.Product.specificationGroups(
        productSpecs as any,
        {},
        mockContext
      )

      expect(specificationGroups).toHaveLength(5)

      // If the completeSpecifications doesn't give info on the specs, return it
      const cucardas = specificationGroups?.find(
        (group) => group.name === 'Cucardas'
      )

      expect(cucardas).toBeTruthy()
    })
  })

  describe('properties resolver', () => {
    it('properties should have expected format but not translated if same locale as tenant', async () => {
      const product = getProduct()
      const result = await resolvers.Product.properties(
        product as any,
        {},
        mockContext as any
      )

      expect(result).toMatchObject([
        { name: 'Numero do calçado', values: ['35'] },
      ])
    })
    it('specifications groups should have expected format and translated values', async () => {
      mockContext.vtex.locale = 'fr-FR'
      const product = getProduct({
        allSpecifications: ['Numero do calçado', 'testeName'],
        testeName: ['teste value'],
      })

      mockContext.clients.search.filtersInCategoryFromId.mockImplementationOnce(
        () => [
          {
            Name: 'Numero do calçado',
            FieldId: 'specification-Id',
          },
          {
            Name: 'testeName',
            FieldId: 'teste-id',
          },
        ]
      )
      const result = await resolvers.Product.properties(
        product as any,
        {},
        mockContext as any
      )

      expect(result).toMatchObject([
        {
          name: 'Numero do calçado (((specification-Id))) <<<pt-BR>>> [[[original]]]',
          values: ['35 (((specification-Id))) <<<pt-BR>>> [[[original]]]'],
        },
        {
          name: 'testeName (((teste-id))) <<<pt-BR>>> [[[original]]]',
          values: ['teste value (((teste-id))) <<<pt-BR>>> [[[original]]]'],
        },
      ])
    })
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/productSearch.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { path } from 'ramda'
import { IOResponse } from '@vtex/api'
import { Functions } from '@gocommerce/utils'
import { zipQueryAndMap, breadcrumbMapKey, logDegradedSearchError } from './utils'
import { shouldTranslateToBinding } from '../../utils/i18n'

interface ProductSearchParent {
  productsRaw: IOResponse<SearchProduct[]>
  translatedArgs: SearchArgs
  searchMetaData: {
    titleTag: string | null
    metaTagDescription: string | null
  }
}

interface Metadata {
  id: string
  name: string
}

const getTypeForCategory = (index: number) => {
  if (index === 0) {
    return 'department'
  }
  if (index === 1) {
    return 'category'
  }
  return 'subcategory'
}

const getRouteForQueryUnit = async (queryUnit: string, mapUnit: string, categoriesSearched: string[], index: number, ctx: Context) => {
  const bindingId = ctx.vtex.binding!.id!
  const key = `${queryUnit}-${mapUnit}`

  if (mapUnit === 'b') {
    const brandPageType = await ctx.clients.search.pageType(queryUnit, 'map=b')

    if (index === 0) {
      try {
        // if it is a brand page, we should check if there is a route on rewriter for this brand
        const brandFromRewriter = await ctx.clients.rewriter.getRoute(brandPageType.id, 'brand', bindingId)
        if (brandFromRewriter) {
          return { path: brandFromRewriter, key, name: brandPageType.name, id: brandPageType.id }
        }
      } catch (e) {
        logDegradedSearchError(ctx.vtex.logger, {
          service: 'Rewriter getRoute',
          error: `Rewriter getRoute query returned an error for brand ${queryUnit}. Breadcrumb data may be incorrect.`,
          errorStack: e,
        })
      }
    }
    return { path: queryUnit, key, name: brandPageType.name, id: brandPageType.id }
  }

  if (mapUnit === 'c') {
    const categoryPosition = categoriesSearched.findIndex(cat => cat === queryUnit)
    const category = await ctx.clients.search.pageType(categoriesSearched.slice(0, categoryPosition + 1).join('/'))
    try {
      const route = await ctx.clients.rewriter.getRoute(category.id, getTypeForCategory(categoryPosition), bindingId)
      return { path: route ?? queryUnit, key, name: category.name, id: category.id }
    } catch (e) {
      logDegradedSearchError(ctx.vtex.logger, {
        service: 'Rewriter getRoute',
        error: `Rewriter getRoute query returned an error for category ${category.id}. Breadcrumb data may be incorrect.`,
        errorStack: e,
      })
      return { path: queryUnit, key, name: category.name, id: category.id }
    }
  }

  return { path: queryUnit, key, name: null, id: null }
}

const breadcrumbDataWithBinding = async (queryAndMap: [string, string][], categoriesSearched: string[], mapArray: string[], ctx: Context) => {
  const queryTranslationsAndKeys = await Promise.all(queryAndMap.map(([queryUnit, mapUnit], index) => {
    return getRouteForQueryUnit(queryUnit, mapUnit, categoriesSearched, index, ctx)
  }))
  const queryTranslations = queryTranslationsAndKeys.reduce((acc, curr) => {
    acc[curr.key] = curr.path
    return acc
  }, {} as Record<string, string>)

  const metadataMap = queryTranslationsAndKeys.filter(({ name }) => Boolean(name)).reduce((acc, curr) => {
    acc[curr.key] = { name: curr.name!, id: curr.id! }
    return acc
  }, {} as Record<string, Metadata>)

  const indexFirstCategory = mapArray.findIndex(m => m === 'c')
  const hrefs = queryAndMap.reduce((acc, curr) => {
    const [queryUnit, mapUnit] = curr
    const slug = mapUnit === 'c' || mapUnit === 'b' ? queryTranslations[breadcrumbMapKey(queryUnit, mapUnit)] : queryUnit
    let prefix = acc.length > 0 ? acc[acc.length - 1] : ''
    if (mapUnit === 'c') {
      prefix = indexFirstCategory > 0 ? acc[indexFirstCategory - 1] : ''
    }
    const noSlashSlug = slug.startsWith('/') ? slug.slice(1) : slug
    const url = `${prefix}/${noSlashSlug}`
    acc.push(url)
    return acc
  }, [] as string[])

  return { hrefs, metadataMap }
}

export const resolvers = {
  ProductSearch: {
    titleTag: path(['searchMetaData', 'titleTag']),
    metaTagDescription: path(['searchMetaData', 'metaTagDescription']),
    recordsFiltered: ({ productsRaw }: ProductSearchParent) => {
      const {
        headers: { resources },
      } = productsRaw
      const quantity = resources.split('/')[1]
      return parseInt(quantity, 10)
    },
    products: ({ productsRaw }: ProductSearchParent, _: any, ctx: Context) => {
      let hasBroken = false
      for (const product of productsRaw.data) {
        for (const item of product.items) {
          if (item.sellers == null) {
            const config: any = (productsRaw as any).config
            ctx.vtex.logger.error({
              message: 'Item missing sellers!',
              searchUrl: config.url.replace(/\/proxy\/authenticated\/catalog|\/proxy\/catalog/, ''),
              params: JSON.stringify(config.params)
            })
            hasBroken = true
            break
          }
        }
        if (hasBroken) {
          break
        }
      }
      return productsRaw.data
    },
    breadcrumb: async (
      { translatedArgs, productsRaw: { data: products } }: ProductSearchParent,
      _: any,
      ctx: Context
    ) => {
      const { vtex: { account }, clients: { search } } = ctx
      const query = translatedArgs?.query || ''
      const map = translatedArgs?.map || ''
      const queryAndMap = zipQueryAndMap(
        translatedArgs?.query,
        translatedArgs?.map
      )
      const categoriesSearched = queryAndMap
        .filter(([_, m]) => m === 'c')
        .map(([q]) => q)

      const categoriesCount = map.split(',').filter(m => m === 'c').length
      const categories =
        !!categoriesCount && Functions.isGoCommerceAcc(account)
          ? await search.categories(categoriesCount)
          : []

      const queryArray = query.split('/')
      const mapArray = map.split(',')

      const { metadataMap, hrefs } =
        shouldTranslateToBinding(ctx) ?
          await breadcrumbDataWithBinding(queryAndMap, categoriesSearched, mapArray, ctx)
          : { metadataMap: {}, hrefs: null }

      return queryAndMap.map(
        ([queryUnit, mapUnit]: [string, string], index: number) => ({
          queryUnit,
          mapUnit,
          index,
          queryArray,
          mapArray,
          categories,
          categoriesSearched,
          products,
          metadataMap,
          hrefs
        })
      )
    },
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/sku.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { find, head, map, replace, slice } from 'ramda'

import {
  formatTranslatableProp,
  addContextToTranslatableString,
} from '../../utils/i18n'

type Attachment = {
  id: number
  name: string
  required: boolean
  domainValues: DomainValue[]
}

type DomainValue = {
  FieldName: string
  MaxCaracters: string
  DomainValues: string
}

export const resolvers = {
  SKU: {
    name: formatTranslatableProp<SearchItem, 'name', 'itemId'>(
      'name',
      'itemId'
    ),

    nameComplete: formatTranslatableProp<SearchItem, 'nameComplete', 'itemId'>(
      'nameComplete',
      'itemId'
    ),

    attachments: ({ attachments = [] }: SearchItem) => {
      const attachmentsResults: Attachment[] = []

      attachments.forEach((attachment) => {
        // Response from Portal Search
        if (typeof attachment.domainValues === 'string') {
          attachmentsResults.push({
            ...attachment,
            required: attachment.required ?? false,
            domainValues: JSON.parse(attachment.domainValues) as DomainValue[],
          })
        }

        // Response from IS API
        if (attachment.fields) {
          attachmentsResults.push({
            ...attachment,
            required: attachment.isRequired ?? false,
            domainValues: attachment.fields.map((field) => ({
              FieldName: field.fieldName,
              MaxCaracters: field.maxCharacters,
              DomainValues: field.domainValues,
            })),
          })
        }
      })

      return attachmentsResults
    },

    images: ({ images = [] }: SearchItem, { quantity }: { quantity: number }) =>
      map(
        (image) => ({
          cacheId: image.imageId,
          ...image,
          imageUrl: replace('http://', 'https://', image.imageUrl),
        }),
        quantity > 0 ? slice(0, quantity, images) : images
      ),

    kitItems: (
      { kitItems }: SearchItem,
      _: any,
      { clients: { search } }: Context
    ) =>
      !kitItems
        ? []
        : kitItems.map(async (kitItem) => {
            const products = await search.productBySku(kitItem.itemId)
            const { items: skus = [], ...product } = head(products) || {}
            const sku = find(({ itemId }) => itemId === kitItem.itemId, skus)

            return { ...kitItem, product, sku }
          }),

    variations: (sku: SearchItemExtended, _: any, ctx: Context) => {
      if (!sku) {
        return sku
      }

      const variations = (sku.variations || []).map((variationObj) => {
        const variationName =
          typeof variationObj === 'string'
            ? variationObj
            : (variationObj as { name: string }).name

        const fieldId = (sku.skuSpecifications || []).find(
          (specification) => specification.field.name === variationName
        )?.field?.id

        const variationsValues = (sku as any)[variationName] as string[]

        return {
          name: addContextToTranslatableString(
            { content: variationName, context: fieldId },
            ctx
          ),
          values: variationsValues.map((value) =>
            addContextToTranslatableString(
              { content: value, context: fieldId },
              ctx
            )
          ),
        }
      })

      return variations
    },

    videos: ({ Videos, videos }: SearchItem) => {
      const sanitizedVideo = Videos ?? videos

      const formattedVideo = sanitizedVideo.map((video: string) => ({
        videoUrl: video,
      }))

      return formattedVideo
    },
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/benefits/index.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { flatten, indexOf, path } from 'ramda'

const SKU_SEPARATOR = ','
const CATALOG = 'Catalog'
const DEFAULT_SELLER = '1'
const DEFAULT_QUANTITY = 1

export const fieldResolvers = {
  Benefit: {
    items: async (benefit: any, _: any, { clients: { search } }: Context) => {
      const { teaserType, conditions, effects } = benefit

      if (teaserType === CATALOG) {
        const {
          parameters: conditionsParameters,
          minimumQuantity = DEFAULT_QUANTITY,
        } = conditions

        const { parameters: effectsParameters } = effects

        const items = await Promise.all(
          conditionsParameters.map(
            async (conditionsParameter: any, index: any) => {
              const skuIds: string[] = conditionsParameter.value.split(
                SKU_SEPARATOR
              )
              const discount = effectsParameters[index].value
              const products = await search.productsBySku(skuIds)

              return products.map((product: any) => {
                const benefitSKUIds: any = []

                product.items.map((item: any) => {
                  if (indexOf(item.itemId, skuIds) > -1) {
                    benefitSKUIds.push(item.itemId)
                  }
                })

                return {
                  benefitProduct: product,
                  benefitSKUIds,
                  discount,
                  minQuantity: minimumQuantity,
                }
              })
            }
          )
        )
        return flatten(items)
      }
      return
    },
  },
}

export const getBenefits = async (
  itemId: string,
  { clients: { checkout } }: Context
): Promise<unknown[]> => {
  const requestBody = {
    items: [
      {
        id: itemId,
        quantity: DEFAULT_QUANTITY,
        seller: DEFAULT_SELLER,
      },
    ],
  }
  const benefitsData = await checkout.simulation(requestBody)
  return path(['ratesAndBenefitsData', 'teaser'], benefitsData) || []
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/stats/searchURLsCount.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { VBase } from '@vtex/api'
import { VBASE_BUCKET, URL_COUNT_FILE_PATH } from './constants'
import { SearchStatsUrlCount } from './searchStats'

export enum SORT {
  ASC = 'ASC',
  DESC = 'DESC',
}

export const searchURLsCount = async (
  vbase: VBase,
  limit: number,
  sort: SORT = SORT.DESC
) => {
  const data = (await vbase.getJSON<SearchStatsUrlCount>(
    VBASE_BUCKET,
    URL_COUNT_FILE_PATH,
    true
  )) as SearchStatsUrlCount
  let comparator = urlsCountDESCComparator

  if (sort === SORT.ASC) {
    comparator = urlsCountASCComparator
  }

  return (
    (data &&
      Object.entries(data.searchUrlCount)
        .sort(comparator)
        .slice(0, limit)
        .map(searchUrlStat => {
          return { path: searchUrlStat[0], count: searchUrlStat[1] }
        })) ||
    []
  )
}

const urlsCountASCComparator = (
  urlStatsA: [string, number],
  urlStatsB: [string, number]
) => {
  return urlStatsA[1] - urlStatsB[1]
}

const urlsCountDESCComparator = (
  urlStatsA: [string, number],
  urlStatsB: [string, number]
) => {
  return urlStatsB[1] - urlStatsA[1]
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/stats/searchStats.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  PATH_SEPARATOR,
  MAP_QUERY_PARAM,
  MAP_SEPARATOR,
  VBASE_BUCKET,
  URL_COUNT_FILE_PATH,
  INDEXING_UPDATE_FREQUENCY,
  INDEXING_EVENT_ROUTE_NAME,
} from './constants'

export interface SearchStatsUrlCount {
  updatedCount: number
  searchUrlCount: Record<string, number>
}

export const count = async (ctx: Context, args: SearchArgs) => {
  const {
    clients: { vbase },
  } = ctx

  const searchURLPath = `${PATH_SEPARATOR}${args.query}?${MAP_QUERY_PARAM}=${args.map}`
  const pathSegments = args.query && args.query.split(PATH_SEPARATOR)
  const mapSegments = args.map && args.map.split(MAP_SEPARATOR)

  if (pathSegments && mapSegments && pathSegments.length === mapSegments.length) {
    const data = ((await vbase.getJSON<SearchStatsUrlCount>(
      VBASE_BUCKET,
      URL_COUNT_FILE_PATH,
      true
    )) || { updatedCount: 0, searchUrlCount: {} }) as SearchStatsUrlCount

    data.searchUrlCount[searchURLPath] = (data.searchUrlCount[searchURLPath] || 0) + 1
    data.updatedCount = data.updatedCount + 1
    if (data.updatedCount === INDEXING_UPDATE_FREQUENCY) {
      data.updatedCount = 0
      sendIndexSearchUrlsEvent(ctx)
    }

    vbase.saveJSON<SearchStatsUrlCount>(VBASE_BUCKET, URL_COUNT_FILE_PATH, data)
  }
}

const sendIndexSearchUrlsEvent = async (ctx: Context) => {
  const { clients: { events }, vtex: { locale, tenant, binding } } = ctx
  events.sendEvent('', INDEXING_EVENT_ROUTE_NAME, {locale, tenant, binding})
}
```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/stats/constants.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
export const VBASE_BUCKET = 'searchStats'
export const URL_COUNT_FILE_PATH = 'searchUrlsCount'

export const INDEXING_UPDATE_FREQUENCY = 500
export const INDEXING_EVENT_ROUTE_NAME = 'searchUrlsCountIndex'

export const MAP_QUERY_PARAM = 'map'
export const PATH_SEPARATOR = '/'
export const MAP_SEPARATOR = ','
```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/stats/index.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { searchURLsCount, SORT } from "./searchURLsCount"

export const queries = {
  searchURLsCount: async (
    _: any,
    args: { limit: number, sort?: SORT },
    ctx: Context
  ) => {
    const { clients: { vbase } } = ctx
    const count = searchURLsCount(vbase, args.limit, args.sort)
    return count
  },
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-resolver/node/resolvers/search/modules/metadata.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  tail,
  head,
  compose,
  prop,
  join,
  map,
  filter,
  findLastIndex,
  split,
  toLower,
  reverse,
  complement,
  isNil,
} from 'ramda'
import { Functions } from '@gocommerce/utils'

import { zipQueryAndMap, findCategoryInTree, getBrandFromSlug, searchDecodeURI } from '../utils'
import { toTitleCase } from '../../../utils/string'
import { formatTranslatableProp, translateManyToCurrentLanguage, Message, shouldTranslateToUserLocale } from '../../../utils/i18n'

type TupleString = [string, string]

const categoryKeys = ['c', 'category', 'category-1', 'category-2', 'category-3']

const isTupleMap = (t: TupleString) => t[1] ? categoryKeys.includes(t[1]) : false

const getLastCategoryIndex = findLastIndex(isTupleMap)

const categoriesOnlyQuery = compose<
  TupleString[],
  TupleString[],
  string[],
  string
>(
  join('/'),
  map(prop('0')),
  filter(isTupleMap)
)

const getAndParsePagetype = async (path: string, ctx: Context) => {
  const pagetype = await ctx.clients.search.pageType(path).catch((e) => {
    ctx.vtex.logger.error({
      message: 'Failed to call the pageType API.',
      data: e
    })

    return null
  })

  if (!pagetype) {
    return emptyTitleTag
  }

  return {
    titleTag: pagetype.title || pagetype.name,
    metaTagDescription: pagetype.metaTagDescription,
    id: pagetype.id,
  }
}

const getCategoryMetadata = async (
  { map, query }: SearchMetadataArgs,
  ctx: Context
): Promise<SearchMetadata> => {
  const {
    vtex: { account },
    clients: { search },
  } = ctx
  const queryAndMap = zipQueryAndMap(query, map)
  const cleanQuery = categoriesOnlyQuery(queryAndMap)

  if (Functions.isGoCommerceAcc(account)) {
    // GoCommerce does not have pagetype query implemented yet
    const category =
      findCategoryInTree(
        await search.categories(cleanQuery.split('/').length),
        cleanQuery.split('/')
      )
    return {
      id: null,
      metaTagDescription: category?.MetaTagDescription,
      titleTag: category?.Title ?? category?.name,
    }
  }

  return getAndParsePagetype(cleanQuery, ctx)
}

const getBrandMetadata = async (
  query: SearchMetadataArgs['query'],
  ctx: Context
): Promise<SearchMetadata> => {
  const {
    vtex: { account },
    clients: { search },
  } = ctx
  const cleanQuery = head(split('/', query || '')) || ''

  if (Functions.isGoCommerceAcc(account)) {
    const brand = await getBrandFromSlug(toLower(cleanQuery), search)
    return {
      id: null,
      metaTagDescription: brand?.metaTagDescription,
      titleTag: brand?.title ?? brand?.name,
    }
  }
  return getAndParsePagetype(cleanQuery, ctx)
}

export const getSpecificationFilterName = (name: string) => {
  return toTitleCase(searchDecodeURI(decodeURI(name)))
}

const getClusterMetadata = async (
  args: SearchMetadataArgs,
  ctx: Context,
): Promise<SearchMetadata> => {
  const {
    clients: { search }
  } = ctx

  const searchArgs: SearchArgs = Object.assign({}, {
    query: args.query ?? "",
    map: args.map ?? "",
    selectedFacets: args.selectedFacets
  }, {
    category: null,
    specificationFilters: null,
    collection: null,
    salesChannel: null,
    orderBy: null,
    from: null,
    to: null,
    hideUnavailableItems: null,
    simulationBehavior: null,
    completeSpecifications: false,
  })

  const clusterId = head(args.query?.split(',') ?? [])
  const products = await search.products(searchArgs)
  const productWithCluster = products?.find(
    ({ productClusters }) => !!productClusters[clusterId]
  )
  const clusterName = (productWithCluster && productWithCluster.productClusters[clusterId]) || ""

  try {
    return {
      titleTag: decodeURI(clusterName),
      metaTagDescription: null,
    }
  } catch {
    return {
      titleTag: clusterName,
      metaTagDescription: null,
    }
  }
}

const getPrimaryMetadata = (
  args: SearchMetadataArgs,
  ctx: Context
): Promise<SearchMetadata> | SearchMetadata => {
  const map = args.map || ''
  const firstMap = head(map.split(','))
  if (categoryKeys.includes(firstMap)) {
    return getCategoryMetadata(args, ctx)
  }
  if (firstMap === 'b' || firstMap === 'brand') {
    return getBrandMetadata(args.query, ctx)
  }
  if (firstMap === 'productClusterIds') {
    return getClusterMetadata(args, ctx)
  }
  if (firstMap && firstMap.includes('specificationFilter')) {
    const cleanQuery = args.query || ''
    const name = head(cleanQuery.split('/')) || ''
    return {
      titleTag: getSpecificationFilterName(name),
      metaTagDescription: null,
    }
  }
  if (firstMap === 'ft') {
    const cleanQuery = args.query || ''
    const term = head(cleanQuery.split('/')) || ''
    return {
      titleTag: decodeURI(term),
      metaTagDescription: null,
    }
  }
  return emptyTitleTag
}

const getNameForRemainingMaps = async (
  remainingTuples: [string, string][],
  ctx: Context
) => {
  const {
    vtex: { account },
    clients: { search },
  } = ctx
  const lastCategoryIndex = getLastCategoryIndex(remainingTuples)
  const isGC = Functions.isGoCommerceAcc(account)
  const names = await Promise.all(
    remainingTuples.map(async ([query, map], index) => {
      if (map === 'c' && index === lastCategoryIndex && !isGC) {
        const cleanQuery = categoriesOnlyQuery(remainingTuples)
        const pagetype = await search.pageType(cleanQuery).catch(() => null)
        if (pagetype) {
          return pagetype.name
        }
      }
      if ((map === 'b' || map === 'brand') && !isGC) {
        const brand = await search.pageType(decodeURI(query), 'map=b').catch(() => null)
        if (brand) {
          return brand.name
        }
      }
      if (map.includes('specificationFilter')) {
        return getSpecificationFilterName(query)
      }
      return null
    })
  )
  return names
}

export const emptyTitleTag = {
  titleTag: null,
  metaTagDescription: null,
}

type StringNull = string | null | undefined

const removeNulls = <T>(array: (T | null | undefined)[]): T[] => array.filter(Boolean) as T[]

const isNotNil = complement(isNil)
const joinNames = compose<StringNull[], string[], string[], string>(
  join(' - '),
  reverse,
  filter(isNotNil) as any
)

const translateTitles = (metadata: SearchMetadata, otherNames: (string | null)[], ctx: Context) => {
  const messages: Message[] = []
  if (metadata.titleTag) {
    messages.push({ content: metadata.titleTag, context: metadata.id ?? undefined })
  }
  messages.push(...removeNulls(otherNames).map(name => ({ content: name })))
  return translateManyToCurrentLanguage(messages, ctx)
}

/**
 * Get metadata of category/brand APIs
 *
 * @param _
 * @param args
 * @param ctx
 */
export const getSearchMetaData = async (
  _: any,
  args: SearchMetadataArgs,
  ctx: Context
) => {
  const queryAndMap = zipQueryAndMap(args.query, args.map)
  if (queryAndMap.length === 0) {
    return emptyTitleTag
  }

  const isFirstCategory = queryAndMap[0][1] === 'c'
  const tailTuples = tail(queryAndMap)

  const validTuples = tailTuples.filter(
    ([_, m]) =>
      m === 'b' ||
      m === 'brand' ||
      m.includes('specificationFilter') ||
      (m === 'c' && !isFirstCategory)
  )
  const [metadata, otherNames] = await Promise.all([
    getPrimaryMetadata(args, ctx),
    getNameForRemainingMaps(validTuples, ctx),
  ])

  const titleTagNames =
    shouldTranslateToUserLocale(ctx) ?
      (await translateTitles(metadata, otherNames, ctx))
      : [metadata.titleTag, ...otherNames]
  return {
    titleTag: joinNames(titleTagNames),
    metaTagDescription: formatTranslatableProp<SearchMetadata, 'metaTagDescription', 'id'>('metaTagDescription', 'id')(metadata, {}, ctx),
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of trusted/docs/README.md from the trusted VTEX app.

[Assistant]
Here's the implementation:

```markdown
# Trusted

Open the VTEX APP Store and install the app on your store.

or

Run the following command:

```sh
vtex install vtexeurope.trusted@0.x
```

Next, open the app settings on your admin and fill the form with your appKey, and the days after you should receive the email.

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of trusted/react/index.tsx from the trusted VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { canUseDOM } from 'vtex.render-runtime'
import { PixelMessage } from './typings/events'
import { createIFrame } from './utils'

export async function handleEvents(e: PixelMessage) {
  switch (e.data.eventName) {
    case 'vtex:orderPlaced': {

      const eventData = e?.data
      const email = eventData?.visitorContactInfo?.[0]
      const firstName = eventData?.visitorContactInfo?.[1]
      const orderId = eventData?.transactionId

      const DEFAULT_VISITOR_ADDRESS_STATE = window?.__trusted?.defaultAddressState
      const shippingCounty = eventData?.visitorAddressState || DEFAULT_VISITOR_ADDRESS_STATE

      createIFrame({
        appKey: window?.__trusted?.appKey,
        days: window?.__trusted?.days,
        email,
        firstName,
        orderId,
        shippingCounty,
      })
      return
    }
    default: {
      return
    }
  }
}

if (canUseDOM) {
  window.addEventListener('message', handleEvents)
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of trusted/react/utils.ts from the trusted VTEX app.

[Assistant]
Here's the implementation:

```typescript
export const createIFrame = (params: Params) => {
  const iframe = document.createElement('iframe')
  iframe.height = '1'
  iframe.width = '1'
  iframe.scrolling = 'no'
  iframe.marginHeight = '0'
  iframe.marginWidth = '0'
  const iframeSrc =
    `https://feedback.trusted.ro/api/${params.appKey}/?email=${params.email}&name=${params.firstName}&order=${params.orderId}&county=${params.shippingCounty}&email_after=${params.days}`

  iframe.src = iframeSrc

  document.body.appendChild(iframe)
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of trusted/react/package.json from the trusted VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "scripts": {
    "test": "vtex-test-tools test"
  },
  "dependencies": {
    "axios": "^0.21.1",
    "ramda": "^0.26.1",
    "moment": "^2.29.1"
  },
  "devDependencies": {
    "@types/graphql": "^14.5.0",
    "@types/jest": "^24.0.11",
    "@types/node": "^11.13.0",
    "@types/ramda": "^0.26.5",
    "@vtex/test-tools": "^1.1.0",
    "eslint": "^5.16.0",
    "eslint-config-vtex-react": "^4.1.0",
    "graphql": "^14.5.4",
    "prettier": "^1.18.2",
    "tslint-eslint-rules": "^5.4.0",
    "typescript": "3.9.7",
    "vtex.pixel-interfaces": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.pixel-interfaces@1.1.1/public/_types/react",
    "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.126.11/public/@types/vtex.render-runtime"
  },
  "version": "0.1.4"
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of trusted/store/interfaces.json from the trusted VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "pixel.trusted": {
    "component": "index"
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of trusted/react/typings/events.d.ts from the trusted VTEX app.

[Assistant]
Here's the implementation:

```typescript
export interface PixelMessage extends MessageEvent {
  data:
    | ProductViewData
    | ProductClickData
    | OrderPlacedData
    | OrderPlacedTrackedData
    | PageViewData
    | ProductImpressionData
    | AddToCartData
    | RemoveToCartData
    | UserData
}

export interface EventData {
  event: string
  eventName: string
  currency: string
}

export interface PageViewData extends EventData {
  event: 'pageView'
  eventName: 'vtex:pageView'
  pageTitle: string
  pageUrl: string
  referrer: string
}

export interface UserData extends EventData {
  event: 'userData'
  eventName: 'vtex:userData'
  email: string
  firstName: string
  id: string
  isAuthenticated: boolean
  lastName: string
  phone: string
}

export interface AddToCartData extends EventData {
  event: 'addToCart'
  eventName: 'vtex:addToCart'
  items: CartItem[]
}

export interface RemoveToCartData extends EventData {
  event: 'removeFromCart'
  eventName: 'vtex:removeFromCart'
  items: CartItem[]
}

export interface OrderPlacedData extends Order, EventData {
  event: 'orderPlaced'
  eventName: 'vtex:orderPlaced'
}

export interface OrderPlacedTrackedData extends Order, EventData {
  event: 'orderPlacedTracked'
  eventName: 'vtex:orderPlacedTracked'
}

export interface ProductViewData extends EventData {
  event: 'productView'
  eventName: 'vtex:productView'
  product: Product
}

export interface ProductClickData extends EventData {
  event: 'productClick'
  eventName: 'vtex:productClick'
  product: Product
}

export interface ProductImpressionData extends EventData {
  event: 'productImpression'
  eventName: 'vtex:productImpression'
  impressions: Impression[]
  product?: Product // deprecated, use impressions list!
  position?: number // deprecated, use impressions list!
  list: string
}

interface CartItem {
  skuId: string
  variant: string
  price: number
  name: string
  quantity: number
  productRefId: string
  brand: string
  category: string
}

export interface Order {
  currency: string
  accountName: string
  orderGroup: string
  salesChannel: string
  coupon: string
  visitorType: string
  visitorContactInfo: string[]
  visitorAddressState: string
  visitorAddressCountry: string
  visitorAddressPostalCode: string
  transactionId: string
  transactionDate: string
  transactionAffiliation: string
  transactionTotal: number
  transactionShipping: number
  transactionSubtotal: number
  transactionDiscounts: number
  transactionTax: number
  transactionCurrency: string
  transactionPaymentType: PaymentType[]
  transactionShippingMethod: ShippingMethod[]
  transactionProducts: ProductOrder[]
  transactionPayment: {
    id: string
  }
}

export interface Impression {
  product: Product
  position: number
}

interface PaymentType {
  group: string
  paymentSystemName: string
  installments: number
  value: number
}

interface ShippingMethod {
  itemId: string
  selectedSla: string
}

interface ProductOrder {
  id: string
  name: string
  sku: string
  skuRefId: string
  skuName: string
  productRefId: string
  ean: string
  slug: string
  brand: string
  brandId: string
  seller: string
  sellerId: string
  category: string
  categoryId: string
  categoryTree: string[]
  categoryIdTree: string[]
  priceTags: PriceTag[]
  originalPrice: number
  price: number
  sellingPrice: number
  tax: number
  quantity: number
  components: any[]
  measurementUnit: string
  unitMultiplier: number
}

interface PriceTag {
  identifier: string
  isPercentual: boolean
  value: number
}

interface Product {
  brand: string
  categoryId?: string // inconsistency
  categories: string[]
  productId: string
  productName: string
  selectedSku?: string // inconsistency
  items: Item[]
  sku: Item
  [key: string]: any
}

interface Item {
  itemId: string
  name: string
  seller?: Seller
  [key: string]: any
}

interface Seller {
  commertialOffer: CommertialOffer
  [key: string]: any
}

interface CommertialOffer {
  Price: number
  [key: string]: any
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of trusted/react/typings/vtex.render-runtime.d.ts from the trusted VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module 'vtex.render-runtime' {
  export const canUseDOM: boolean
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of trusted/react/typings/global.d.ts from the trusted VTEX app.

[Assistant]
Here's the implementation:

```typescript
interface Window extends Window {
  __trusted: {
    appKey: string
    days: number
    defaultAddressState: string
  }
}

interface Params {
  appKey: string
  email: string
  firstName: string
  orderId: string
  shippingCounty: string
  days: number
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of jivochat/docs/README.md from the jivochat VTEX app.

[Assistant]
Here's the implementation:

```markdown
# JivoChat

This app provides a native integration for all [JivoChat](https://www.jivochat.com/?utm_campaign=direct&utm_source=google.com&_ga=2.144978160.551999859.1580832835-1477813135.1580832835) customer comunication functionality.

## Configuration

1. [Install](https://vtex.io/docs/recipes/store/installing-an-app) the `vtex.jivochat@1.x` app in the desired account using your terminal. You can also install the app through the VTEX App Store.
2. In the admin's account, access the **Apps** section and then select the **JivoChat** box
3. Fill out the form with your **JivoChat's Widget ID** and save your changes.

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of jivochat/public/metadata/messages/es-AR.json from the jivochat VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "JivoChat",
  "headline": "Con unos pocos clics, acceda a chats, correos electrónicos y llamadas en un solo lugar.",
  "overview": "JivoChat es un completo servicio de mensajería en línea centrado en la conversión y la retención. Le permite comunicarse con visitantes y clientes desde cualquier lugar: Chat, Teléfono, Callback, E-mail, Facebook Messenger, Telegram y Viber.\r\nLos operadores reciben todos los chats, correos electrónicos y llamadas en una aplicación Jivo, por lo que ya no tienen que cambiar entre múltiples ventanas y pueden usarse en dispositivos móviles y computadoras, lo que significa menos distracciones y todas las herramientas que necesita. para una atención al cliente excepcional.",
  "features": [
    "Obtener información detallada del visitante",
    "Configurar activadores automáticos",
    "Monitorear visitantes en tiempo real",
    "Recibir opiniones sobre la calidad del servicio",
    "Formulario de contacto offline"
  ],
  "websiteURL": "https://vtex.com/",
  "support": {
    "url": "https://support.vtex.com/hc/requests"
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of jivochat/public/metadata/messages/pt-BR.json from the jivochat VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "JivoChat",
  "headline": "Com poucos cliques, acesse os serviços de chats, e-mails e ligações em um só lugar.",
  "overview": "O JivoChat é um messenger completo para lojas virtuais e focado em conversão e retenção. Permite que você se comunique com visitantes e clientes de qualquer lugar: Chat, Telefone, Callback, E-mail, Facebook Messenger, Telegram e Viber.\r\nOs operadores recebem todos os chats, e-mails e ligações em um só app do Jivo, então não precisam mais ficar alternando entre várias janelas e pode ser utilizado no celular e no computador ao mesmo tempo, isso significa menos distrações e todas as ferramentas necessárias para um excepcional serviço de suporte ao cliente.",
  "features": [
    "Obtenha informações detalhadas de visitantes",
    "Configure gatilhos automáticos",
    "Monitore visitantes em tempo real",
    "Receba avaliações da qualidade do serviço",
    "Ofereça formulário de contato offline"
  ],
  "websiteURL": "https://vtex.com/",
  "support": {
    "url": "https://support.vtex.com/hc/requests"
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of jivochat/public/metadata/messages/en-US.json from the jivochat VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "JivoChat",
  "headline": "With a few clicks, access the services of chats, e-mails and calls in one place.",
  "overview": "JivoChat is a complete online store messenger focused on conversion and retention. Allows you to communicate with visitors and customers from anywhere: Chat, Phone, Callback, Email, Facebook Messenger, Telegram and Viber.\r\nOperators receive all chats, emails and calls in one Jivo app, so they no longer have to switch between multiple windows and can be used on both mobile and computer, meaning fewer distractions and all the tools you need. for exceptional customer support.",
  "features": [
    "Get detailed visitor information",
    "Set up automatic triggers",
    "Monitor visitors in real time",
    "Receive quality of service reviews",
    "Offer offline contact form"
  ],
  "websiteURL": "https://vtex.com/",
  "support": {
    "url": "https://support.vtex.com/hc/requests"
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of my-authentication/messages/nl.json from the my-authentication VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "warn.aboutToBlock": "Nog een ongeldige poging blokkeert uw account gedurende één uur.",
  "error.unknown": "Sorry, er is een onbekende fout opgetreden. Probeer het nog eens",
  "error.connection": "Er is een fout opgetreden bij het verbinden met het systeem, probeer de pagina opnieuw te laden.",
  "error.wrongPass": "Huidig wachtwoord is onjuist.",
  "error.wrongCode": "De code is verkeerd.",
  "error.blockedUser": "U bent geblokkeerd. Probeer het later opnieuw.",
  "error.throttled": "Wacht enkele minuten voordat u de code opnieuw verstuurt.",
  "feedback.codeSent": "Code verzonden via e-mail.",
  "feedback.passwordUpdated": "Wachtwoord bijgewerkt.",
  "feedback.logoutSuccess": "Sessie afgemeld.",
  "ExtensionLinks.authentication": "Authenticatie",
  "ReloadableError.reload": "Opnieuw laden",
  "AuthenticationScreen.title": "Authenticatie",
  "AuthenticationScreen.backButton": "Profiel",
  "AuthenticationScreen.EditPassword.MaskedPassword.label": "Wachtwoord",
  "AuthenticationScreen.EditPassword.MaskedPassword.CreatePassButton.text": "Wachtwoord instellen",
  "AuthenticationScreen.EditPassword.MaskedPassword.ChangePassButton.text": "Wachtwoord opnieuw instellen",
  "AuthenticationScreen.EditPassword.MakedPassword.noPass": "U heeft nog geen wachtwoord ingesteld.",
  "AuthenticationScreen.EditPassword.SavePassButton.text": "Wachtwoord opslaan",
  "AuthenticationScreen.EditPassword.NewPassInput.label": "Nieuw wachtwoord",
  "AuthenticationScreen.EditPassword.PassValidation.title": "Uw wachtwoord moet bevatten ten minste:",
  "AuthenticationScreen.EditPassword.PassValidation.minLength": "8 tekens",
  "AuthenticationScreen.EditPassword.PassValidation.number": "1 cijfer",
  "AuthenticationScreen.EditPassword.PassValidation.upper": "1 hoofdletter",
  "AuthenticationScreen.EditPassword.PassValidation.lower": "1 kleine letter",
  "AuthenticationScreen.EditPassword.CreatePassword.emailSentText": "Er is een verificatiecode naar {email} gestuurd.",
  "AuthenticationScreen.EditPassword.CreatePassword.CodeInput.label": "Code",
  "AuthenticationScreen.EditPassword.CreatePassword.ResendCodeButton.text": "Code opnieuw verzenden",
  "AuthenticationScreen.EditPassword.ChangePassword.CurrPassInput.label": "Huidig wachtwoord",
  "AuthenticationScreen.LoginSessions.label": "Sessiebeheer",
  "AuthenticationScreen.LoginSessions.activeSessions": "U hebt {amount} actieve sessies",
  "AuthenticationScreen.LoginSessions.OpenSessionsButton.text": "Sessies controleren",
  "LoginSessionsScreen.title": "Sessiebeheer",
  "LoginSessionsScreen.backButton": "Authenticatie",
  "LoginSessionsScreen.LoginSession.currentSession": "Huidige sessie",
  "LoginSessionsScreen.LoginSession.lastAccess": "Laatste toegang: {lastAccess}",
  "LoginSessionsScreen.LoginSession.ipDetails": "IP: {ip}",
  "LoginSessionsScreen.LoginSession.firstAccess": "Eerste toegang: {dateTime}",
  "LoginSessionsScreen.LoginSession.LogoutButton.text": "Afmelden",
  "LoginSessionsScreen.LoginSession.DeviceMsg.desktop": "Desktop",
  "LoginSessionsScreen.LoginSession.DeviceMsg.mobile": "Mobiel",
  "LoginSessionsScreen.LoginSession.DeviceMsg.tablet": "Tablet"
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of my-authentication/messages/ja.json from the my-authentication VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "warn.aboutToBlock": "あと 1 回無効になると、アカウントが 1 時間ブロックされます。",
  "error.unknown": "申し訳ありません、不明なエラーが発生しました。もう一度やり直してください",
  "error.connection": "システムへの接続時にエラーが発生しました。ページを再読み込みしてください。",
  "error.wrongPass": "このパスワードは間違っています。",
  "error.wrongCode": "コードが違います。",
  "error.blockedUser": "ブロックされました。後でもう一度やり直してください。",
  "error.throttled": "数分待ってから、コードを再送してください。",
  "feedback.codeSent": "コードがメール経由で送信されました。",
  "feedback.passwordUpdated": "パスワードが更新されました。",
  "feedback.logoutSuccess": "セッションをログアウトしました。",
  "ExtensionLinks.authentication": "認証",
  "ReloadableError.reload": "再読み込み",
  "AuthenticationScreen.title": "認証",
  "AuthenticationScreen.backButton": "プロフィール",
  "AuthenticationScreen.EditPassword.MaskedPassword.label": "パスワード",
  "AuthenticationScreen.EditPassword.MaskedPassword.CreatePassButton.text": "パスワードを設定",
  "AuthenticationScreen.EditPassword.MaskedPassword.ChangePassButton.text": "パスワードを設定し直す",
  "AuthenticationScreen.EditPassword.MakedPassword.noPass": "パスワードをまだ設定していません。",
  "AuthenticationScreen.EditPassword.SavePassButton.text": "パスワードを保存",
  "AuthenticationScreen.EditPassword.NewPassInput.label": "新しいパスワード",
  "AuthenticationScreen.EditPassword.PassValidation.title": "パスワードには少なくとも以下が含まれている必要があります:",
  "AuthenticationScreen.EditPassword.PassValidation.minLength": "8 つの文字",
  "AuthenticationScreen.EditPassword.PassValidation.number": "1 つの数字",
  "AuthenticationScreen.EditPassword.PassValidation.upper": "1 つの大文字",
  "AuthenticationScreen.EditPassword.PassValidation.lower": "1 つの小文字",
  "AuthenticationScreen.EditPassword.CreatePassword.emailSentText": "確認コードが {email} に送信されました。",
  "AuthenticationScreen.EditPassword.CreatePassword.CodeInput.label": "コード",
  "AuthenticationScreen.EditPassword.CreatePassword.ResendCodeButton.text": "コードを再送",
  "AuthenticationScreen.EditPassword.ChangePassword.CurrPassInput.label": "現在のパスワード",
  "AuthenticationScreen.LoginSessions.label": "セッション管理",
  "AuthenticationScreen.LoginSessions.activeSessions": "{amount} のアクティブセッションがあります",
  "AuthenticationScreen.LoginSessions.OpenSessionsButton.text": "セッションを確認",
  "LoginSessionsScreen.title": "セッション管理",
  "LoginSessionsScreen.backButton": "認証",
  "LoginSessionsScreen.LoginSession.currentSession": "現在のセッション",
  "LoginSessionsScreen.LoginSession.lastAccess": "前回のアクセス: {lastAccess}",
  "LoginSessionsScreen.LoginSession.ipDetails": "IP: {ip}",
  "LoginSessionsScreen.LoginSession.firstAccess": "初回のアクセス: {dateTime}",
  "LoginSessionsScreen.LoginSession.LogoutButton.text": "ログアウト",
  "LoginSessionsScreen.LoginSession.DeviceMsg.desktop": "デスクトップ",
  "LoginSessionsScreen.LoginSession.DeviceMsg.mobile": "モバイル",
  "LoginSessionsScreen.LoginSession.DeviceMsg.tablet": "タブレット"
}

```