[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/utils/slug.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import slugify from 'slugify'
import { toLower } from 'ramda'

export function Slugify(str: any) {
  return slugify(str, { lower: true, remove: /[*+~.()'"!:@]/g })
}

const from =
  'ÁÄÂÀÃÅČÇĆĎÉĚËÈÊẼĔȆÍÌÎÏŇÑÓÖÒÔÕØŘŔŠŤÚŮÜÙÛÝŸŽáäâàãåčçćďéěëèêẽĕȇíìîïňñóöòôõøðřŕšťúůüùûýÿžþÞĐđßÆa·/_,:;'
const to =
  'AAAAAACCCDEEEEEEEEIIIINNOOOOOORRSTUUUUUYYZaaaaaacccdeeeeeeeeiiiinnooooooorrstuuuuuyyzbBDdBAa------'
const removeAccents = (str: string) => {
  let newStr = str.slice(0)
  for (let i = 0; i < from.length; i++) {
    newStr = newStr.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
  }
  return newStr
}

// Parameter "S. Coifman" should output "s--coifman"
export function searchSlugify(str: string) {
  // According to Bacelar, the search API uses a legacy method for slugifying strings.
  // replaces special characters with dashes, remove accents and lower cases everything
  // eslint-disable-next-line no-useless-escape
  const noCommas = str.replace(/,/g, '')
  const replaced = noCommas.replace(/[*+~.()'"!:@&\[\]`/ %$#?{}|><=_^]/g, '-')
  return toLower(removeAccents(replaced))
}

export const slugifyStoreIndexer = (str: string) =>
  str
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[*+~.()'"!:@&\[\]`,/ %$#?{}|><=_^]/g, '-')

```


[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/utils/searchState.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
/**
 * Parse `searchState` string into a `object`.
 * We treat `searchState` as a stringified javascript object.
 * Parsing errors are returned as an empty object
 *
 * ```
 *       { location: "l:-1:1", utm_source: "xxx" }
 * ```
 * @private
 * @param {string} state
 * @returns {{ [key: string]: string }}
 * @memberof BiggySearchClient
 */
export const parseState = (state?: string): { [key: string]: string } => {
  if (!state) {
    return {}
  }

  try {
    const parsed = JSON.parse(decodeURI(state))
    if (typeof parsed === 'object') {
      return parsed
    }
  } catch (err) {
    /* ignore parsing errors */
  }

  return {}
}

```


[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/utils/with-fallback.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
/**
 * Helper function to execute intsch as primary with intelligentSearchApi as fallback
 */
export async function withFallback<T>(
    primaryFn: () => Promise<T>,
    fallbackFn: () => Promise<T>,
    logger: any,
    operationName: string,
    args?: Record<string, unknown>
  ): Promise<T> {
    try {
      return await primaryFn()
    } catch (error) {
      logger.warn({
        message: `${operationName}: Primary call failed, using fallback`,
        error: error.message,
        args,
      })

      return fallbackFn()
    }
  }
```


[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/utils/string.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
export const toTitleCase = (str: string) => {
  return str
    .toLowerCase()
    .split(' ')
    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
    .join(' ')
}

export const removeDiacriticsFromURL = (url: string) =>
  encodeURIComponent(
    decodeURIComponent(url)
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
  )

```


[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/utils/searchState.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { parseState } from './searchState'

describe('parseState', () => {
  it('should correctly parse empty state', () => {
    expect(parseState('')).toEqual({})
    expect(parseState(undefined)).toEqual({})
  })

  it('should return empty on invalid state', () => {
    expect(parseState('adsasd')).toEqual({})
    expect(parseState('4')).toEqual({})
  })

  it('should correctly parse state', () => {
    expect(parseState(JSON.stringify({ property: 'value' }))).toEqual({
      property: 'value',
    })
  })
})

```


[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/utils/i18n.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
/* eslint-disable @typescript-eslint/no-use-before-define */
import {
  createMessagesLoader,
  formatTranslatableStringV2,
  parseTranslatableStringV2,
} from '@vtex/api'

import { logDegradedSearchError } from '../resolvers/search/utils'

export const formatTranslatableProp =
  <R, P extends keyof R, I extends keyof R>(prop: P, idProp: I) =>
  (root: R, _: unknown, ctx: Context) =>
    addContextToTranslatableString(
      {
        content: root[prop] as unknown as string,
        context: root[idProp] as unknown as string,
      },
      ctx
    )

interface BaseMessage {
  content: string
  from?: string
}

interface MessageWithContext extends BaseMessage {
  context: string | number
}

export interface Message extends BaseMessage {
  context?: string
}

export const addContextToTranslatableString = (
  message: Message,
  ctx: Context
) => {
  const {
    vtex: { tenant },
    translated,
  } = ctx

  const { locale } = tenant!

  if (!message.content) {
    return message.content
  }

  const state = translated ? 'translated' : 'original'

  try {
    const {
      content,
      context: originalContext,
      from: originalFrom,
    } = parseTranslatableStringV2(message.content)

    const context = (originalContext || message.context)?.toString()
    const from = originalFrom || message.from || locale

    return formatTranslatableStringV2({ content, context, from, state })
  } catch (e) {
    logDegradedSearchError(ctx.vtex.logger, {
      service: 'node-vtex-api translation',
      error: 'Error when trying to add context to translatable string',
      errorStack: e,
    })

    return message.content
  }
}

export const translateToCurrentLanguage = (
  message: MessageWithContext,
  ctx: Context
) => {
  const {
    state,
    clients,
    vtex: { binding, tenant, locale },
  } = ctx

  if (!state.messagesBindingLanguage) {
    state.messagesBindingLanguage = createMessagesLoader(
      clients,
      locale ?? binding!.locale
    )
  }

  return state.messagesBindingLanguage!.load({
    content: message.content,
    context: message.context?.toString(),
    from: message.from ?? tenant!.locale,
  })
}

export const translateManyToCurrentLanguage = (
  messages: Message[],
  ctx: Context
) => {
  const {
    state,
    clients,
    vtex: { binding, tenant, locale },
  } = ctx

  if (!state.messagesBindingLanguage) {
    state.messagesBindingLanguage = createMessagesLoader(
      clients,
      locale ?? binding!.locale
    )
  }

  return state.messagesBindingLanguage!.loadMany(
    messages.map((message) => ({
      content: message.content,
      context: message.context,
      from: message.from ?? tenant!.locale,
    }))
  )
}

export const shouldTranslateToUserLocale = ({
  vtex: { tenant, locale },
}: Context) => tenant?.locale !== locale

export const shouldTranslateToBinding = (
  { translated, vtex: { binding, tenant } }: Context,
  ignoreIndexedTranslation?: boolean
) =>
  binding &&
  binding?.locale !== tenant?.locale &&
  (!translated || ignoreIndexedTranslation)

export const shouldTranslateToTenantLocale = ({
  vtex: { locale, tenant },
}: Context) => Boolean(tenant?.locale && locale && tenant.locale !== locale)

```


[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/utils/vbase.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { VBase } from '@vtex/api'
import { createHash } from 'crypto'

export const staleFromVBaseWhileRevalidate = async <T>(
  vbase: VBase,
  bucket: string,
  filePath: string,
  validateFunction: (params?: any) => Promise<T>,
  params?: any,
  options?: { expirationInMinutes?: number}
): Promise<T> => {
    const normalizedFilePath = normalizedJSONFile(filePath)
    const cachedData = await vbase.getJSON<StaleRevalidateData<T>>(bucket, normalizedFilePath, true).catch() as StaleRevalidateData<T>
    if(!cachedData){
      const endDate = getTTL(options?.expirationInMinutes)
      return await revalidate<T>(vbase, bucket, normalizedFilePath, endDate, validateFunction, params)
    }

    const { data, ttl} = cachedData as StaleRevalidateData<T>

    const today = new Date()
    const ttlDate = new Date(ttl)
    if(today < ttlDate){
      return data
    }
    const endDate = getTTL(options?.expirationInMinutes)
    revalidate<T>(vbase, bucket, normalizedFilePath, endDate, validateFunction, params)
    return data
}

const getTTL = (expirationInMinutes?: number) => {
  const ttl = new Date()
  ttl.setMinutes(ttl.getMinutes() + (expirationInMinutes || 30 ))
  return ttl
}

const revalidate = async<T> (
  vbase: VBase, bucket: string, filePath: string,
  endDate: Date, validateFunction: (params?: any) => Promise<T>, params?: any) => {
  const data = await validateFunction(params)
  const revalidatedData = { data, ttl: endDate }
  vbase.saveJSON<StaleRevalidateData<T>>(bucket, filePath, revalidatedData).catch()
  return data
}

const normalizedJSONFile = (filePath: string) => createHash('md5').update(filePath).digest('hex') + '.json'
```


[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/utils/attachments.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { all, propEq } from 'ramda'

export const CHOICE_TYPES = {
  MULTIPLE: 'MULTIPLE',
  SINGLE: 'SINGLE',
  TOGGLE: 'TOGGLE',
}

const isParentOptionSingleChoice = ({ composition }: AssemblyOption) => {
  if (!composition) {
    return false
  }

  const { minQuantity, maxQuantity } = composition

  return minQuantity === 1 && maxQuantity === 1
}

const isParentOptionToggleChoice = ({ composition }: AssemblyOption) => {
  if (!composition) {
    return false
  }

  const { items } = composition

  return all(propEq('maxQuantity', 1))(items)
}

export const getItemChoiceType = (childAssemblyData?: AssemblyOption) => {
  if (!childAssemblyData) {
    return CHOICE_TYPES.MULTIPLE
  }

  const isSingle = isParentOptionSingleChoice(childAssemblyData!)

  if (isSingle) {
    return CHOICE_TYPES.SINGLE
  }

  const isToggle = isParentOptionToggleChoice(childAssemblyData)

  if (isToggle) {
    return CHOICE_TYPES.TOGGLE
  }

  return CHOICE_TYPES.MULTIPLE
}

```


[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/utils/cookie.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { parse } from 'cookie'
import { keys } from 'ramda'

const isUserLoggedIn = (ctx: Context) => {
  const {
    vtex: { account },
  } = ctx

  return !!ctx.cookies.get(`VtexIdclientAutCookie_${account}`)
}

const parseCookie = (cookie: string) => {
  const parsed = parse(cookie)
  const cookieName = keys(parsed)[0]
  const cookieValue = parsed[cookieName]

  const extraOptions = {
    path: parsed.path,
    domain: parsed.domain,
    expires: parsed.expires ? new Date(parsed.expires) : undefined,
  }

  return {
    name: cookieName,
    value: cookieValue,
    options: extraOptions,
  }
}

/** Checkout cookie methods */
const CHECKOUT_COOKIE = 'checkout.vtex.com'

const checkoutCookieFormat = (orderFormId: string) =>
  `${CHECKOUT_COOKIE}=__ofid=${orderFormId};`

const getOrderFormIdFromCookie = (cookies: Context['cookies']) => {
  const cookie = cookies.get(CHECKOUT_COOKIE)

  return cookie && cookie.split('=')[1]
}

export {
  isUserLoggedIn,
  CHECKOUT_COOKIE,
  checkoutCookieFormat,
  getOrderFormIdFromCookie,
  parseCookie,
}

```


[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/utils/CategoryTreeSegmentsFinder.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { searchSlugify } from './slug'
import { VBase } from '@vtex/api'
import { Search } from '../clients/search'
import { CATEGORY_TREE_CHILDREN_BUCKET, CATEGORY_TREE_ROOT_BUCKET, CATEGORY_TREE_ROOT_PATH } from '../resolvers/search/constants'
import { staleFromVBaseWhileRevalidate } from './vbase'
interface Clients{
  vbase: VBase
  search: Search
}

interface LazyCategoryTreeNode{
  id: number
  name: string
  hasChildren: boolean
}

export interface CategoryIdNamePair{
  id: string
  name: string
}

export class CategoryTreeSegmentsFinder {

  private clients: Clients
  private segments: string[]
  protected categoryTreeRoot: Record<string, LazyCategoryTreeNode>

  public constructor(clients: Clients, segments: string[]){
    this.clients = clients
    this.segments = segments
    this.categoryTreeRoot = {}
  }

  public find = async () => {
    const { segments } = this
    const result: (CategoryIdNamePair|null)[] = []
    await this.initCategoryTreeRoot()

    const rootCategorySegment = this.findRootCategorySegment()
    if(!rootCategorySegment){
      return []
    }

    const {category, index} = rootCategorySegment

    result[index] = {id: category.id.toString(), name: category.name}
    const segmentsTail = segments.slice(index+1)
    const categorySegmentsFromChildren = await this.findCategoriesFromChildren(category.id, segmentsTail)
    return result.concat(categorySegmentsFromChildren)
  }

  /**
   * Fetches a category children from vbase or search
   * clients: {vbase: VBase, search: Search}
   */
  protected lazyFetchChildren = async (id: number) => {
    const { search } = this.clients
    return await this.staleWhileRevalidate<Record<string, string>>(CATEGORY_TREE_CHILDREN_BUCKET, id.toString(), this.fetchChildrenFromSearch, { search, id })
  }

  protected staleWhileRevalidate = async <T>(bucket: string, path: string,validateFunction: (params?: any) => Promise<T>, params?: any) => {
    return await staleFromVBaseWhileRevalidate<T>(this.clients.vbase, bucket, path, validateFunction, params)
  }

  protected getCategoryTreeRoot = async () => {
    const categoryTree = await this.clients.search.categories(0)
    return categoryTree.reduce((acc, categoryTreeNode) => {
      const categorySlug = searchSlugify(categoryTreeNode.name)
      const lazyCategoryTreeNode = {
        id: categoryTreeNode.id,
        name: categoryTreeNode.name,
        hasChildren: categoryTreeNode.hasChildren,
      }
      acc[categorySlug] = lazyCategoryTreeNode
      return acc
    }, {} as Record<string, LazyCategoryTreeNode>)
  }

  private initCategoryTreeRoot = async () => {
    this.categoryTreeRoot = await this.staleWhileRevalidate<Record<string, LazyCategoryTreeNode>>(
      CATEGORY_TREE_ROOT_BUCKET, CATEGORY_TREE_ROOT_PATH, this.getCategoryTreeRoot)
  }

  // Returns {id: categoryId, name: categorySlug }
  private findCategoriesFromChildren = async (categoryId: number, segments: string[]) => {
    const result: (CategoryIdNamePair|null)[] = []
    for(const segment of segments){
      const children = await this.getChildren(categoryId)
      const childCategoryId = children[segment]
      if(childCategoryId){
        categoryId = Number(childCategoryId)
        result.push({ id: childCategoryId, name: segment })
      }else{
        result.push(null)
      }
    }
    return result
  }

  private findRootCategorySegment = () => {
    const { segments, categoryTreeRoot } = this
    const segmentIndex = segments.findIndex(segment => !!categoryTreeRoot[segment])
    return segmentIndex !== -1? {index: segmentIndex, category: categoryTreeRoot[segments[segmentIndex]]}: null
  }

  private getChildren = async (id: number) => {
    return await this.lazyFetchChildren(id)
  }

  // Returns { categorySlug: categoryId }
  private fetchChildrenFromSearch = async (params: { search: Search, id: number }): Promise<Record<string, string>> => {
    const {search, id} = params
    const categoryChildren = await search.getCategoryChildren(id)
    const categoryChildrenBySlug = Object.keys(categoryChildren).reduce((acc, categoryChildId: string) => {
      const categoryChildName = categoryChildren[categoryChildId]
      const categoryChildSlug = searchSlugify(categoryChildName)
      acc[categoryChildSlug] = categoryChildId
      return acc
    }, {} as Record<string, string>)
    return categoryChildrenBySlug
  }
}
```


[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/utils/compareResults.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import type { Logger } from '@vtex/api'

/**
 * Utility to compare the results of two functions in parallel
 * Used primarily for comparing the results of two API implementations
 * during the migration from intelligent-search-api to intsch
 */

/**
 * Represents a difference found between two values
 */
export interface ObjectDifference {
  path: string
  type:
    | 'missing_key'
    | 'extra_key'
    | 'different_value'
    | 'different_type'
    | 'array_length_mismatch'
  expected?: unknown
  actual?: unknown
  details?: string
}

/**
 * Result of deep comparison with differences
 */
export interface DeepComparisonResult {
  isEqual: boolean
  differences: ObjectDifference[]
}

/**
 * Finds differences between two values recursively
 * @param a First value to compare
 * @param b Second value to compare
 * @param path Current path in the object (for tracking location of differences)
 * @param maxDepth Maximum recursion depth (default: 20)
 * @param currentDepth Current recursion depth (internal use)
 * @returns Array of differences found
 */
export function findDifferences(
  a: unknown,
  b: unknown,
  path = '',
  maxDepth = 50,
  currentDepth = 0
): ObjectDifference[] {
  const differences: ObjectDifference[] = []

  // If both values are exactly the same, no differences
  if (a === b) return differences

  // Check for null values or different types
  if (a === null || b === null || typeof a !== typeof b) {
    differences.push({
      path,
      type: 'different_type',
      expected: a === null ? 'null' : typeof a,
      actual: b === null ? 'null' : typeof b,
    })

    return differences
  }

  // If neither is an object, they are different primitive values
  if (typeof a !== 'object' || typeof b !== 'object') {
    differences.push({
      path,
      type: 'different_value',
      expected: a,
      actual: b,
    })

    return differences
  }

  // Check if maximum recursion depth has been reached (only for objects)
  if (currentDepth > maxDepth) {
    // When max depth is reached, consider values equal (original behavior)
    return differences
  }

  // Handle arrays specially
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) {
      differences.push({
        path,
        type: 'array_length_mismatch',
        expected: a.length,
        actual: b.length,
      })
    }

    const maxLength = Math.max(a.length, b.length)

    for (let i = 0; i < maxLength; i++) {
      const currentPath = path ? `${path}[${i}]` : `[${i}]`

      if (i >= a.length) {
        differences.push({
          path: currentPath,
          type: 'missing_key',
          actual: b[i],
        })
      } else if (i >= b.length) {
        differences.push({
          path: currentPath,
          type: 'extra_key',
          expected: a[i],
        })
      } else {
        differences.push(
          ...findDifferences(
            a[i],
            b[i],
            currentPath,
            maxDepth,
            currentDepth + 1
          )
        )
      }
    }

    return differences
  }

  // If one is an array but the other isn't
  if (Array.isArray(a) !== Array.isArray(b)) {
    differences.push({
      path,
      type: 'different_type',
      expected: Array.isArray(a) ? 'array' : 'object',
      actual: Array.isArray(b) ? 'array' : 'object',
    })

    return differences
  }

  // Cast to Record<string, unknown> for type safety
  const objA = a as Record<string, unknown>
  const objB = b as Record<string, unknown>

  // Compare object keys
  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)
  const allKeys = new Set([...keysA, ...keysB])

  for (const key of allKeys) {
    const currentPath = path ? `${path}.${key}` : key
    const hasKeyA = Object.prototype.hasOwnProperty.call(objA, key)
    const hasKeyB = Object.prototype.hasOwnProperty.call(objB, key)

    if (hasKeyA && !hasKeyB) {
      differences.push({
        path: currentPath,
        type: 'extra_key',
        expected: objA[key],
      })
    } else if (!hasKeyA && hasKeyB) {
      differences.push({
        path: currentPath,
        type: 'missing_key',
        actual: objB[key],
      })
    } else if (hasKeyA && hasKeyB) {
      differences.push(
        ...findDifferences(
          objA[key],
          objB[key],
          currentPath,
          maxDepth,
          currentDepth + 1
        )
      )
    }
  }

  return differences
}

/**
 * Performs a deep equality comparison between two values
 * @param a First value to compare
 * @param b Second value to compare
 * @param maxDepth Maximum recursion depth (default: 20)
 * @returns DeepComparisonResult with isEqual boolean and differences array
 */
export function isDeepEqual(
  a: unknown,
  b: unknown,
  maxDepth = 20
): DeepComparisonResult {
  const differences = findDifferences(a, b, '', maxDepth, 0)

  return {
    isEqual: differences.length === 0,
    differences,
  }
}

/**
 * Filters out differences that should be ignored based on path and type
 * @param differences Array of differences to filter
 * @param ignoredDifferences Array of ignored difference configurations
 * @returns Filtered array of differences
 */
export function filterIgnoredDifferences(
  differences: ObjectDifference[],
  ignoredDifferences: IgnoredDifference[] = []
): ObjectDifference[] {
  if (ignoredDifferences.length === 0) {
    return differences
  }

  return differences.filter((difference) => {
    // Check if this difference should be ignored
    return !ignoredDifferences.some(
      (ignored) =>
        ignored.path === difference.path && ignored.type === difference.type
    )
  })
}

/**
 * Configuration for ignoring expected differences
 */
export interface IgnoredDifference {
  path: string
  type:
    | 'missing_key'
    | 'extra_key'
    | 'different_value'
    | 'different_type'
    | 'array_length_mismatch'
}

/**
 * Options for the compareApiResults function
 */
export interface CompareApiResultsOptions {
  args?: unknown
  logPrefix?: string
  ignoredDifferences?: IgnoredDifference[]
}

/**
 * Executes two functions in parallel and compares their results
 * @param func1 First function to execute
 * @param func2 Second function to execute
 * @param sample Percentage of traffic that should execute the comparison (0-100)
 * @param logger Logger instance to use for logging
 * @param options Configuration options
 * @returns Promise with a result object containing the results and comparison details
 */
export async function compareApiResults<T>(
  func1: () => Promise<T>,
  func2: () => Promise<T>,
  sample: number,
  logger: Logger,
  options: CompareApiResultsOptions = {}
): Promise<T> {
  const { logPrefix = 'API Comparison', ignoredDifferences = [] } = options

  // Check if we should perform comparison based on sample percentage
  const shouldCompare = Math.random() * 100 < sample

  if (!shouldCompare) {
    // If not in sample, just execute func1 and return its result
    return func1()
  }

  // Execute both functions in parallel
  const [result1, result2] = await Promise.all([
    func1().catch((error) => ({ __error: error })),
    func2().catch((error) => ({ __error: error })),
  ])

  // Check if either function resulted in an error
  const hasError1 =
    result1 && typeof result1 === 'object' && '__error' in result1

  const hasError2 =
    result2 && typeof result2 === 'object' && '__error' in result2

  let areEqual = false
  let differences: ObjectDifference[] = []
  let filteredDifferences: ObjectDifference[] = []

  try {
    if (!hasError1 && !hasError2) {
      const comparisonResult = isDeepEqual(result1, result2)

      differences = comparisonResult.differences
      filteredDifferences = filterIgnoredDifferences(
        differences,
        ignoredDifferences
      )
      areEqual = filteredDifferences.length === 0
    }
  } catch (error) {
    logger.error({
      message: `${logPrefix}: Error during deep comparison`,
      error,
    })
  }

  if (!areEqual && !hasError1 && !hasError2) {
    logger.error({
      message: `${logPrefix}: Results differ`,
      params: JSON.stringify(options.args),
      differences: filteredDifferences.slice(0, 10), // Limit to first 10 differences to avoid log overflow
      differenceCount: filteredDifferences.length,
      totalDifferences: differences.length,
      ignoredDifferences: differences.length - filteredDifferences.length,
    })
  } else if (areEqual) {
    logger.info({
      //  Since the log level is indexed we are using it to get a sense of the % of results that are equal.
      message: `${logPrefix}: Results are equal`,
      params: JSON.stringify(options.args),
      totalDifferences: differences.length,
      ignoredDifferences: differences.length - filteredDifferences.length,
    })
  }

  if (!hasError1) {
    return result1 as T
  }

  if (hasError1 && !hasError2) {
    return result2 as T
  }

  throw Error('Both calls resulted in errors')
}

```


[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/utils/object.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  adjust,
  curry,
  fromPairs,
  map,
  mergeAll,
  pipe,
  toPairs,
  zipObj,
} from 'ramda'

/**
 * Creates a new object with the own properties of the provided object, but the
 * keys renamed according to logic of renaming function.
 *
 * Keep in mind that in the case of keys conflict is behaviour undefined and
 * the result may vary between various JS engines!
 *
 * @func renameKeysWith
 * @category Object
 * @param {Function} func Function that renames the keys
 * @param {!Object} object Provided object
 * @return {!Object} New object with renamed keys
 * @see {@link https://github.com/ramda/ramda/wiki/Cookbook#rename-keys-of-an-object-by-a-function|Ramda Cookbook}
 * @example
 *
 * renameKeysWith(concat('a'), { A: 1, B: 2, C: 3 }) //=> { aA: 1, aB: 2, aC: 3 }
 */
type Tuple = [string, any]
const renameKeysWith = curry((func: any, object: any) =>
  pipe<any, Tuple[], any, any>(toPairs, map(adjust(0, func)), fromPairs)(object)
)

/*
 * Convert a list of fields like [ {key: 'propertyName', value: 'String'}, ... ]
 * to a JSON format.
 */
const parseFieldsToJson = (fields: any) =>
  mergeAll(fields.map((field: any) => zipObj([field.key], [field.value])))

/**
 * Pass this to a filter to get distinct values from an array.
 * .filter(dinstinct)
 */
function distinct<T>(value: T, index: number, self: T[]) {
  return self.indexOf(value) === index
}

export { renameKeysWith, parseFieldsToJson, distinct }

```


[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/utils/index.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { AxiosError } from 'axios'
import { AuthenticationError, ForbiddenError, UserInputError } from '@vtex/api'

export * from './object'
export * from './cookie'

export function generateRandomName() {
  return (1 + Math.random()).toString(36).substring(2)
}

export function getFileExtension(fileName: string) {
  return fileName.match(/\.[0-9a-z]+$/i)
    ? (fileName.match(/\.[0-9a-z]+$/i) as any[])[0]
    : undefined
}

export function statusToError(e: any) {
  if (!e.response) {
    throw e
  }

  const { response } = e as AxiosError
  const { status } = response!

  if (status === 401) {
    throw new AuthenticationError(e)
  }
  if (status === 403) {
    throw new ForbiddenError(e)
  }
  if (status === 400) {
    throw new UserInputError(e)
  }

  throw e
}

```


[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/utils/compareResults.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import type { Logger } from '@vtex/api'

import {
  isDeepEqual,
  compareApiResults,
  findDifferences,
  filterIgnoredDifferences,
  type IgnoredDifference,
  type ObjectDifference,
} from './compareResults'

describe('isDeepEqual', () => {
  // Basic equality tests
  describe('basic equality', () => {
    it('should return true for identical primitive values', () => {
      expect(isDeepEqual(1, 1).isEqual).toBe(true)
      expect(isDeepEqual('hello', 'hello').isEqual).toBe(true)
      expect(isDeepEqual(true, true).isEqual).toBe(true)
      expect(isDeepEqual(null, null).isEqual).toBe(true)
      expect(isDeepEqual(undefined, undefined).isEqual).toBe(true)
    })

    it('should return false for different primitive values', () => {
      expect(isDeepEqual(1, 2).isEqual).toBe(false)
      expect(isDeepEqual('hello', 'world').isEqual).toBe(false)
      expect(isDeepEqual(true, false).isEqual).toBe(false)
      expect(isDeepEqual(null, undefined).isEqual).toBe(false)
      expect(isDeepEqual(0, null).isEqual).toBe(false)
      expect(isDeepEqual('', null).isEqual).toBe(false)
    })

    it('should handle empty arrays and objects', () => {
      expect(isDeepEqual([], []).isEqual).toBe(true)
      expect(isDeepEqual({}, {}).isEqual).toBe(true)
      expect(isDeepEqual([], {}).isEqual).toBe(false)
    })
  })

  // Simple objects and arrays tests
  describe('simple objects and arrays', () => {
    it('should compare simple arrays correctly', () => {
      expect(isDeepEqual([1, 2, 3], [1, 2, 3]).isEqual).toBe(true)
      expect(isDeepEqual([1, 2, 3], [1, 2, 4]).isEqual).toBe(false)
      expect(isDeepEqual([1, 2], [1, 2, 3]).isEqual).toBe(false)
    })

    it('should compare simple objects correctly', () => {
      expect(isDeepEqual({ a: 1, b: 2 }, { a: 1, b: 2 }).isEqual).toBe(true)
      expect(isDeepEqual({ a: 1, b: 2 }, { a: 1, b: 3 }).isEqual).toBe(false)
      expect(isDeepEqual({ a: 1 }, { a: 1, b: 2 }).isEqual).toBe(false)
      expect(isDeepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }).isEqual).toBe(true) // order doesn't matter
    })

    it('should handle arrays with objects', () => {
      expect(isDeepEqual([{ a: 1 }], [{ a: 1 }]).isEqual).toBe(true)
      expect(isDeepEqual([{ a: 1 }], [{ a: 2 }]).isEqual).toBe(false)
    })

    it('should handle objects with arrays', () => {
      expect(isDeepEqual({ a: [1, 2] }, { a: [1, 2] }).isEqual).toBe(true)
      expect(isDeepEqual({ a: [1, 2] }, { a: [1, 3] }).isEqual).toBe(false)
    })
  })

  // Complex objects tests
  describe('complex nested objects', () => {
    it('should handle deeply nested objects', () => {
      const obj1 = {
        a: 1,
        b: {
          c: [1, 2, 3],
          d: {
            e: 'hello',
            f: {
              g: true,
            },
          },
        },
      }

      const obj2 = {
        a: 1,
        b: {
          c: [1, 2, 3],
          d: {
            e: 'hello',
            f: {
              g: true,
            },
          },
        },
      }

      const obj3 = {
        a: 1,
        b: {
          c: [1, 2, 3],
          d: {
            e: 'hello',
            f: {
              g: false, // Different value here
            },
          },
        },
      }

      expect(isDeepEqual(obj1, obj2).isEqual).toBe(true)
      expect(isDeepEqual(obj1, obj3).isEqual).toBe(false)
    })

    it('should handle circular references', () => {
      const obj1: Record<string, unknown> = { a: 1 }
      const obj2: Record<string, unknown> = { a: 1 }

      // Create circular references
      obj1.self = obj1
      obj2.self = obj2

      // These should fail as we're not specifically handling circular references
      // But we want to make sure it doesn't cause an infinite loop
      expect(() => isDeepEqual(obj1, obj2)).not.toThrow()
    })

    it('should handle arrays with mixed types', () => {
      const arr1 = [1, 'string', true, { a: 1 }, [1, 2]]
      const arr2 = [1, 'string', true, { a: 1 }, [1, 2]]
      const arr3 = [1, 'string', true, { a: 2 }, [1, 2]]

      expect(isDeepEqual(arr1, arr2).isEqual).toBe(true)
      expect(isDeepEqual(arr1, arr3).isEqual).toBe(false)
    })

    it('should handle objects with same content but different property order in nested arrays', () => {
      const obj1 = {
        searches: [
          {
            term: 'areia',
            count: 217,
            attributes: [
              {
                key: 'departamento',
                labelKey: 'Departamento',
                labelValue: 'Material de Construção',
                value: 'material-de-construcao',
              },
              {
                key: 'categoria',
                labelKey: 'Categoria',
                labelValue: 'Materiais básicos para obra',
                value: 'materiais-basicos-para-obra',
              },
              {
                key: 'subcategoria',
                labelKey: 'Subcategoria',
                labelValue: 'Areias',
                value: 'areias',
              },
            ],
          },
          {
            term: 'vaso caixa acoplada',
            count: 239,
          },
          {
            term: 'caixa acoplada',
            count: 86,
          },
          {
            term: 'vasos caixa acopladas',
            count: 66,
          },
          {
            term: 'kit vaso caixa acoplada',
            count: 117,
          },
        ],
      }

      const obj2 = {
        searches: [
          {
            term: 'areia',
            count: 217,
            attributes: [
              {
                key: 'departamento',
                value: 'material-de-construcao',
                labelKey: 'Departamento',
                labelValue: 'Material de Construção',
              },
              {
                key: 'categoria',
                value: 'materiais-basicos-para-obra',
                labelKey: 'Categoria',
                labelValue: 'Materiais básicos para obra',
              },
              {
                key: 'subcategoria',
                value: 'areias',
                labelKey: 'Subcategoria',
                labelValue: 'Areias',
              },
            ],
          },
          {
            term: 'vaso caixa acoplada',
            count: 239,
          },
          {
            term: 'caixa acoplada',
            count: 86,
          },
          {
            term: 'vasos caixa acopladas',
            count: 66,
          },
          {
            term: 'kit vaso caixa acoplada',
            count: 117,
          },
        ],
      }

      // These objects should be considered equal since they have the same content
      // but different property order in the attributes objects
      expect(isDeepEqual(obj1, obj2).isEqual).toBe(true)
    })
  })

  // maxDepth parameter tests
  describe('maxDepth parameter', () => {
    it('should respect the maxDepth parameter for nested objects', () => {
      const deepObj1 = {
        level1: {
          level2: {
            level3: {
              level4: {
                value: 'deep',
              },
            },
          },
        },
      }

      const deepObj2 = {
        level1: {
          level2: {
            level3: {
              level4: {
                value: 'different',
              },
            },
          },
        },
      }

      // With default depth (20), the difference should be detected
      expect(isDeepEqual(deepObj1, deepObj2).isEqual).toBe(false)

      // With depth of 3, the difference at level4 should not be detected
      expect(isDeepEqual(deepObj1, deepObj2, 3).isEqual).toBe(true)

      // With depth of 4, the difference should be detected
      expect(isDeepEqual(deepObj1, deepObj2, 4).isEqual).toBe(false)
    })

    it('should respect the maxDepth parameter for nested arrays', () => {
      const nestedArray1 = [1, [2, [3, [4, [5, 6]]]]]
      const nestedArray2 = [1, [2, [3, [4, [5, 7]]]]]

      // With default depth, the difference should be detected
      expect(isDeepEqual(nestedArray1, nestedArray2).isEqual).toBe(false)

      // With depth of 4, the difference at the deepest level should not be detected
      expect(isDeepEqual(nestedArray1, nestedArray2, 3).isEqual).toBe(true)

      // With depth of 5, the difference should be detected
      expect(isDeepEqual(nestedArray1, nestedArray2, 4).isEqual).toBe(false)
    })
  })

  describe('difference detection', () => {
    it('should return differences when using returnDifferences option', () => {
      const obj1 = { name: 'John', age: 25 }
      const obj2 = { name: 'Jane', age: 30 }

      const result = isDeepEqual(obj1, obj2)

      expect(result.isEqual).toBe(false)
      expect(result.differences).toHaveLength(2)
      expect(result.differences).toEqual([
        {
          path: 'name',
          type: 'different_value',
          expected: 'John',
          actual: 'Jane',
        },
        {
          path: 'age',
          type: 'different_value',
          expected: 25,
          actual: 30,
        },
      ])
    })

    it('should return no differences when objects are equal', () => {
      const obj1 = { name: 'John', age: 25 }
      const obj2 = { name: 'John', age: 25 }

      const result = isDeepEqual(obj1, obj2)

      expect(result.isEqual).toBe(true)
      expect(result.differences).toHaveLength(0)
    })

    it('should detect missing and extra keys', () => {
      const obj1 = { name: 'John', age: 25 }
      const obj2 = { name: 'John', email: 'john@example.com' }

      const result = isDeepEqual(obj1, obj2)

      expect(result.isEqual).toBe(false)
      expect(result.differences).toHaveLength(2)
      expect(result.differences).toEqual(
        expect.arrayContaining([
          {
            path: 'age',
            type: 'extra_key',
            expected: 25,
          },
          {
            path: 'email',
            type: 'missing_key',
            actual: 'john@example.com',
          },
        ])
      )
    })

    it('should detect nested differences with correct paths', () => {
      const obj1 = {
        user: {
          profile: {
            name: 'John',
            preferences: { theme: 'dark' },
          },
        },
      }

      const obj2 = {
        user: {
          profile: {
            name: 'Jane',
            preferences: { theme: 'light' },
          },
        },
      }

      const result = isDeepEqual(obj1, obj2)

      expect(result.isEqual).toBe(false)
      expect(result.differences).toHaveLength(2)
      expect(result.differences).toEqual([
        {
          path: 'user.profile.name',
          type: 'different_value',
          expected: 'John',
          actual: 'Jane',
        },
        {
          path: 'user.profile.preferences.theme',
          type: 'different_value',
          expected: 'dark',
          actual: 'light',
        },
      ])
    })

    it('should detect array differences', () => {
      const arr1 = [1, 2, { value: 'a' }]
      const arr2 = [1, 3, { value: 'b' }]

      const result = isDeepEqual(arr1, arr2)

      expect(result.isEqual).toBe(false)
      expect(result.differences).toHaveLength(2)
      expect(result.differences).toEqual([
        {
          path: '[1]',
          type: 'different_value',
          expected: 2,
          actual: 3,
        },
        {
          path: '[2].value',
          type: 'different_value',
          expected: 'a',
          actual: 'b',
        },
      ])
    })

    it('should detect type differences', () => {
      const obj1 = { value: 'string' }
      const obj2 = { value: 42 }

      const result = isDeepEqual(obj1, obj2)

      expect(result.isEqual).toBe(false)
      expect(result.differences).toHaveLength(1)
      expect(result.differences[0]).toEqual({
        path: 'value',
        type: 'different_type',
        expected: 'string',
        actual: 'number',
      })
    })
  })
})

describe('findDifferences', () => {
  it('should find differences between simple objects', () => {
    const obj1 = { a: 1, b: 2 }
    const obj2 = { a: 1, b: 3, c: 4 }

    const differences = findDifferences(obj1, obj2)

    expect(differences).toHaveLength(2)
    expect(differences).toEqual(
      expect.arrayContaining([
        {
          path: 'b',
          type: 'different_value',
          expected: 2,
          actual: 3,
        },
        {
          path: 'c',
          type: 'missing_key',
          actual: 4,
        },
      ])
    )
  })

  it('should respect maxDepth parameter', () => {
    const deep1 = { level1: { level2: { level3: { value: 'a' } } } }
    const deep2 = { level1: { level2: { level3: { value: 'b' } } } }

    const differences = findDifferences(deep1, deep2, '', 2)

    expect(differences).toHaveLength(0) // Should not find differences beyond maxDepth
  })
})

describe('filterIgnoredDifferences', () => {
  describe('basic filtering', () => {
    it('should return all differences when no ignored differences are provided', () => {
      const differences: ObjectDifference[] = [
        {
          path: 'name',
          type: 'different_value',
          expected: 'John',
          actual: 'Jane',
        },
        { path: 'age', type: 'extra_key', expected: 25 },
      ]

      const result = filterIgnoredDifferences(differences, [])

      expect(result).toEqual(differences)
    })

    it('should return all differences when ignoredDifferences is undefined', () => {
      const differences: ObjectDifference[] = [
        {
          path: 'name',
          type: 'different_value',
          expected: 'John',
          actual: 'Jane',
        },
        { path: 'age', type: 'extra_key', expected: 25 },
      ]

      const result = filterIgnoredDifferences(differences)

      expect(result).toEqual(differences)
    })

    it('should filter out exact path and type matches', () => {
      const differences: ObjectDifference[] = [
        {
          path: 'name',
          type: 'different_value',
          expected: 'John',
          actual: 'Jane',
        },
        { path: 'age', type: 'extra_key', expected: 25 },
        { path: 'email', type: 'missing_key', actual: 'john@example.com' },
      ]

      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'age', type: 'extra_key' },
        { path: 'email', type: 'missing_key' },
      ]

      const result = filterIgnoredDifferences(differences, ignoredDifferences)

      expect(result).toEqual([
        {
          path: 'name',
          type: 'different_value',
          expected: 'John',
          actual: 'Jane',
        },
      ])
    })

    it('should not filter out differences with same path but different type', () => {
      const differences: ObjectDifference[] = [
        {
          path: 'value',
          type: 'different_value',
          expected: 'John',
          actual: 'Jane',
        },
        {
          path: 'value',
          type: 'different_type',
          expected: 'string',
          actual: 'number',
        },
      ]

      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'value', type: 'different_value' },
      ]

      const result = filterIgnoredDifferences(differences, ignoredDifferences)

      expect(result).toEqual([
        {
          path: 'value',
          type: 'different_type',
          expected: 'string',
          actual: 'number',
        },
      ])
    })

    it('should not filter out differences with same type but different path', () => {
      const differences: ObjectDifference[] = [
        { path: 'name', type: 'extra_key', expected: 'John' },
        { path: 'age', type: 'extra_key', expected: 25 },
      ]

      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'name', type: 'extra_key' },
      ]

      const result = filterIgnoredDifferences(differences, ignoredDifferences)

      expect(result).toEqual([{ path: 'age', type: 'extra_key', expected: 25 }])
    })
  })

  describe('array path filtering', () => {
    it('should filter array index paths correctly', () => {
      const differences: ObjectDifference[] = [
        { path: '[0].productReferenceCode', type: 'extra_key', expected: null },
        { path: '[0].brandImageUrl', type: 'extra_key', expected: null },
        {
          path: '[1].productReference',
          type: 'different_value',
          expected: 'ABC',
          actual: 'DEF',
        },
      ]

      const ignoredDifferences: IgnoredDifference[] = [
        { path: '[0].productReferenceCode', type: 'extra_key' },
        { path: '[0].brandImageUrl', type: 'extra_key' },
      ]

      const result = filterIgnoredDifferences(differences, ignoredDifferences)

      expect(result).toEqual([
        {
          path: '[1].productReference',
          type: 'different_value',
          expected: 'ABC',
          actual: 'DEF',
        },
      ])
    })

    it('should handle nested array paths', () => {
      const differences: ObjectDifference[] = [
        { path: 'data[0].items[2].specs', type: 'missing_key', actual: {} },
        {
          path: 'data[1].categories[0]',
          type: 'different_value',
          expected: 'A',
          actual: 'B',
        },
      ]

      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'data[0].items[2].specs', type: 'missing_key' },
      ]

      const result = filterIgnoredDifferences(differences, ignoredDifferences)

      expect(result).toEqual([
        {
          path: 'data[1].categories[0]',
          type: 'different_value',
          expected: 'A',
          actual: 'B',
        },
      ])
    })
  })

  describe('complex nested paths', () => {
    it('should filter deeply nested object paths', () => {
      const differences: ObjectDifference[] = [
        {
          path: 'user.profile.preferences.theme',
          type: 'different_value',
          expected: 'dark',
          actual: 'light',
        },
        {
          path: 'user.profile.avatar',
          type: 'extra_key',
          expected: 'avatar.jpg',
        },
        {
          path: 'user.settings.notifications',
          type: 'missing_key',
          actual: true,
        },
      ]

      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'user.profile.avatar', type: 'extra_key' },
        { path: 'user.settings.notifications', type: 'missing_key' },
      ]

      const result = filterIgnoredDifferences(differences, ignoredDifferences)

      expect(result).toEqual([
        {
          path: 'user.profile.preferences.theme',
          type: 'different_value',
          expected: 'dark',
          actual: 'light',
        },
      ])
    })
  })

  describe('all difference types', () => {
    it('should handle all supported difference types', () => {
      const differences: ObjectDifference[] = [
        { path: 'missing', type: 'missing_key', actual: 'value' },
        { path: 'extra', type: 'extra_key', expected: 'value' },
        {
          path: 'different',
          type: 'different_value',
          expected: 'old',
          actual: 'new',
        },
        {
          path: 'type',
          type: 'different_type',
          expected: 'string',
          actual: 'number',
        },
        {
          path: 'array',
          type: 'array_length_mismatch',
          expected: 3,
          actual: 5,
        },
      ]

      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'missing', type: 'missing_key' },
        { path: 'extra', type: 'extra_key' },
        { path: 'type', type: 'different_type' },
        { path: 'array', type: 'array_length_mismatch' },
      ]

      const result = filterIgnoredDifferences(differences, ignoredDifferences)

      expect(result).toEqual([
        {
          path: 'different',
          type: 'different_value',
          expected: 'old',
          actual: 'new',
        },
      ])
    })
  })

  describe('edge cases', () => {
    it('should handle empty differences array', () => {
      const differences: any[] = []
      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'any', type: 'extra_key' },
      ]

      const result = filterIgnoredDifferences(differences, ignoredDifferences)

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

    it('should handle empty ignored differences array', () => {
      const differences: ObjectDifference[] = [
        {
          path: 'name',
          type: 'different_value',
          expected: 'John',
          actual: 'Jane',
        },
      ]

      const result = filterIgnoredDifferences(differences, [])

      expect(result).toEqual(differences)
    })

    it('should filter all differences when all are ignored', () => {
      const differences: ObjectDifference[] = [
        {
          path: 'name',
          type: 'different_value',
          expected: 'John',
          actual: 'Jane',
        },
        { path: 'age', type: 'extra_key', expected: 25 },
      ]

      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'name', type: 'different_value' },
        { path: 'age', type: 'extra_key' },
      ]

      const result = filterIgnoredDifferences(differences, ignoredDifferences)

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

describe('compareApiResults', () => {
  const mockLogger = {
    error: jest.fn(),
    warn: jest.fn(),
    info: jest.fn(),
    debug: jest.fn(),
    account: '',
    workspace: '',
    operationId: '',
    requestId: '',
    traceId: '',
    transactionId: '',
    config: {},
    child: jest.fn(),
    addTags: jest.fn(),
  } as unknown as Logger

  let originalMathRandom: () => number

  beforeAll(() => {
    // Store original Math.random
    originalMathRandom = Math.random
  })

  afterAll(() => {
    // Restore original Math.random
    Math.random = originalMathRandom
  })

  beforeEach(() => {
    jest.clearAllMocks()
    // Reset Math.random to original implementation
    Math.random = originalMathRandom
  })

  describe('sampling functionality', () => {
    it('should only call func1 when sample is 0', async () => {
      const func1 = jest.fn().mockResolvedValue('result1')
      const func2 = jest.fn().mockResolvedValue('result2')

      const result = await compareApiResults(func1, func2, 0, mockLogger)

      expect(func1).toHaveBeenCalledTimes(1)
      expect(func2).not.toHaveBeenCalled()
      expect(result).toBe('result1')
      expect(mockLogger.error).not.toHaveBeenCalled()
    })

    it('should call both functions when sample is 100', async () => {
      const func1 = jest.fn().mockResolvedValue('result1')
      const func2 = jest.fn().mockResolvedValue('result1')

      const result = await compareApiResults(func1, func2, 100, mockLogger)

      expect(func1).toHaveBeenCalledTimes(1)
      expect(func2).toHaveBeenCalledTimes(1)
      expect(result).toBe('result1')
    })

    it('should only call func1 when Math.random indicates not in sample', async () => {
      // Mock Math.random to return 0.6 (60%), which should be >= 50% sample
      jest.spyOn(Math, 'random').mockImplementation().mockReturnValue(0.6)

      const func1 = jest.fn().mockResolvedValue('result1')
      const func2 = jest.fn().mockResolvedValue('result2')

      // 50% sample rate, but random returns 60%, so should not be in sample
      const result = await compareApiResults(func1, func2, 50, mockLogger)

      expect(func1).toHaveBeenCalledTimes(1)
      expect(func2).not.toHaveBeenCalled()
      expect(result).toBe('result1')
    })

    it('should call both functions when Math.random indicates in sample', async () => {
      // Mock Math.random to return 0.3 (30%), which should be < 50% sample
      jest.spyOn(Math, 'random').mockImplementation().mockReturnValue(0.3)

      const func1 = jest.fn().mockResolvedValue('result1')
      const func2 = jest.fn().mockResolvedValue('result1')

      // 50% sample rate, and random returns 30%, so should be in sample
      const result = await compareApiResults(func1, func2, 50, mockLogger)

      expect(func1).toHaveBeenCalledTimes(1)
      expect(func2).toHaveBeenCalledTimes(1)
      expect(result).toBe('result1')
    })
  })

  describe('comparison logic when in sample', () => {
    beforeEach(() => {
      // Always be in sample for these tests
      jest.spyOn(Math, 'random').mockImplementation().mockReturnValue(0.1)
    })

    it('should return result1 when both functions succeed and results are equal', async () => {
      const func1 = jest.fn().mockResolvedValue({ data: 'test' })
      const func2 = jest.fn().mockResolvedValue({ data: 'test' })

      const result = await compareApiResults(func1, func2, 100, mockLogger)

      expect(result).toEqual({ data: 'test' })
      expect(mockLogger.error).not.toHaveBeenCalled()
    })

    it('should log error when results differ', async () => {
      const func1 = jest.fn().mockResolvedValue({ data: 'test1' })
      const func2 = jest.fn().mockResolvedValue({ data: 'test2' })

      const result = await compareApiResults(func1, func2, 100, mockLogger, {
        args: { query: 'test' },
        logPrefix: 'Test API',
      })

      expect(result).toEqual({ data: 'test1' })
      expect(mockLogger.error).toHaveBeenCalledWith({
        message: 'Test API: Results differ',
        params: JSON.stringify({ query: 'test' }),
        differences: [
          {
            path: 'data',
            type: 'different_value',
            expected: 'test1',
            actual: 'test2',
          },
        ],
        differenceCount: 1,
        totalDifferences: 1,
        ignoredDifferences: 0,
      })
    })

    it('should return result2 when func1 errors but func2 succeeds', async () => {
      const func1 = jest.fn().mockRejectedValue(new Error('func1 error'))
      const func2 = jest.fn().mockResolvedValue({ data: 'test2' })

      const result = await compareApiResults(func1, func2, 100, mockLogger)

      expect(result).toEqual({ data: 'test2' })
    })

    it('should throw error when both functions fail', async () => {
      const func1 = jest.fn().mockRejectedValue(new Error('func1 error'))
      const func2 = jest.fn().mockRejectedValue(new Error('func2 error'))

      await expect(
        compareApiResults(func1, func2, 100, mockLogger)
      ).rejects.toThrow('Both calls resulted in errors')
    })
  })

  describe('ignored differences functionality', () => {
    beforeEach(() => {
      // Always be in sample for these tests
      jest.spyOn(Math, 'random').mockImplementation().mockReturnValue(0.1)
    })

    it('should consider results equal when all differences are ignored', async () => {
      const func1 = jest.fn().mockResolvedValue({
        name: 'John',
        age: 25,
        extra: 'field',
      })

      const func2 = jest.fn().mockResolvedValue({
        name: 'Jane',
        age: 30,
      })

      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'name', type: 'different_value' },
        { path: 'age', type: 'different_value' },
        { path: 'extra', type: 'extra_key' },
      ]

      const result = await compareApiResults(func1, func2, 100, mockLogger, {
        ignoredDifferences,
        logPrefix: 'Test API',
      })

      expect(result).toEqual({ name: 'John', age: 25, extra: 'field' })
      expect(mockLogger.error).not.toHaveBeenCalled()
      expect(mockLogger.info).toHaveBeenCalledWith({
        message: 'Test API: Results are equal',
        params: undefined,
        totalDifferences: 3,
        ignoredDifferences: 3,
      })
    })

    it('should log differences after filtering out ignored ones', async () => {
      const func1 = jest.fn().mockResolvedValue([
        {
          productReferenceCode: null,
          brandImageUrl: null,
          name: 'Product A',
          price: 100,
        },
      ])

      const func2 = jest.fn().mockResolvedValue([
        {
          name: 'Product B',
          price: 150,
        },
      ])

      const ignoredDifferences: IgnoredDifference[] = [
        { path: '[0].productReferenceCode', type: 'extra_key' },
        { path: '[0].brandImageUrl', type: 'extra_key' },
      ]

      const result = await compareApiResults(func1, func2, 100, mockLogger, {
        ignoredDifferences,
        args: { query: 'test product' },
        logPrefix: 'Product API',
      })

      expect(result).toEqual([
        {
          productReferenceCode: null,
          brandImageUrl: null,
          name: 'Product A',
          price: 100,
        },
      ])

      expect(mockLogger.error).toHaveBeenCalledWith({
        message: 'Product API: Results differ',
        params: JSON.stringify({ query: 'test product' }),
        differences: [
          {
            path: '[0].name',
            type: 'different_value',
            expected: 'Product A',
            actual: 'Product B',
          },
          {
            path: '[0].price',
            type: 'different_value',
            expected: 100,
            actual: 150,
          },
        ],
        differenceCount: 2,
        totalDifferences: 4,
        ignoredDifferences: 2,
      })
    })

    it('should work with complex nested paths as in the example', async () => {
      const func1 = jest.fn().mockResolvedValue([
        {
          productReferenceCode: null,
          brandImageUrl: null,
          releaseDate: '2023-12-01',
          clusterHighlights: { '259': 'Solução Completa' },
          productClusters: { '260': 'Campanha' },
          searchableClusters: {
            '259': 'Solução Completa',
            '260': 'Campanha Pais Colaborador',
          },
          categories: [
            '/Segurança eletrônica/Câmeras de segurança/Câmeras Wi-fi/',
          ],
          categoriesIds: ['/2/33/101/'],
        },
      ])

      const func2 = jest.fn().mockResolvedValue([
        {
          releaseDate: 1702857600,
          clusterHighlights: ['Solução Completa'],
          productClusters: ['Campanha'],
          categories: ['/Segurança eletrônica/'],
          categoriesIds: ['2'],
        },
      ])

      const ignoredDifferences: IgnoredDifference[] = [
        { path: '[0].productReferenceCode', type: 'extra_key' },
        { path: '[0].brandImageUrl', type: 'extra_key' },
        { path: '[0].searchableClusters', type: 'extra_key' },
        { path: '[0].releaseDate', type: 'different_type' },
        { path: '[0].clusterHighlights', type: 'different_type' },
        { path: '[0].productClusters', type: 'different_type' },
      ]

      const result = await compareApiResults(func1, func2, 100, mockLogger, {
        ignoredDifferences,
        logPrefix: 'Complex Product API',
      })

      expect(result).toBeDefined()
      // Should still log errors for categories and categoriesIds differences
      expect(mockLogger.error).toHaveBeenCalledWith(
        expect.objectContaining({
          message: 'Complex Product API: Results differ',
          differenceCount: 2, // Only categories and categoriesIds differences remain
          totalDifferences: 8, // Total before filtering
          ignoredDifferences: 6, // Number filtered out
        })
      )
    })

    it('should handle empty ignored differences array', async () => {
      const func1 = jest.fn().mockResolvedValue({ name: 'John' })
      const func2 = jest.fn().mockResolvedValue({ name: 'Jane' })

      const result = await compareApiResults(func1, func2, 100, mockLogger, {
        ignoredDifferences: [],
      })

      expect(result).toEqual({ name: 'John' })
      expect(mockLogger.error).toHaveBeenCalled()
    })

    it('should handle undefined ignored differences', async () => {
      const func1 = jest.fn().mockResolvedValue({ name: 'John' })
      const func2 = jest.fn().mockResolvedValue({ name: 'Jane' })

      const result = await compareApiResults(func1, func2, 100, mockLogger, {
        // No ignoredDifferences property
      })

      expect(result).toEqual({ name: 'John' })
      expect(mockLogger.error).toHaveBeenCalled()
    })

    it('should handle mixed difference types in ignored differences', async () => {
      const func1 = jest.fn().mockResolvedValue({
        user: {
          name: 'John',
          avatar: 'avatar.jpg',
          settings: { theme: 'dark' },
        },
        items: [1, 2, 3],
      })

      const func2 = jest.fn().mockResolvedValue({
        user: {
          name: 'John',
          settings: { theme: 'light', notifications: true },
        },
        items: [1, 2],
      })

      const ignoredDifferences: IgnoredDifference[] = [
        { path: 'user.avatar', type: 'extra_key' },
        { path: 'user.settings.notifications', type: 'missing_key' },
        { path: 'items', type: 'array_length_mismatch' },
        { path: 'items[2]', type: 'extra_key' },
      ]

      const result = await compareApiResults(func1, func2, 100, mockLogger, {
        ignoredDifferences,
        logPrefix: 'Mixed Types API',
      })

      expect(result).toBeDefined()
      // Should still have one difference for theme
      expect(mockLogger.error).toHaveBeenCalledWith(
        expect.objectContaining({
          message: 'Mixed Types API: Results differ',
          differenceCount: 1, // Only theme difference remains
          totalDifferences: 5, // Total before filtering
          ignoredDifferences: 4, // Number filtered out
        })
      )
    })
  })
})

```


[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/utils/categoryTreeSearch.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import * as TypeMoq from 'typemoq'
import { CategoryTreeSegmentsFinder } from "./CategoryTreeSegmentsFinder"
import { VBase, IOContext } from "@vtex/api"
import { Search } from "../clients/search"

/*  CategoryTree
 *         c0
 *        / \
 *      c1   c2
 *     / \
 *   c3   c4
 *         \
 *          c5
 */

const context = TypeMoq.Mock.ofType<IOContext>()
const vbaseMock = TypeMoq.Mock.ofInstance(VBase)
const searchMock = TypeMoq.Mock.ofInstance(Search)

const c5 = {'c5': '5'}
const c4 = {'c4': '4'}
const c3 = {'c3': '3'}
const c2 = {'c2': '2'}
const c1 = { 'c1': '1'}
const c0 = { 'c0': '0' }
const root = {'c0': {id: 0, name: 'c0', hasChildren: true}}

const childrenTree = {
  "0": {...c1, ...c2},
  "1": {...c3, ...c4},
  "2": {},
  "3": {},
  "4": {...c5},
  "5": {}
} as any

const clients = {vbase: new vbaseMock.object(context.object), search: new searchMock.object(context.object) }

const toPair = (categories: any[]) =>
  categories.map(c => {
    const [key, value] = c && Object.entries(c)[0] || []
    return key? { id: value, name: key }: null
  })

describe('Category Tree Search tests', () => {
  const finder = class CategoryTreeSegmentsFinderMock extends CategoryTreeSegmentsFinder {
    public constructor(segments: string[]){
      super(clients, segments)
      this.categoryTreeRoot = root
    }

    protected lazyFetchChildren = async (id: number) => {
      const children = childrenTree[id.toString()]
      return children
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    protected staleWhileRevalidate = async <T>(_: string, __: string, func: (params?: any) => Promise<T>, params?: any) => {
      return func(params)
    }

    protected getCategoryTreeRoot = () => {
      return this.categoryTreeRoot as any
    }
  }
  test('It should find a complete tree of categories', async () => {
    const segments = 'c0/c1/c4'.split('/')
    const treeFinder = new finder(segments)
    const result = await treeFinder.find()
    expect(result).toStrictEqual(toPair([c0, c1, c4]))
  })

  test('It should find the maximum categories possible ', async () => {
    const segments = 'c0/c1/c2'.split('/')
    const treeFinder = new finder(segments)
    const result = await treeFinder.find()
    expect(result).toStrictEqual(toPair([c0, c1, null]))
  })

  test('It should not find when doesnt exist', async () => {
    const segments = 'x/c1/c4'.split('/')
    const treeFinder = new finder(segments)
    const result = await treeFinder.find()
    expect(result).toStrictEqual([])
  })
})
```


[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/index.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { fieldResolvers as benefitsFieldResolvers } from './benefits'
import {
  fieldResolvers as searchFieldResolvers,
  queries as searchQueries,
} from './search'
import { queries as statsQueries } from './stats'

export const resolvers = {
  ...searchFieldResolvers,
  ...benefitsFieldResolvers,
  Query: {
    ...searchQueries,
    ...statsQueries,
  },
}

```


[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/routes/workspaceSearchParams.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
const BUCKET = 'workspace-search-params'
const MASTER = 'master'

function disableCache(ctx: Context) {
    ctx.set('Cache-control', 'no-cache,no-store')
}

export async function getWorkspaceSearchParamsFromStorage(ctx: Context) {
    const masterParams = await ctx.clients.vbase.getJSON<object>(BUCKET, MASTER, true)
    if (ctx.vtex.workspace == MASTER) return masterParams
    const filePath = ctx.vtex.workspace
    const params = await ctx.clients.vbase.getJSON<object>(BUCKET, filePath, true)
    // Logic: workspaces inherit params from master, and can override as desired
    return {
        ...masterParams,
        ...params
    }
}

export async function getWorkspaceSearchParams(ctx: Context, next: () => Promise<object>) {
    const data = await getWorkspaceSearchParamsFromStorage(ctx)
    disableCache(ctx)
    ctx.body = data
    ctx.status = 200
    await next()
}

export async function setWorkspaceSearchParams(ctx: Context, next: () => Promise<object>) {
    const filePath = ctx.vtex.workspace
    await ctx.clients.vbase.saveJSON<object>(BUCKET, filePath, ctx.query)
    disableCache(ctx)
    ctx.body = ctx.query
    ctx.status = 200
    await next()
}

```


[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/services/facets.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { buildAttributePath } from '../commons/compatibility-layer'
import { compareApiResults } from '../utils/compareResults'
import { fetchAppSettings } from './settings'
import type { FacetsInput } from '../typings/Search'
import { decodeQuery } from '../clients/intelligent-search-api'
import { parseState } from '../utils/searchState'

/**
 * Builds a query string from an object of params, filtering out undefined/null values
 */
function buildQueryString(params: Record<string, any>): string {
  const searchParams = new URLSearchParams()

  for (const [key, value] of Object.entries(params)) {
    if (value !== undefined && value !== null && value !== '') {
      searchParams.append(key, String(value))
    }
  }

  return searchParams.toString()
}

/**
 * Builds curl commands for debugging API requests
 */
function buildCurlCommands(
  ctx: Context,
  path: string,
  params: Record<string, any>,
  shippingOptions?: string[]
): { biggyCurl: string; intschCurl: string } {
  const { workspace, account } = ctx.vtex
  const queryString = buildQueryString(params)
  const shippingHeader = shippingOptions?.length
    ? ` -H "x-vtex-shipping-options: ${shippingOptions.join(',')}"`
    : ''

  const biggyCurl = `curl "https://${workspace}--${account}.myvtex.com/_v/api/intelligent-search/facets/${path}?${queryString}"${shippingHeader}`
  const intschCurl = `curl "https://${account}.myvtex.com/api/intelligent-search/v0/facets/${path}?${queryString}"${shippingHeader}`

  return { biggyCurl, intschCurl }
}

type FetchFacetsOptions = {
  args: FacetsInput
  selectedFacets: SelectedFacet[]
  shippingOptions?: string[]
}

/**
 * Fetches facets using the intelligentSearchApi client (Biggy)
 */
async function fetchFacetsFromBiggy(ctx: Context, options: FetchFacetsOptions) {
  const { args, selectedFacets, shippingOptions } = options
  const {
    clients: { intelligentSearchApi },
  } = ctx

  const biggyArgs: { [key: string]: any } = {
    ...args,
  }

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

  const result: any = await intelligentSearchApi.facets(
    { ...biggyArgs, query: args.fullText },
    buildAttributePath(selectedFacets),
    shippingOptions
  )

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

  return result
}

/**
 * Fetches facets using the intsch client (Intelligent Search)
 */
async function fetchFacetsFromIntsch(
  ctx: Context,
  options: FetchFacetsOptions
) {
  const { args, selectedFacets, shippingOptions } = options
  const {
    clients: { intsch },
  } = ctx

  const intschArgs: { [key: string]: any } = {
    ...args,
  }

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

  const result: any = await intsch.facets(
    { ...intschArgs, query: args.fullText },
    buildAttributePath(selectedFacets),
    shippingOptions
  )

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

  return result
}

/**
 * Facets service that extracts facets fetching logic and implements comparison or flag-based routing
 */
export async function fetchFacets(ctx: Context, options: FetchFacetsOptions) {
  const { args, selectedFacets, shippingOptions } = options
  const { shouldUseNewPLPEndpoint } = await fetchAppSettings(ctx)

  // If flag is explicitly true, use intsch
  if (shouldUseNewPLPEndpoint) {
    return fetchFacetsFromIntsch(ctx, options)
  }

  // Build the exact request params as the clients do for debugging
  const path = buildAttributePath(selectedFacets)
  const clientArgs: { [key: string]: any } = { ...args }

  delete clientArgs.selectedFacets

  const query = args.fullText
  const { leap, searchState } = args as { leap?: boolean; searchState?: string }

  const requestParams = {
    ...clientArgs,
    query: query && decodeQuery(query),
    locale: ctx.vtex.locale ?? ctx.vtex.tenant?.locale,
    bgy_leap: leap ? true : undefined,
    ...parseState(searchState),
  }

  const { biggyCurl, intschCurl } = buildCurlCommands(
    ctx,
    path,
    requestParams,
    shippingOptions
  )

  // If flag is undefined, compare both APIs
  return compareApiResults(
    () => fetchFacetsFromBiggy(ctx, options),
    () => fetchFacetsFromIntsch(ctx, options),
    ctx.vtex.production ? 1 : 100,
    ctx.vtex.logger,
    {
      logPrefix: 'Facets',
      args: {
        biggyCurl,
        intschCurl,
      },
    }
  )
}

```


[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/services/autocomplete.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
export function fetchAutocompleteSuggestions(ctx: Context, query: string) {
  const { intsch } = ctx.clients

  const args = { query }

  const locale = (ctx.vtex.segment?.cultureInfo ??
    ctx.vtex.tenant?.locale ??
    ctx.vtex.locale) as string

  return intsch.fetchAutocompleteSuggestionsV1({ ...args, locale })
}

export function fetchTopSearches(ctx: Context) {
  const { intsch } = ctx.clients

  const locale = (ctx.vtex.segment?.cultureInfo ??
    ctx.vtex.tenant?.locale ??
    ctx.vtex.locale) as string

  return intsch.fetchTopSearchesV1(locale)
}

export function fetchSearchSuggestions(ctx: Context, query: string) {
  const { intsch } = ctx.clients

  const args = { query }

  const locale = (ctx.vtex.segment?.cultureInfo ??
    ctx.vtex.tenant?.locale ??
    ctx.vtex.locale) as string

  return intsch.fetchSearchSuggestionsV1({ ...args, locale })
}

export function fetchCorrection(ctx: Context, query: string) {
  const { intsch } = ctx.clients

  const args = { query }

  const locale = (ctx.vtex.segment?.cultureInfo ??
    ctx.vtex.tenant?.locale ??
    ctx.vtex.locale) as string

  return intsch.fetchCorrectionV1({ ...args, locale })
}

```


[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/services/productSearch.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { fetchProductSearch } from './productSearch'
import { createContext } from '../mocks/contextFactory'
import type { ProductSearchInput } from '../typings/Search'
import * as compareResultsModule from '../utils/compareResults'

describe('fetchProductSearch service', () => {
  const mockProductSearchResponse = {
    products: [] as any,
    recordsFiltered: 2,
  }

  const mockArgs: ProductSearchInput = {
    fullText: 'test query',
    query: 'test',
    map: 'ft',
    selectedFacets: [],
    orderBy: 'OrderByTopSaleDESC',
    from: 0,
    to: 10,
    fuzzy: '0',
    operator: 'and',
    productOriginVtex: false,
    simulationBehavior: 'default',
    hideUnavailableItems: false,
  }

  const mockSelectedFacets: SelectedFacet[] = []

  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('should use intsch when shouldUseNewPLPEndpoint is true', async () => {
    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: true,
      },
      intschSettings: {
        productSearch: mockProductSearchResponse,
      },
    })

    const result = await fetchProductSearch(ctx, mockArgs, mockSelectedFacets)

    expect(ctx.clients.intsch.productSearch).toHaveBeenCalled()
    expect(
      ctx.clients.intelligentSearchApi.productSearch
    ).not.toHaveBeenCalled()
    expect(result).toEqual({
      searchState: undefined,
      ...mockProductSearchResponse,
    })
  })

  it('should compare both APIs when shouldUseNewPLPEndpoint is undefined', async () => {
    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: undefined,
      },
      intelligentSearchApiSettings: {
        productSearch: mockProductSearchResponse,
      },
      intschSettings: {
        productSearch: mockProductSearchResponse,
      },
    })

    const mockResult = {
      searchState: undefined,
      ...mockProductSearchResponse,
    }

    const compareApiResultsSpy = jest
      .spyOn(compareResultsModule, 'compareApiResults')
      .mockResolvedValue(mockResult)

    const result = await fetchProductSearch(ctx, mockArgs, mockSelectedFacets)

    expect(compareApiResultsSpy).toHaveBeenCalled()
    expect(result).toEqual(mockResult)

    compareApiResultsSpy.mockRestore()
  })

  it('should handle shipping options correctly', async () => {
    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: false,
      },
      intelligentSearchApiSettings: {
        productSearch: mockProductSearchResponse,
      },
    })

    const shippingOptions = ['delivery', 'pickup']

    await fetchProductSearch(ctx, mockArgs, mockSelectedFacets, shippingOptions)

    expect(ctx.clients.intelligentSearchApi.productSearch).toHaveBeenCalledWith(
      expect.objectContaining({ query: 'test query' }),
      expect.any(String),
      shippingOptions
    )
  })

  it('should preserve searchState in response', async () => {
    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: false,
      },
      intelligentSearchApiSettings: {
        productSearch: mockProductSearchResponse,
      },
    })

    const argsWithSearchState = {
      ...mockArgs,
      searchState: 'test-search-state',
    }

    const result = await fetchProductSearch(
      ctx,
      argsWithSearchState,
      mockSelectedFacets
    )

    expect(result.searchState).toBe('test-search-state')
  })

  it('should set translated flag in context when tenant is present and productOriginVtex is false', async () => {
    const mockResponseWithTranslated = {
      ...mockProductSearchResponse,
      translated: true,
    } as any

    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: false,
      },
      intelligentSearchApiSettings: {
        productSearch: mockResponseWithTranslated,
      },
      tenantLocale: 'en-US',
    })

    await fetchProductSearch(ctx, mockArgs, mockSelectedFacets)

    expect(ctx.translated).toBe(true)
  })

  it('should not set translated flag when productOriginVtex is true', async () => {
    const mockResponseWithTranslated = {
      ...mockProductSearchResponse,
      translated: true,
    } as any

    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: false,
      },
      intelligentSearchApiSettings: {
        productSearch: mockResponseWithTranslated,
      },
      tenantLocale: 'en-US',
    })

    const argsWithProductOriginVtex = {
      ...mockArgs,
      productOriginVtex: true,
    }

    await fetchProductSearch(ctx, argsWithProductOriginVtex, mockSelectedFacets)

    expect(ctx.translated).toBeUndefined()
  })

  it('should include advertisement options in the request', async () => {
    const ctx = createContext({
      accountName: 'testaccount',
      appSettings: {
        shouldUseNewPLPEndpoint: false,
      },
      intelligentSearchApiSettings: {
        productSearch: mockProductSearchResponse,
      },
    })

    const argsWithAdOptions = {
      ...mockArgs,
      advertisementOptions: {
        showSponsored: true,
        sponsoredCount: 5,
        repeatSponsoredProducts: false,
      },
    }

    await fetchProductSearch(ctx, argsWithAdOptions, mockSelectedFacets)

    expect(ctx.clients.intelligentSearchApi.productSearch).toHaveBeenCalledWith(
      expect.objectContaining({
        showSponsored: true,
        sponsoredCount: 5,
        repeatSponsoredProducts: false,
      }),
      expect.any(String),
      undefined
    )
  })
})

```


[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/services/product.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import type { SegmentData } from '../typings/Search'
import { fetchAppSettings } from './settings'

export type ProductIdentifier = {
  field: 'id' | 'slug' | 'ean' | 'reference' | 'sku'
  value: string
}

export type ProductArgs = {
  slug?: string
  identifier?: ProductIdentifier
  regionId?: string
  salesChannel?: number
}

interface FetchProductArgs {
  identifier: ProductIdentifier
  salesChannel?: number | null
  regionId?: string
  vtexSegment?: string
}

/**
 * Product service that extracts product fetching logic and implements compareApiResults
 * using search client as primary and intsch.fetchProduct as secondary
 */

/**
 * Fetches a product using the search client with different identifier types
 */
async function fetchProductFromSearch(
  ctx: Context,
  args: FetchProductArgs
): Promise<SearchProduct[]> {
  const { search } = ctx.clients
  const { identifier, salesChannel, vtexSegment } = args
  const { field, value } = identifier

  switch (field) {
    case 'id':
      return search.productById(value, vtexSegment, salesChannel)

    case 'slug':
      return search.product(value, vtexSegment, salesChannel)

    case 'ean':
      return search.productByEan(value, vtexSegment, salesChannel)

    case 'reference':
      return search.productByReference(value, vtexSegment, salesChannel)

    case 'sku':
      return search.productBySku(value, vtexSegment, salesChannel)

    default:
      throw new Error(`Unsupported product identifier field: ${field}`)
  }
}

/**
 * Fetches a product using the intsch client
 */
async function fetchProductFromIntsch(
  ctx: Context,
  args: FetchProductArgs
): Promise<SearchProduct[]> {
  const { intsch } = ctx.clients
  const { identifier, salesChannel, regionId, vtexSegment } = args
  const { field, value } = identifier

  // Get locale from context (fallback)
  const defaultLocale = ctx.vtex.tenant?.locale ?? ctx.vtex.locale

  // Default salesChannel from args
  let finalSalesChannel = salesChannel ? `${salesChannel}` : undefined
  let finalLocale = defaultLocale

  // Parse vtexSegment if available to extract locale and salesChannel
  if (vtexSegment) {
    try {
      const segmentData: SegmentData = JSON.parse(
        Buffer.from(vtexSegment, 'base64').toString()
      )

      // Use segment's salesChannel if available, otherwise use the provided one
      if (segmentData.channel) {
        finalSalesChannel = segmentData.channel
      }

      // Use segment's locale (cultureInfo) if available, otherwise use default
      if (segmentData.cultureInfo) {
        finalLocale = segmentData.cultureInfo
      }
    } catch (error) {
      // If parsing fails, continue with defaults
      ctx.vtex.logger.warn({
        message: 'Failed to parse vtexSegment token, using defaults',
        error: error.message,
        vtexSegment,
      })
    }
  }

  const product = await intsch.fetchProduct({
    field,
    value,
    salesChannel: finalSalesChannel?.toString(),
    regionId,
    locale: finalLocale,
  })

  // intsch.fetchProduct returns a single SearchProduct, but we need to return an array
  // to match the search client interface
  return product ? [product] : []
}

/**
 * Builds vtex segment token for product fetching
 */

export function buildVtexSegment({
  vtexSegment,
  salesChannel,
  regionId,
}: {
  vtexSegment?: SegmentData
  salesChannel?: string
  regionId?: string
}): string {
  const cookie = {
    ...vtexSegment,
    regionId: regionId ?? vtexSegment?.regionId,
    channel: salesChannel ?? vtexSegment?.channel,
  }

  return Buffer.from(JSON.stringify(cookie)).toString('base64')
}

export async function fetchProduct(
  ctx: Context,
  args: FetchProductArgs
): Promise<SearchProduct[]> {
  const { shouldUseNewPDPEndpoint } = await fetchAppSettings(ctx)

  // Check if current account should skip comparison and use intsch directly
  if (shouldUseNewPDPEndpoint) {
    ctx.translated = true
    return fetchProductFromIntsch(ctx, args)
  }

  return fetchProductFromSearch(ctx, args)
}

/**
 * Helper function to validate product identifier
 */
export function isValidProductIdentifier(
  identifier: ProductIdentifier | undefined
): identifier is ProductIdentifier {
  return (
    identifier?.field !== undefined &&
    identifier.value !== undefined &&
    ['id', 'slug', 'ean', 'reference', 'sku'].includes(identifier.field)
  )
}

export async function resolveProduct(
  ctx: Context,
  rawArgs: ProductArgs
): Promise<SearchProduct | null> {
  const args =
    rawArgs && isValidProductIdentifier(rawArgs.identifier)
      ? rawArgs
      : { identifier: { field: 'slug', value: rawArgs.slug ?? '' } }

  if (!args.identifier?.value) {
    throw new Error('No product identifier provided')
  }

  const cookie: SegmentData | undefined = ctx.vtex.segment as
    | SegmentData
    | undefined

  const { salesChannel } = rawArgs

  const vtexSegment =
    !cookie || (!cookie?.regionId && rawArgs.regionId)
      ? buildVtexSegment({
          vtexSegment: ctx.vtex.segment as SegmentData | undefined,
          salesChannel: rawArgs.salesChannel?.toString(),
          regionId: rawArgs.regionId,
        })
      : ctx.vtex.segmentToken

  const fetchArgs: FetchProductArgs = {
    identifier: args.identifier as ProductIdentifier,
    salesChannel,
    regionId: rawArgs.regionId,
    vtexSegment,
  }

  try {
    const products = await fetchProduct(ctx, fetchArgs)

    if (!products || products.length === 0) {
      return null
    }

    const [product] = products

    return product
  } catch (error) {
    ctx.vtex.logger.error({
      message: 'Error fetching product',
      error: error.message,
      args: fetchArgs,
    })
    throw error
  }
}

export type ProductsByIdentifierArgs = {
  field: 'id' | 'ean' | 'reference' | 'sku'
  values: string[]
  salesChannel?: string | null
  regionId?: string | null
}

/**
 * Fetches multiple products by identifier using the search client
 */
async function fetchProductsByIdentifierFromSearch(
  ctx: Context,
  args: ProductsByIdentifierArgs,
  vtexSegment?: string
): Promise<SearchProduct[]> {
  const { search } = ctx.clients
  const { field, values, salesChannel } = args

  switch (field) {
    case 'id':
      return search.productsById(values, vtexSegment, salesChannel)

    case 'ean':
      return search.productsByEan(values, vtexSegment, salesChannel)

    case 'reference':
      return search.productsByReference(values, vtexSegment, salesChannel)

    case 'sku':
      return search.productsBySku(values, vtexSegment, salesChannel)

    default:
      throw new Error(`Unsupported product identifier field: ${field}`)
  }
}

/**
 * Fetches multiple products by identifier using the intsch client
 */
async function fetchProductsByIdentifierFromIntsch(
  ctx: Context,
  args: ProductsByIdentifierArgs,
  vtexSegment?: string
): Promise<SearchProduct[]> {
  const { intsch } = ctx.clients
  const { field, values, salesChannel, regionId } = args

  // Get locale from context (fallback)
  const defaultLocale = ctx.vtex.tenant?.locale ?? ctx.vtex.locale

  // Default salesChannel from args
  let finalSalesChannel = salesChannel ? `${salesChannel}` : undefined
  let finalLocale = defaultLocale

  // Parse vtexSegment if available to extract locale and salesChannel
  if (vtexSegment) {
    try {
      const segmentData: SegmentData = JSON.parse(
        Buffer.from(vtexSegment, 'base64').toString()
      )

      // Use segment's salesChannel if available, otherwise use the provided one
      if (segmentData.channel) {
        finalSalesChannel = segmentData.channel
      }

      // Use segment's locale (cultureInfo) if available, otherwise use default
      if (segmentData.cultureInfo) {
        finalLocale = segmentData.cultureInfo
      }
    } catch (error) {
      // If parsing fails, continue with defaults
      ctx.vtex.logger.warn({
        message: 'Failed to parse vtexSegment token, using defaults',
        error: error.message,
        vtexSegment,
      })
    }
  }

  // Fetch all products in parallel
  const productPromises = values.map((value) =>
    intsch.fetchProduct({
      field,
      value,
      salesChannel: finalSalesChannel?.toString(),
      regionId: regionId ?? undefined,
      locale: finalLocale,
    })
  )

  const products = await Promise.all(productPromises)

  return products as SearchProduct[]
}

export async function resolveProductsByIdentifier(
  ctx: Context,
  args: ProductsByIdentifierArgs
): Promise<SearchProduct[]> {
  const vtexSegment =
    !ctx.vtex.segment || (!ctx.vtex.segment?.regionId && args.regionId)
      ? buildVtexSegment({
          vtexSegment: ctx.vtex.segment as SegmentData | undefined,
          salesChannel: args.salesChannel?.toString(),
          regionId: args.regionId ?? undefined,
        })
      : ctx.vtex.segmentToken

  const { shouldUseNewPDPEndpoint } = await fetchAppSettings(ctx)

  try {
    let products: SearchProduct[]

    // Check if current account should use intsch directly
    if (shouldUseNewPDPEndpoint) {
      products = await fetchProductsByIdentifierFromIntsch(
        ctx,
        args,
        vtexSegment
      )
    } else {
      products = await fetchProductsByIdentifierFromSearch(
        ctx,
        args,
        vtexSegment
      )
    }

    return products
  } catch (error) {
    ctx.vtex.logger.error({
      message: 'Error fetching products by identifier',
      error: error.message,
      args,
    })
    throw error
  }
}

```


[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/services/settings.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
type AppSettings = {
  shouldUseNewPDPEndpoint: boolean
  shouldUseNewPLPEndpoint: boolean
}

export async function fetchAppSettings(ctx: Context): Promise<AppSettings> {
  const {
    clients: { apps },
  } = ctx

  try {
    const { shouldUseNewPDPEndpoint, shouldUseNewPLPEndpoint }: AppSettings =
      await apps.getAppSettings('vtex.search-resolver@1.x')

    return {
      shouldUseNewPDPEndpoint,
      shouldUseNewPLPEndpoint,
    }
  } catch (error) {
    ctx.vtex.logger.error({
      message: 'Error fetching app settings',
      error: error.message,
    })

    return {
      shouldUseNewPDPEndpoint: false,
      shouldUseNewPLPEndpoint: false,
    }
  }
}

```


[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/services/autocomplete.test.ts from the search-resolver VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  fetchAutocompleteSuggestions,
  fetchTopSearches,
  fetchSearchSuggestions,
  fetchCorrection,
} from './autocomplete'
import { createContext } from '../mocks/contextFactory'

describe('fetchAutocompleteSuggestions', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('should call intsch and return its result when successful', async () => {
    const intschResult = {
      searches: [{ term: 'test', count: 1 }],
    }

    const ctx = createContext({
      intschSettings: {
        fetchAutocompleteSuggestionsV1: intschResult,
      },
    })

    const result = await fetchAutocompleteSuggestions(ctx, 'test')

    expect(
      ctx.clients.intsch.fetchAutocompleteSuggestionsV1
    ).toHaveBeenCalledWith({
      query: 'test',
      locale: undefined,
    })

    expect(result).toEqual(intschResult)
  })

  it('should throw error when intsch fails', async () => {
    const ctx = createContext({
      intschSettings: {
        fetchAutocompleteSuggestionsV1: new Error('Service unavailable'),
      },
    })

    await expect(fetchAutocompleteSuggestions(ctx, 'test')).rejects.toThrow(
      'Service unavailable'
    )

    expect(
      ctx.clients.intsch.fetchAutocompleteSuggestionsV1
    ).toHaveBeenCalledWith({
      query: 'test',
      locale: undefined,
    })
  })

  it('should use locale from segment cultureInfo when available', async () => {
    const intschResult = {
      searches: [{ term: 'test-result', count: 1 }],
    }

    const ctx = createContext({
      intschSettings: {
        fetchAutocompleteSuggestionsV1: intschResult,
      },
      segment: {
        campaigns: null,
        channel: '1',
        priceTables: null,
        utm_campaign: null,
        utm_source: null,
        utmi_campaign: null,
        currencyCode: 'BRL',
        currencySymbol: 'R$',
        countryCode: 'BRA',
        cultureInfo: 'pt-BR',
      },
      tenantLocale: 'en-US',
      vtexLocale: 'es-ES',
    })

    await fetchAutocompleteSuggestions(ctx, 'test')

    expect(
      ctx.clients.intsch.fetchAutocompleteSuggestionsV1
    ).toHaveBeenCalledWith({
      query: 'test',
      locale: 'pt-BR',
    })
  })

  it('should fallback to tenant locale when segment cultureInfo is not available', async () => {
    const fallbackResult = {
      searches: [{ term: 'test-result', count: 1 }],
    }

    const ctx = createContext({
      intschSettings: {
        fetchAutocompleteSuggestionsV1: fallbackResult,
      },
      tenantLocale: 'en-US',
      vtexLocale: 'es-ES',
    })

    await fetchAutocompleteSuggestions(ctx, 'test')

    expect(
      ctx.clients.intsch.fetchAutocompleteSuggestionsV1
    ).toHaveBeenCalledWith({
      query: 'test',
      locale: 'en-US',
    })
  })

  it('should fallback to vtex locale when both segment cultureInfo and tenant locale are not available', async () => {
    const intschResult = {
      searches: [{ term: 'test-result', count: 1 }],
    }

    const ctx = createContext({
      intschSettings: {
        fetchAutocompleteSuggestionsV1: intschResult,
      },
      vtexLocale: 'es-ES',
    })

    await fetchAutocompleteSuggestions(ctx, 'test')

    expect(
      ctx.clients.intsch.fetchAutocompleteSuggestionsV1
    ).toHaveBeenCalledWith({
      query: 'test',
      locale: 'es-ES',
    })
  })
})

describe('fetchTopSearches', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('should call intsch and return its result when successful', async () => {
    const intschResult = {
      searches: [{ term: 'popular', count: 10 }],
    }

    const ctx = createContext({
      intschSettings: {
        fetchTopSearchesV1: intschResult,
      },
    })

    const result = await fetchTopSearches(ctx)

    expect(ctx.clients.intsch.fetchTopSearchesV1).toHaveBeenCalledWith(
      undefined
    )

    expect(result).toEqual(intschResult)
  })

  it('should throw error when intsch fails', async () => {
    const ctx = createContext({
      intschSettings: {
        fetchTopSearchesV1: new Error('Service unavailable'),
      },
    })

    await expect(fetchTopSearches(ctx)).rejects.toThrow('Service unavailable')

    expect(ctx.clients.intsch.fetchTopSearchesV1).toHaveBeenCalledWith(
      undefined
    )
  })
})

describe('fetchSearchSuggestions', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('should call intsch and return its result when successful', async () => {
    const intschResult = {
      searches: [{ term: 'suggestion', count: 5 }],
    }

    const ctx = createContext({
      intschSettings: {
        fetchSearchSuggestionsV1: intschResult,
      },
    })

    const result = await fetchSearchSuggestions(ctx, 'search')

    expect(ctx.clients.intsch.fetchSearchSuggestionsV1).toHaveBeenCalledWith({
      query: 'search',
      locale: undefined,
    })

    expect(result).toEqual(intschResult)
  })

  it('should throw error when intsch fails', async () => {
    const ctx = createContext({
      intschSettings: {
        fetchSearchSuggestionsV1: new Error('Service unavailable'),
      },
    })

    await expect(fetchSearchSuggestions(ctx, 'search')).rejects.toThrow(
      'Service unavailable'
    )

    expect(ctx.clients.intsch.fetchSearchSuggestionsV1).toHaveBeenCalledWith({
      query: 'search',
      locale: undefined,
    })
  })
})

describe('fetchCorrection', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  it('should call intsch and return its result when successful', async () => {
    const intschResult = {
      correction: {
        text: 'text',
        highlighted: '<b>text</b>',
        misspelled: true,
        correction: true,
      },
    }

    const ctx = createContext({
      intschSettings: {
        fetchCorrectionV1: intschResult,
      },
    })

    const result = await fetchCorrection(ctx, 'test')

    expect(ctx.clients.intsch.fetchCorrectionV1).toHaveBeenCalledWith({
      query: 'test',
      locale: undefined,
    })

    expect(result).toEqual(intschResult)
  })

  it('should throw error when intsch fails', async () => {
    const ctx = createContext({
      intschSettings: {
        fetchCorrectionV1: new Error('Service unavailable'),
      },
    })

    await expect(fetchCorrection(ctx, 'test')).rejects.toThrow(
      'Service unavailable'
    )

    expect(ctx.clients.intsch.fetchCorrectionV1).toHaveBeenCalledWith({
      query: 'test',
      locale: undefined,
    })
  })
})

```