[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 store-graphql/node/resolvers/catalog/itemMetadataPriceTableItem.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { isEmpty, path } from 'ramda'

import { getItemChoiceType, CHOICE_TYPES } from '../checkout/attachmentsHelper'

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

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

// Generate a valid simulation list with this child, considering its a TOGGLE item type
const getSimulationPayloadItemsForToggleFromTree = (
  parent: CatalogMetadataItem,
  childId: string,
  seller: string,
  assemblyOption: AssemblyOption,
  parentBasicTree: PayloadItem[]
) => {
  const parentPayloadItem = {
    id: parent.id,
    quantity: 1,
    seller: parent.seller,
  }

  const basicChildItem = {
    id: childId,
    seller,
    quantity: 1,
    parentItemIndex: 0,
    parentAssemblyBinding: assemblyOption.id,
  }

  const siblings = parentBasicTree.filter(
    ({ parentAssemblyBinding }) => parentAssemblyBinding === assemblyOption.id
  )

  const siblingCount = siblings.length

  // See if we can add this item, considering the group max quantity
  if (siblingCount < assemblyOption.composition!.maxQuantity) {
    return [parentPayloadItem, basicChildItem]
  }

  // If there are siblings on the basic tree, remove it and add this child
  const brotherToBeRemoved = siblings.find((item) => {
    const brotherComposition = assemblyOption.composition!.items.find(
      (comp) => comp.id === item.id
    )

    return brotherComposition!.minQuantity === 0
  })

  if (!brotherToBeRemoved) {
    return []
  }

  const withoutBrother = parentBasicTree.filter(
    ({ id, parentAssemblyBinding }) =>
      id !== brotherToBeRemoved.id &&
      parentAssemblyBinding !== brotherToBeRemoved.parentAssemblyBinding
  )

  return [parentPayloadItem, ...withoutBrother, basicChildItem]
}

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

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

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

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

  const siblingCount = siblings.reduce((sum, sib) => sum + sib.quantity, 0)
  const siblingsAllowed = assemblyOption.composition!.maxQuantity - siblingCount

  if (basicItemQuantity <= siblingsAllowed) {
    return [parentPayloadItem, ...siblings, basicChildItem]
  }

  let currentSiblingCount = siblingCount

  // We will iterate through the childs siblings and remove the minimum we should
  for (const brother of siblings) {
    const brotherComposition = assemblyOption.composition!.items.find(
      (comp) => comp.id === brother.id
    )

    const oldBrotherQuantity = brother.quantity

    brother.quantity = brotherComposition!.minQuantity
    currentSiblingCount -= oldBrotherQuantity - brotherComposition!.minQuantity
    const currentSiblingsAllowed =
      assemblyOption.composition!.maxQuantity - currentSiblingCount

    if (currentSiblingsAllowed >= basicItemQuantity) {
      // We can now break because we can add out chuld item safely
      break
    }
  }

  const nonEmptySiblings = siblings.filter(({ quantity }) => quantity > 0)

  return [parentPayloadItem, ...nonEmptySiblings, basicChildItem]
}

const simulateAndGetPrice = async (
  payload: SimulationPayload,
  checkout: Context['clients']['checkout'],
  itemId: string,
  assemblyId: string
) => {
  const simulation = await checkout.simulation(payload)
  const childInTree = simulation.items.find(
    (item) => itemId === item.id && assemblyId === item.parentAssemblyBinding
  )

  return childInTree ? childInTree.sellingPrice : 0
}

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

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

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

  return getSimulationPayloadItemsForMultipleFromTree(
    parent,
    assemblyOption,
    fatherBasicTree,
    compositionItem
  )
}

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

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

      for (const fatherAssemblyOption of parent.assemblyOptions) {
        const assemblyId = fatherAssemblyOption.id

        if (!fatherAssemblyOption.composition) {
          continue
        }

        const groupMinimum = fatherAssemblyOption.composition!.minQuantity
        const itemsInitialsSum = fatherAssemblyOption.composition!.items.reduce(
          (sum, item) => sum + item.initialQuantity,
          0
        )

        // The basic tree will not contain children of this group
        if (groupMinimum !== itemsInitialsSum) {
          continue
        }

        for (const compItem of fatherAssemblyOption.composition!.items) {
          if (compItem.initialQuantity > 0) {
            parentBasicTree.push({
              id: compItem.id,
              quantity: compItem.initialQuantity,
              seller: compItem.seller,
              parentItemIndex: 0,
              parentAssemblyBinding: assemblyId,
            })
          }
        }
      }

      const itemInTree = parentBasicTree.find(
        (item) =>
          id === item.id && assemblyOption.id === item.parentAssemblyBinding
      )

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

      // If not in tree, include it, then simulate and get price
      const newSimulationPayloadItems = getSimulationPayloadItems(
        assemblyOption,
        compositionItem,
        parent,
        parentBasicTree
      )

      return simulateAndGetPrice(
        { ...payload, items: newSimulationPayloadItems },
        checkout,
        id,
        assemblyOption.id
      )
    },
  },
}

```


[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 store-graphql/node/resolvers/catalog/index.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Functions } from '@gocommerce/utils'
import { NotFoundError, ResolverWarning, UserInputError } from '@vtex/api'
import {
  compose,
  equals,
  filter,
  head,
  isEmpty,
  isNil,
  join,
  map,
  path,
  prop,
  split,
  test,
  toLower,
  zip,
} from 'ramda'

import { resolvers as autocompleteResolvers } from './autocomplete'
import { resolvers as brandResolvers } from './brand'
import { resolvers as categoryResolvers } from './category'
import { resolvers as discountResolvers } from './discount'
import { resolvers as facetsResolvers } from './facets'
import { resolvers as itemMetadataResolvers } from './itemMetadata'
import { resolvers as itemMetadataUnitResolvers } from './itemMetadataUnit'
import { resolvers as itemMetadataPriceTableItemResolvers } from './itemMetadataPriceTableItem'
import { resolvers as offerResolvers } from './offer'
import { resolvers as productResolvers } from './product'
import { resolvers as productSearchResolvers } from './productSearch'
import { resolvers as recommendationResolvers } from './recommendation'
import { resolvers as searchResolvers } from './search'
import { resolvers as breadcrumbResolvers } from './searchBreadcrumb'
import { resolvers as skuResolvers } from './sku'
import {
  CatalogCrossSellingTypes,
  findCategoryInTree,
  getBrandFromSlug,
  searchContextGetCategory,
  translatePageType,
} from './utils'
import { catalogSlugify } from './slug'

interface SearchContext {
  brand: string | null
  category: string | number | null
  contextKey: string
}

interface SearchContextParams {
  brand?: string
  department?: string
  category?: string
  subcategory?: string
}

interface SearchMetadataArgs {
  query?: string | null
  map?: string | null
}

interface ProductIndentifier {
  field: 'id' | 'slug' | 'ean' | 'reference' | 'sku'
  value: string
}

interface ProductArgs {
  slug?: string
  identifier?: ProductIndentifier
}

interface PageTypeArgs {
  path: string
  query: string
}

// eslint-disable-next-line no-restricted-syntax
enum CrossSellingInput {
  view = 'view',
  buy = 'buy',
  similars = 'similars',
  viewAndBought = 'viewAndBought',
  suggestions = 'suggestions',
  accessories = 'accessories',
}

interface ProductRecommendationArg {
  identifier?: ProductIndentifier
  type?: CrossSellingInput
}

interface ProductsByIdentifierArgs {
  field: 'id' | 'ean' | 'reference' | 'sku'
  values: [string]
}

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

const brandFromList = async (
  slug: string,
  catalog: Context['clients']['catalog']
) => {
  const dataBrandFromList = await getBrandFromSlug(toLower(slug), catalog)

  return dataBrandFromList?.id ?? null
}

const getBrandId = async (
  brand: string | undefined,
  catalog: Context['clients']['catalog'],
  isVtex: boolean,
  logger: Context['clients']['logger']
) => {
  if (!brand) {
    return null
  }

  if (!isVtex) {
    return brandFromList(brand, catalog)
  }

  const slugified = catalogSlugify(brand)
  const brandPagetype = await catalog.pageType(slugified).catch(() => null)

  if (!brandPagetype) {
    logger.info(`brand ${brand}, slug ${slugified}`, 'pagetype-brand-error')
  }

  if (!brandPagetype || brandPagetype.pageType !== 'Brand') {
    return brandFromList(brand, catalog)
  }

  return brandPagetype.id
}

type TupleString = [string, string]

const isTupleMap = compose<TupleString, string, boolean>(equals('c'), prop('1'))

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

const getAndParsePagetype = async (pathQuery: string, ctx: Context) => {
  const pagetype = await ctx.clients.catalog
    .pageType(pathQuery)
    .catch(() => null)

  if (!pagetype) {
    return { titleTag: null, metaTagDescription: null }
  }

  return {
    titleTag: pagetype.title ?? pagetype.name,
    metaTagDescription: pagetype.metaTagDescription,
  }
}

const getCategoryMetadata = async (
  options: SearchMetadataArgs,
  ctx: Context
) => {
  const {
    vtex: { account },
  } = ctx

  const queryAndMap: TupleString[] = zip(
    (options.query ?? '').split('/'),
    (options.map ?? '').split(',')
  )

  const cleanQuery = categoriesOnlyQuery(queryAndMap)

  if (Functions.isGoCommerceAcc(account)) {
    // GoCommerce does not have pagetype query implemented yet
    const category =
      findCategoryInTree(
        await queries.categories(
          {},
          { treeLevel: cleanQuery.split('/').length },
          ctx
        ),
        cleanQuery.split('/')
      ) ?? {}

    return {
      metaTagDescription: path(['MetaTagDescription'], category),
      titleTag: path(['Title'], category) || path(['Name'], category),
    }
  }

  return getAndParsePagetype(cleanQuery, ctx)
}

const getBrandMetadata = async (
  { query }: SearchMetadataArgs,
  ctx: Context
) => {
  const {
    vtex: { account },
    clients: { catalog },
  } = ctx

  const cleanQuery = head(split('/', query ?? '')) ?? ''

  if (Functions.isGoCommerceAcc(account)) {
    const brand = (await getBrandFromSlug(toLower(cleanQuery), catalog)) ?? {}

    return {
      metaTagDescription: path(['metaTagDescription'], brand),
      titleTag: path(['title'], brand) || path(['name'], brand),
    }
  }

  return getAndParsePagetype(cleanQuery, ctx)
}

/**
 * Get metadata of category/brand APIs
 *
 * @param _
 * @param args
 * @param ctx
 */
const getSearchMetaData = async (
  _: any,
  args: SearchMetadataArgs,
  ctx: Context
) => {
  const argsMap = args.map ?? ''
  const firstMap = head(argsMap.split(','))

  if (firstMap === 'c') {
    return getCategoryMetadata(args, ctx)
  }

  if (firstMap === 'b') {
    return getBrandMetadata(args, ctx)
  }

  return { titleTag: null, metaTagDescription: null }
}

const translateToStoreDefaultLanguage = async (
  clients: Context['clients'],
  term: string
): Promise<any> => {
  const { segment, messagesGraphQL } = clients
  const [{ cultureInfo: to }, { cultureInfo: from }] = await Promise.all([
    segment.getSegmentByToken(null),
    segment.getSegment(),
  ])

  return from && from !== to
    ? messagesGraphQL
        .translateV2({
          indexedByFrom: [
            {
              from,
              messages: [{ content: term }],
            },
          ],
          to,
        })
        .then(head)
    : term
}

export const fieldResolvers = {
  ...autocompleteResolvers,
  ...brandResolvers,
  ...categoryResolvers,
  ...facetsResolvers,
  ...itemMetadataResolvers,
  ...itemMetadataUnitResolvers,
  ...itemMetadataPriceTableItemResolvers,
  ...offerResolvers,
  ...discountResolvers,
  ...productResolvers,
  ...recommendationResolvers,
  ...searchResolvers,
  ...skuResolvers,
  ...breadcrumbResolvers,
  ...productSearchResolvers,
}

const isValidProductIdentifier = (identifier: ProductIndentifier | undefined) =>
  !!identifier && !isNil(identifier.value) && !isEmpty(identifier.value)

export const queries = {
  autocomplete: async (_: any, args: any, ctx: Context) => {
    const {
      clients: { catalog },
      clients,
    } = ctx

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

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

    return {
      cacheId: args.searchTerm,
      itemsReturned,
    }
  },

  facets: async (_: any, args: FacetsArgs, ctx: Context) => {
    const {
      clients: { catalog },
      clients,
    } = ctx

    if (args.facets?.includes('undefined')) {
      throw new UserInputError('Bad facets parameter provided')
    }

    let result
    const translatedQuery = await translateToStoreDefaultLanguage(
      clients,
      args.query
    )

    const segmentData = ctx.vtex.segment
    const salesChannel = segmentData?.channel.toString() ?? ''

    const unavailableString = args.hideUnavailableItems
      ? `&fq=isAvailablePerSalesChannel_${salesChannel}:1`
      : ''

    if (args.facets) {
      result = await catalog.facets(args.facets)
    } else {
      result = await catalog.facets(
        `${translatedQuery}?map=${args.map}${unavailableString}`
      )
    }

    result.queryArgs = {
      query: translatedQuery,
      map: args.map,
    }

    return result
  },

  product: async (_: any, rawArgs: ProductArgs, ctx: Context) => {
    const {
      vtex: { account },
      clients: { catalog },
    } = ctx

    const args =
      rawArgs &&
      isValidProductIdentifier(rawArgs.identifier) &&
      !Functions.isGoCommerceAcc(account)
        ? rawArgs
        : { identifier: { field: 'slug', value: rawArgs.slug! } }

    if (!args.identifier) {
      throw new UserInputError('No product identifier provided')
    }

    const { field, value } = args.identifier
    let products = [] as Product[]

    switch (field) {
      case 'id':
        products = await catalog.productById(value)
        break

      case 'slug':
        products = await catalog.product(value)
        break

      case 'ean':
        products = await catalog.productByEan(value)
        break

      case 'reference':
        products = await catalog.productByReference(value)
        break

      case 'sku':
        products = await catalog.productBySku([value])
        break

      default:
        break
    }

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

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

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

    const queryTerm = args.query

    if (queryTerm == null || test(/[?&[\]=]/, queryTerm)) {
      throw new UserInputError(
        `The query term contains invalid characters. query=${queryTerm}`
      )
    }

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

    return catalog.products(args)
  },

  productsByIdentifier: async (
    _: any,
    args: ProductsByIdentifierArgs,
    ctx: Context
  ) => {
    const {
      clients: { catalog },
    } = ctx

    let products = [] as Product[]
    const { field, values } = args

    switch (field) {
      case 'id':
        products = await catalog.productsById(values)
        break

      case 'ean':
        products = await catalog.productsByEan(values)
        break

      case 'reference':
        products = await catalog.productsByReference(values)
        break

      case 'sku':
        products = await catalog.productBySku(values)
        break

      default:
        break
    }

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

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

  productSearch: async (_: any, args: SearchArgs, ctx: Context) => {
    const {
      clients,
      clients: { catalog },
    } = ctx

    const queryTerm = args.query

    if (queryTerm == null || test(/[?&[\]=]/, queryTerm)) {
      throw new UserInputError(
        `The query term contains invalid characters. query=${queryTerm}`
      )
    }

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

    const query = await translateToStoreDefaultLanguage(
      clients,
      args.query ?? ''
    )

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

    const [productsRaw, searchMetaData] = await Promise.all([
      catalog.products(args, true),
      getSearchMetaData(_, translatedArgs, ctx),
    ])

    return {
      translatedArgs,
      searchMetaData,
      productsRaw,
    }
  },

  brand: async (
    _: any,
    { id }: { id?: number },
    { clients: { catalog } }: Context
  ) => {
    if (id == null) {
      throw new ResolverWarning(`No brand ID provided`)
    }

    const brand = await catalog.brand(id)

    if (!brand) {
      throw new NotFoundError(`Brand not found`)
    }

    return brand
  },

  brands: async (_: any, __: any, { clients: { catalog } }: Context) =>
    catalog.brands(),

  category: async (
    _: any,
    { id }: { id?: number },
    { clients: { catalog } }: Context
  ) => {
    if (id == null) {
      throw new ResolverWarning(`No category ID provided`)
    }

    return catalog.category(id)
  },

  categories: async (
    _: any,
    { treeLevel }: { treeLevel: number },
    { clients: { catalog } }: Context
  ) => catalog.categories(treeLevel),

  /** TODO: This method should be removed in the next major.
   * @author Bruno Dias
   */
  search: async (_: any, args: any, ctx: Context) => {
    if (args.query == null || args.map == null) {
      throw new UserInputError('Search query/map cannot be null')
    }

    const { titleTag, metaTagDescription }: any = await getSearchMetaData(
      _,
      args,
      ctx
    )

    return {
      metaTagDescription,
      queryArgs: args,
      titleTag,
    }
  },

  searchContextFromParams: async (
    _: any,
    args: SearchContextParams,
    { clients: { catalog, logger }, vtex: { account } }: Context
  ) => {
    const isVtex = !Functions.isGoCommerceAcc(account)
    const response: SearchContext = {
      brand: null,
      category: null,
      contextKey: 'search',
    }

    const [brandId, categoryId] = await Promise.all<
      string | null,
      string | number | null
    >([
      getBrandId(args.brand, catalog, isVtex, logger),
      searchContextGetCategory(args, catalog, isVtex, logger),
    ])

    response.brand = brandId
    response.category = categoryId

    return response
  },

  pageType: async (_: any, args: PageTypeArgs, ctx: Context) => {
    const response = await ctx.clients.catalog.pageType(args.path, args.query)

    return {
      id: response.id,
      type: translatePageType(response.pageType),
    }
  },

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

    const catalogType = inputToCatalogCrossSelling[type]
    let productId = identifier.value

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

      productId = product!.productId
    }

    return ctx.clients.catalog.crossSelling(productId, catalogType)
  },

  searchMetadata: async (_: any, args: SearchMetadataArgs, ctx: Context) => {
    const { clients } = ctx
    const queryTerm = args.query

    if (queryTerm == null || test(/[?&[\]=]/, queryTerm)) {
      throw new UserInputError(
        `The query term contains invalid characters. query=${queryTerm}`
      )
    }

    const query = await translateToStoreDefaultLanguage(
      clients,
      args.query ?? ''
    )

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

    return getSearchMetaData(_, translatedArgs, ctx)
  },
}

```


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

[User] Show me the implementation of store-graphql/node/resolvers/catalog/offer.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { gte, lte, propOr } from 'ramda'

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

export const resolvers = {
  Offer: {
    Installments: (
      { Installments }: any,
      { criteria, rates }: { criteria?: string; rates?: boolean }
    ) => {
      if (criteria === InstallmentsCriteria.ALL || Installments.length === 0) {
        return Installments
      }

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

      const compareFunc = criteria === InstallmentsCriteria.MAX ? gte : lte
      const value = filteredInstallments.reduce(
        (acc: any, currentValue: any) =>
          compareFunc(
            currentValue.NumberOfInstallments,
            acc.NumberOfInstallments
          )
            ? currentValue
            : acc,
        filteredInstallments[0]
      )

      return [value]
    },
    teasers: propOr([], 'Teasers'),
    giftSkuIds: propOr([], 'GiftSkuIds'),
    discountHighlights: propOr([], 'DiscountHighLight'),
    spotPrice: (offer: Seller['commertialOffer']) => {
      const sellingPrice = offer.Price
      const spotPrice: number | undefined = offer.Installments.find(
        ({ NumberOfInstallments, Value }) => {
          return NumberOfInstallments === 1 && Value < sellingPrice
        }
      )?.Value

      return spotPrice ?? sellingPrice
    },
  },
}

```


[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 store-graphql/node/resolvers/catalog/productSearch.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { path, zip } from 'ramda'
import { IOResponse } from '@vtex/api'
import { Functions } from '@gocommerce/utils'

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

export const resolvers = {
  ProductSearch: {
    titleTag: path(['searchMetaData', 'titleTag']),
    metaTagDescription: path(['searchMetaData', 'metaTagDescription']),
    recordsFiltered: ({ productsRaw }: ProductSearchParent) => {
      const {
        headers: { resources },
      } = productsRaw

      const [, quantity] = resources.split('/')

      return parseInt(quantity, 10)
    },
    products: path(['productsRaw', 'data']),
    breadcrumb: async (
      { translatedArgs, productsRaw: { data: products } }: ProductSearchParent,
      _: any,
      { vtex: { account }, clients: { catalog } }: Context
    ) => {
      const query = translatedArgs.query ?? ''
      const map = translatedArgs.map ?? ''
      const queryAndMap = zip(
        query.toLowerCase().split('/').map(decodeURIComponent),
        map.split(',')
      )

      const categoriesSearched = queryAndMap
        .filter(([__, m]) => m === 'c')
        .map(([q]) => q)

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

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

```


[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 store-graphql/node/resolvers/catalog/sku.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

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

export const resolvers = {
  SKU: {
    attachments: ({ attachments = [] }: any) =>
      map(
        (attachment: any) => ({
          ...attachment,
          domainValues: JSON.parse(attachment.domainValues),
        }),
        attachments
      ),

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

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

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

    variations: (sku: any) =>
      sku &&
      map(
        (name: string) => ({ name, values: sku[name] }),
        sku.variations || []
      ),

    videos: ({ Videos }: any) =>
      map(
        (video: string) => ({
          videoUrl: video,
        }),
        Videos
      ),

    skuSpecifications: (sku: any) => {
      const { variations } = sku
      const skuSpecifications: SkuSpecification[] = []

      ;(variations || []).forEach((variation: string) => {
        const fieldValues: string[] = []

        ;(sku[variation] || []).forEach((value: string) => {
          fieldValues.push(value)
        })

        skuSpecifications.push({
          fieldName: variation,
          fieldValues,
        })
      })

      return skuSpecifications
    },
  },
}

```


[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 store-graphql/node/resolvers/document/attachment.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { ResolverError } from '@vtex/api'
import FormData from 'form-data'

import { generateRandomName } from '../../utils'

export async function uploadAttachment(args: any, ctx: Context) {
  const {
    clients: { masterdata },
  } = ctx

  const { acronym, documentId, field, file } = args
  const { createReadStream, filename, mimetype } = await file
  const buffer = (await new Promise((resolve, reject) => {
    const bufs: any[] = []
    const stream = createReadStream()

    stream.on('data', (d: any) => bufs.push(d))
    stream.on('end', () => {
      resolve(Buffer.concat(bufs))
    })
    stream.on('error', reject)
  })) as Buffer

  const formData = new FormData()

  const randomName = `${generateRandomName()}${getFileExtension(filename)}`

  formData.append(field, buffer, {
    contentType: mimetype,
    filename: randomName,
    knownLength: buffer.byteLength,
  })

  const response = await masterdata.uploadAttachment(
    acronym,
    documentId,
    field,
    formData
  )

  if (response) {
    throw new ResolverError(response)
  }

  return { filename: randomName, mimetype }
}

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

```


[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 store-graphql/node/resolvers/document/index.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { UserInputError } from '@vtex/api'
import { compose, map, union, prop, replace } from 'ramda'

import { parseFieldsToJson } from '../../utils/object'
import { resolvers as documentSchemaResolvers } from './documentSchema'

export const queries = {
  documents: async (_: any, args: DocumentsArgs, context: Context) => {
    const {
      acronym,
      fields,
      page,
      pageSize,
      where,
      schema,
      sort,
      account,
    } = args

    const {
      clients: { masterdata },
    } = context

    const fieldsWithId = union(fields, ['id'])
    const data = (await masterdata.searchDocuments(
      acronym,
      fieldsWithId,
      where,
      {
        page,
        pageSize,
      },
      schema,
      sort,
      account
    )) as any

    return map((document: any) => ({
      cacheId: document.id,
      id: document.id,
      fields: mapKeyAndStringifiedValues(document),
    }))(data)
  },

  document: async (_: any, args: DocumentArgs, context: Context) => {
    const { acronym, fields, id, account } = args
    const {
      clients: { masterdata },
    } = context

    const data = await masterdata.getDocument(acronym, id, fields, account)

    return {
      cacheId: id,
      id,
      fields: mapKeyAndStringifiedValues(data),
    }
  },

  documentSchema: async (
    _: any,
    args: DocumentSchemaArgs,
    context: Context
  ) => {
    const { dataEntity, schema } = args

    const {
      clients: { masterdata },
    } = context

    const data = await masterdata.getSchema<Record<string, unknown>>(
      dataEntity,
      schema
    )

    return { ...data, name: data ? args.schema : null }
  },

  documentPublicSchema: async (
    _: any,
    args: DocumentSchemaArgs,
    context: Context
  ) => {
    const { dataEntity, schema } = args

    const {
      clients: { masterdata },
    } = context

    const data = await masterdata.getPublicSchema<Record<string, unknown>>(
      dataEntity,
      schema
    )

    return { schema: data }
  },
}

export const fieldResolvers = {
  ...documentSchemaResolvers,
}

export const mutations = {
  createDocument: async (
    _: any,
    args: CreateDocumentArgs,
    context: Context
  ) => {
    const {
      acronym,
      document: { fields },
      account,
      schema,
    } = args

    const {
      clients: { masterdata },
    } = context

    const response = (await masterdata.createDocument(
      acronym,
      parseFieldsToJson(fields),
      schema,
      account
    )) as DocumentResponse

    const documentId = removeAcronymFromId(acronym, response)

    return {
      cacheId: documentId,
      id: prop('Id', response),
      href: prop('Href', response),
      documentId: removeAcronymFromId(acronym, response),
    }
  },

  createDocumentV2: async (
    _: any,
    args: CreateDocumentV2Args,
    context: Context
  ) => {
    const {
      dataEntity,
      document: { document },
      account,
      schema,
    } = args

    const {
      clients: { masterdata },
    } = context

    const response = (await masterdata.createDocument(
      dataEntity,
      document,
      schema,
      account
    )) as DocumentResponseV2

    const documentId = removeAcronymFromId(dataEntity, response)

    return {
      cacheId: documentId,
      id: prop('Id', response),
      href: prop('Href', response),
      documentId,
    }
  },

  updateDocument: async (
    _: any,
    args: UpdateDocumentArgs,
    context: Context
  ) => {
    const {
      acronym,
      document: { fields },
      account: accountName,
    } = args

    const documentId = prop('id', parseFieldsToJson(fields)) as string

    if (!documentId) {
      throw new UserInputError('document id field cannot be null/undefined')
    }

    const {
      clients: { masterdata },
      vtex: { account },
    } = context

    await masterdata.updateDocument(
      acronym,
      documentId,
      parseFieldsToJson(fields),
      accountName
    )

    return {
      cacheId: documentId,
      documentId,
      href: generateHref(account, acronym, documentId),
      id: getId(acronym, documentId),
    }
  },

  deleteDocument: async (
    _: any,
    args: DeleteDocumentArgs,
    context: Context
  ) => {
    const { acronym, documentId } = args
    const {
      clients: { masterdata },
      vtex: { account },
    } = context

    await masterdata.deleteDocument(acronym, documentId)

    return {
      documentId,
      href: generateHref(account, acronym, documentId),
      id: getId(acronym, documentId),
      cacheId: documentId,
    }
  },
}

/**
 * Map a document object to a list of {key: 'property', value: 'propertyValue'},
 * Uses `JSON.stringify` in every value.
 */
const mapKeyAndStringifiedValues = (document: any) =>
  Object.keys(document).map((key) => ({
    key,
    value:
      typeof document[key] === 'string'
        ? document[key]
        : JSON.stringify(document[key]),
  }))

const removeAcronymFromId = (acronym: string, data: { Id: string }) => {
  return compose<any, any, any>(
    replace(new RegExp(`${acronym}-`), ''),
    prop('Id')
  )(data)
}

const getId = (acronym: string, documentId: string) =>
  `${acronym}-${documentId}`

const generateHref = (account: string, acronym: string, documentId: string) =>
  `http://api.vtex.com/${account}/dataentities/${acronym}/documents/${documentId}`

```


[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 store-graphql/node/resolvers/document/documentSchema.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { prop, toPairs, map } from 'ramda'

export const resolvers = {
  DocumentSchema: {
    properties: ({ properties }: any) =>
      map(
        ([name, rest]: any) => ({
          name,
          type: rest.type,
          optional: rest.optional || false,
        }),
        toPairs(properties)
      ),
    indexed: prop('v-indexed'),
    cache: prop('v-cache'),
    security: prop('v-security'),
  },
}

```


[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 store-graphql/node/resolvers/checkout/paymentTokenResolver.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import http from 'axios'
import { last, mergeRight, propEq, reject } from 'ramda'
import pMapSeries from 'p-map-series'

import paths from '../paths'

const createClient = (account: any, orderFormId: any, authToken: any) => {
  const headers = {
    Accept: 'application/json',
    Authorization: `bearer ${authToken}`,
    'Content-Type': 'application/json',
  }

  return {
    addToken: (paymentToken: any) => {
      const payload = {
        paymentToken,
        expectedOrderFormSections: ['items', 'paymentData'],
      }

      const url = paths.orderFormPaymentToken(account, { orderFormId })

      return http.put(url, payload, { headers })
    },

    removeToken: (tokenId: any) => {
      const url = paths.orderFormPaymentTokenId(account, {
        orderFormId,
        tokenId,
      })

      return http.delete(url, {
        headers,
        data: { expectedOrderFormSections: ['items'] },
      })
    },
  }
}

export default async (body: any, ioContext: any) => {
  const {
    data: { orderFormId, paymentToken },
  } = body

  const checkout = createClient(
    ioContext.account,
    orderFormId,
    ioContext.authToken
  )

  const response = await checkout.addToken(paymentToken)

  const {
    data: {
      paymentData: { availableTokens },
    },
  } = response

  const tokensToRemove = reject(
    propEq('tokenId', paymentToken.tokenId),
    availableTokens
  )

  if (tokensToRemove.length === 0) {
    return { data: mergeRight(body.data, response.data) }
  }

  const lastDeleteResponse = await pMapSeries(
    tokensToRemove,
    ({ tokenId }: any) => checkout.removeToken(tokenId)
  ).then<any>(last)

  return { data: mergeRight(body.data, lastDeleteResponse.data) }
}

```


[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 store-graphql/node/resolvers/checkout/orderFormItem.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

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

import { getPositionInOrderForm } from './attachmentsHelper'

interface Params extends OrderFormItem {
  assemblyOptionsData: {
    childs: OrderFormItem[]
    index: number
    assemblyOptionsMap: Record<string, AssemblyOption[]>
    orderForm: OrderForm
  }
}

const getPriceForItem = (
  children: OrderFormItem[],
  fatherIndex: number,
  fatherItem: OrderFormItem,
  orderForm: OrderForm
): number => {
  const [itemChildren, others] = partition(
    propEq('parentItemIndex', fatherIndex),
    children
  )

  return itemChildren.reduce(
    (sum, currentChildren) =>
      sum +
      getPriceForItem(
        others,
        getPositionInOrderForm(orderForm, currentChildren),
        currentChildren,
        orderForm
      ),
    fatherItem.sellingPrice * fatherItem.quantity
  )
}

export const resolvers = {
  OrderFormItem: {
    assemblyOptions: ({
      assemblyOptionsData: { childs, index, orderForm, assemblyOptionsMap },
      ...item
    }: Params) => ({ item, childs, index, orderForm, assemblyOptionsMap }),
    cartIndex: ({ assemblyOptionsData: { index } }: Params) => index,
    canHaveAttachment: ({ attachmentOfferings }: OrderFormItem) => {
      return attachmentOfferings && attachmentOfferings.length > 0
    },
    imageUrl: ({ imageUrl }: Params) =>
      imageUrl?.replace('http://', 'https://'),
    listPrice: ({ listPrice }: Params) => listPrice / 100,
    price: ({ price }: Params) => price / 100,
    sellingPrice: ({ sellingPrice }: Params) => sellingPrice / 100,
    sellingPriceWithAssemblies: ({
      assemblyOptionsData: { childs, index, orderForm },
      ...item
    }: Params) => {
      if (childs.length === 0) {
        return item.sellingPrice / 100
      }

      return (
        getPriceForItem(childs, index, item, orderForm) / item.quantity / 100
      )
    },
  },
}

```


[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 store-graphql/node/resolvers/checkout/shipping.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
export const fieldResolvers = {
  ShippingSLA: {
    friendlyName: ({ name, pickupStoreInfo, deliveryChannel }: SLA) => {
      if (deliveryChannel === 'pickup-in-point') {
        return pickupStoreInfo.friendlyName
      }

      return name
    },
  },
}

```


[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 store-graphql/node/resolvers/checkout/assemblyOptionItem.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

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

import {
  buildAddedOptionsForItem,
  buildRemovedOptions,
} from './attachmentsHelper'

interface Params {
  item: OrderFormItem
  childs: OrderFormItem[]
  index: number
  assemblyOptionsMap: Record<string, AssemblyOption[]>
  orderForm: OrderForm
}

export const resolvers = {
  AssemblyOptionItem: {
    added: ({ item, childs, index, assemblyOptionsMap, orderForm }: Params) => {
      if (childs.length === 0 || values(assemblyOptionsMap).length === 0) {
        return []
      }

      return buildAddedOptionsForItem(
        item,
        index,
        childs,
        assemblyOptionsMap,
        orderForm
      )
    },
    parentPrice: ({ item }: Params) => item.sellingPrice * 0.01,
    removed: ({ item, orderForm, assemblyOptionsMap }: Params) => {
      if (values(assemblyOptionsMap).length === 0) {
        return []
      }

      return buildRemovedOptions(item, orderForm, assemblyOptionsMap)
    },
  },
}

```


[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 store-graphql/node/resolvers/checkout/index.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { UserInputError } from '@vtex/api'
import { compose, reject, path } from 'ramda'

import { headers, withAuthToken } from '../headers'
import httpResolver from '../httpResolver'
import { queries as logisticsQueries } from '../logistics/index'
import paths from '../paths'
import { fieldResolvers as slasResolvers } from './checkoutSLA'
import { resolvers as assemblyOptionsItemResolvers } from './assemblyOptionItem'
import {
  addOptionsForItems,
  buildAssemblyOptionsMap,
  isParentItem,
} from './attachmentsHelper'
import { resolvers as orderFormItemResolvers } from './orderFormItem'
import paymentTokenResolver from './paymentTokenResolver'
import { fieldResolvers as shippingFieldResolvers } from './shipping'
import {
  ASPXAUTH_COOKIE,
  CHECKOUT_COOKIE,
  OWNERSHIP_COOKIE,
  parseCookie,
} from '../../utils'
import {
  getSimulationPayloadsByItem,
  orderFormItemToSeller,
} from '../../utils/simulation'
import { LogisticPickupPoint } from '../logistics/types'
import logisticPickupResolvers from '../logistics/fieldResolvers'
import { isSellerFlaggedForChangingOnSimulationBasedOnRegion } from '../../utils/flags/flagRegionalization'

const ALL_SET_COOKIES = [CHECKOUT_COOKIE, ASPXAUTH_COOKIE, OWNERSHIP_COOKIE]

const filterAllowedCookies = (setCookies: string[], allowList: string[]) => {
  return setCookies.filter((setCookie) => {
    const [key] = setCookie.split('=')

    return allowList.includes(key)
  })
}

/**
 * It will convert an integer to float moving the
 * float point two positions left.
 *
 * The OrderForm REST API return an integer
 * colapsing the floating point into the integer
 * part. We needed to make a convention of the product
 * price on different API's. Once the Checkout API
 * returns an integer instead of a float, and the
 * Catalog API returns a float.
 *
 * @param int An integer number
 */
const convertIntToFloat = (int: number) => int * 0.01

interface AddItemArgs {
  orderFormId?: string
  items?: OrderFormItemInput[]
  utmParams?: UTMParams
  utmiParams?: UTMIParams
}

interface SkuPickupSLAListArgs {
  itemId: string
  seller: string
  lat: string
  long: string
  country: string
}

interface SkuPickupSLAArgs extends SkuPickupSLAListArgs {
  pickupId: string
}

interface SLAFromLogistics {
  id: string
  shippingEstimate: null
  pickupStoreInfo: {
    friendlyName: string
    address: CheckoutAddress
  }
}

const checkouSlaFromLogisticPickup = (
  logisticPickup: LogisticPickupPoint
): SLAFromLogistics => {
  return {
    id: logisticPickup.id,
    shippingEstimate: null,
    pickupStoreInfo: {
      friendlyName: logisticPickup.name,
      address: {
        ...logisticPickupResolvers.PickupPoint.address(logisticPickup),
        geoCoordinates: [
          logisticPickup.address.location.longitude,
          logisticPickup.address.location.latitude,
        ],
      },
    },
  }
}

type AllSLAs = SLAItem | SLAFromLogistics

const shouldUpdateMarketingData = (
  orderFormMarketingTags: OrderFormMarketingData | null,
  utmParams?: UTMParams,
  utmiParams?: UTMIParams
) => {
  const {
    utmCampaign = null,
    utmMedium = null,
    utmSource = null,
    utmiCampaign = null,
    utmiPart = null,
    utmipage = null,
  } = orderFormMarketingTags ?? {}

  if (
    !utmParams?.source &&
    !utmParams?.medium &&
    !utmParams?.campaign &&
    !utmiParams?.campaign &&
    !utmiParams?.page &&
    !utmiParams?.part
  ) {
    // Avoid updating at any costs if all fields are invalid
    return false
  }

  return (
    (utmParams?.source ?? null) !== utmSource ||
    (utmParams?.medium ?? null) !== utmMedium ||
    (utmParams?.campaign ?? null) !== utmCampaign ||
    (utmiParams?.part ?? null) !== utmiPart ||
    (utmiParams?.page ?? null) !== utmipage ||
    (utmiParams?.campaign ?? null) !== utmiCampaign
  )
}

type Resolver<TArgs = any, TRoot = any> = (
  root: TRoot,
  args: TArgs,
  context: Context
) => Promise<any>

export const fieldResolvers = {
  OrderForm: {
    cacheId: (orderForm: OrderForm) => {
      return orderForm.orderFormId
    },
    items: (orderForm: OrderForm) => {
      const childs = reject(isParentItem, orderForm.items)
      const assemblyOptionsMap = buildAssemblyOptionsMap(orderForm)

      return orderForm.items.map((item, index) => ({
        ...item,
        assemblyOptionsData: {
          assemblyOptionsMap,
          childs,
          index,
          orderForm,
        },
      }))
    },
    pickupPointCheckedIn: (orderForm: OrderForm, _: any, ctx: Context) => {
      const { isCheckedIn, checkedInPickupPointId } = orderForm

      if (!isCheckedIn || !checkedInPickupPointId) {
        return null
      }

      return logisticsQueries.pickupPoint(
        {},
        { id: checkedInPickupPointId },
        ctx
      )
    },
    value: (orderForm: OrderForm) => {
      return convertIntToFloat(orderForm.value)
    },
  },
  ...assemblyOptionsItemResolvers,
  ...orderFormItemResolvers,
  ...shippingFieldResolvers,
  ...slasResolvers,
}

const replaceDomain = (host: string) => (cookie: string) =>
  cookie.replace(/domain=.+?(;|$)/, `domain=${host};`)

export async function syncWithStoreLocale(
  orderForm: OrderForm,
  cultureInfo: string,
  checkout: Context['clients']['checkout']
) {
  const clientPreferencesData = orderForm.clientPreferencesData || {
    locale: cultureInfo,
  }

  const shouldUpdateClientPreferencesData =
    cultureInfo &&
    clientPreferencesData.locale &&
    cultureInfo !== clientPreferencesData.locale

  if (shouldUpdateClientPreferencesData) {
    const newClientPreferencesData = {
      ...clientPreferencesData,
    }

    newClientPreferencesData.locale = cultureInfo

    try {
      return await checkout.updateOrderFormClientPreferencesData(
        orderForm.orderFormId,
        newClientPreferencesData
      )
    } catch (e) {
      console.error(e)

      return orderForm
    }
  }

  return orderForm
}

export async function setCheckoutCookies(
  rawHeaders: Record<string, any>,
  ctx: Context,
  allowList: string[] = ALL_SET_COOKIES
) {
  const responseSetCookies: string[] = rawHeaders?.['set-cookie'] || []

  const host = ctx.get('x-forwarded-host')
  const forwardedSetCookies = filterAllowedCookies(
    responseSetCookies,
    allowList
  )

  const parseAndClean = compose(parseCookie, replaceDomain(host))

  const cleanCookies = forwardedSetCookies.map(parseAndClean)

  cleanCookies.forEach(({ name, value, options }) => {
    if (options.secure && !ctx.cookies.secure) {
      ctx.cookies.secure = true
    }

    ctx.cookies.set(name, value, options)
  })
}

export const queries: Record<string, Resolver> = {
  orderForm: async (_, __, ctx) => {
    const {
      clients: { checkout },
      vtex: { segment, logger },
    } = ctx

    let { headers: responseHeaders, data } = await checkout.orderFormRaw()

    const hasBrokenCookie = data?.clientProfileData?.email?.startsWith(
      'vrn--vtexsphinx--aws-us-east-1'
    )

    if (hasBrokenCookie) {
      logger.info({
        message: 'Broken order form',
        orderFormId: data.orderFormId,
      })

      await checkout.changeToAnonymousUser(data.orderFormId)

      const obj = await checkout.orderFormRaw()

      data = obj.data
      responseHeaders = obj.headers
    }

    const orderForm = await syncWithStoreLocale(
      data,
      segment!.cultureInfo,
      checkout
    )

    setCheckoutCookies(responseHeaders, ctx)

    return orderForm
  },

  searchOrderForm: async (_, { orderFormId }, ctx) => {
    const {
      clients: { checkout },
    } = ctx

    const orderForm = await checkout.orderForm(orderFormId)

    return orderForm
  },

  orders: (_, __, { clients: { checkout } }) => {
    return checkout.orders()
  },

  shipping: (_, args: any, { clients: { checkout } }) => {
    return checkout.simulation(args)
  },

  skuPickupSLAs: async (
    _: any,
    { itemId, seller, lat, long, country }: SkuPickupSLAListArgs,
    { clients: { checkout, logistics } }: Context
  ) => {
    const simulationPayload = {
      items: [
        {
          id: itemId,
          seller,
          quantity: 1,
        },
      ],
      country,
      shippingData: {
        selectedAddresses: [
          {
            geoCoordinates: [long, lat],
            country,
          },
        ],
      },
    }

    const [simulation, allPickupsOutput] = await Promise.all([
      checkout.simulation(simulationPayload),
      logistics.nearPickupPoints(lat, long),
    ])

    const slas = simulation?.logisticsInfo?.[0]?.slas ?? []
    const slasPickup = slas.filter(
      (sla) => sla.deliveryChannel === 'pickup-in-point'
    ) as AllSLAs[]

    const slaIdsSet = slasPickup.reduce((acc, { pickupStoreInfo }) => {
      if (pickupStoreInfo.address?.addressId) {
        acc.add(pickupStoreInfo.address.addressId)
      }

      return acc
    }, new Set<string>())

    allPickupsOutput.items.forEach((logisticItem) => {
      if (logisticItem.isActive && !slaIdsSet.has(logisticItem.id)) {
        const checkouSla = checkouSlaFromLogisticPickup(logisticItem)

        slasPickup.push(checkouSla)
      }
    })

    return slasPickup
  },

  skuPickupSLA: async (
    _: any,
    { itemId, seller, lat, long, country, pickupId }: SkuPickupSLAArgs,
    ctx: Context
  ) => {
    const slas = (await queries.skuPickupSLAs(
      {},
      { itemId, seller, lat, long, country },
      ctx
    )) as SLA[]

    return slas.find(
      (s) => path(['pickupStoreInfo', 'address', 'addressId'], s) === pickupId
    )
  },

  itemsWithSimulation: async (
    _,
    {
      items,
      regionId,
      salesChannel,
      postalCode,
    }: {
      items: ItemWithSimulationInput[]
      regionId?: string
      salesChannel?: string
      postalCode?: string
    },
    ctx: Context
  ) => {
    const {
      clients: { pvtCheckout },
      vtex: { segment, logger },
    } = ctx

    const changeSeller = isSellerFlaggedForChangingOnSimulationBasedOnRegion(
      ctx.vtex.account
    )

    const countryCode = ctx.vtex.segment?.countryCode

    return items.map((item) => {
      // eslint-disable-next-line no-async-promise-executor
      return new Promise(async (resolve) => {
        const simulationPayloads = getSimulationPayloadsByItem(
          item,
          segment,
          regionId,
          changeSeller,
          postalCode,
          countryCode
        )

        const simulationPromises = simulationPayloads.map((payload) =>
          pvtCheckout.simulation(payload, salesChannel)
        )

        const simulations = await Promise.all(
          simulationPromises.map((p) => p.catch((e: any) => e))
        )

        const sellers = simulations.map((simulation, idx) => {
          if (simulation instanceof Error) {
            logger.error(simulation)

            return {
              error: simulation.message,
              sellerId: item.sellers[idx].sellerId,
              commertialOffer: {
                spotPrice: null,
                AvailableQuantity: 0,
                Price: null,
                ListPrice: null,
                PriceValidUntil: null,
                discountHighlights: [],
                teasers: [],
                Installments: [],
              },
            }
          }

          const [simulationItem] = simulation.items

          return orderFormItemToSeller({
            ...simulationItem,
            paymentData: simulation.paymentData,
            ratesAndBenefitsData: simulation.ratesAndBenefitsData,
            logisticsInfo: simulation.logisticsInfo ?? [],
          })
        })

        resolve({
          itemId: item.itemId,
          sellers,
        })
      })
    })
  },
}

interface UTMParams {
  campaign?: string
  medium?: string
  source?: string
}

interface UTMIParams {
  campaign?: string
  part?: string
  page?: string
}

export const mutations: Record<string, Resolver> = {
  addItem: async (
    _,
    {
      orderFormId: paramsOrderFormId,
      items,
      utmParams,
      utmiParams,
    }: AddItemArgs,
    ctx: Context
  ) => {
    const {
      clients: { checkout },
      vtex,
    } = ctx

    const { orderFormId } = vtex

    if (orderFormId == null) {
      throw new Error('No orderformid in cookies')
    }

    if (items == null) {
      throw new UserInputError('No items to add provided')
    }

    if (orderFormId !== paramsOrderFormId) {
      ctx.vtex.logger.warn(
        `Different orderFormId found: provided=${paramsOrderFormId} and in cookies=${orderFormId}`
      )
    }

    const { marketingData, items: previousItems } = await checkout.orderForm()

    if (shouldUpdateMarketingData(marketingData, utmParams, utmiParams)) {
      const newMarketingData = {
        ...(marketingData ?? {}),
      }

      newMarketingData.utmCampaign = utmParams?.campaign
      newMarketingData.utmMedium = utmParams?.medium
      newMarketingData.utmSource = utmParams?.source
      newMarketingData.utmiCampaign = utmiParams?.campaign
      newMarketingData.utmiPart = utmiParams?.part
      newMarketingData.utmipage = utmiParams?.page

      if (newMarketingData.marketingTags == null) {
        delete newMarketingData.marketingTags
      } else if (Array.isArray(newMarketingData.marketingTags)) {
        newMarketingData.marketingTags = newMarketingData.marketingTags.filter(
          Boolean
        )
      }

      const atLeastOneValidField = Object.values(newMarketingData).some(
        (value) => {
          if (value == null || value === '') {
            return false
          }

          if (Array.isArray(value) && value.length === 0) {
            return false
          }

          return true
        }
      )

      // If all fields of newMarketingData are invalid, it causes checkout to answer with an error 400
      if (atLeastOneValidField) {
        try {
          await checkout.updateOrderFormMarketingData(
            orderFormId,
            newMarketingData
          )
        } catch (e) {
          ctx.vtex.logger.error({
            message: 'Error when updating orderformmarketing data',
            id: orderFormId,
            chkArgs: JSON.stringify(newMarketingData),
            graphqlArgs: JSON.stringify({ utmParams, utmiParams }),
          })
        }
      }
    }

    const cleanItems = items.map(({ options, ...rest }) => rest)
    const withOptions = items.filter(
      ({ options }) => !!options && options.length > 0
    )

    const addItem = await checkout.addItem(orderFormId, cleanItems)

    await addOptionsForItems(withOptions, checkout, addItem, previousItems)

    return withOptions.length === 0 ? addItem : checkout.orderForm()
  },

  addOrderFormPaymentToken: paymentTokenResolver,

  cancelOrder: async (
    _,
    { orderFormId, reason },
    { clients: { checkout } }
  ) => {
    await checkout.cancelOrder(orderFormId, reason)

    return true
  },

  createPaymentSession: httpResolver({
    enableCookies: true,
    headers: withAuthToken(headers.json),
    method: 'POST',
    secure: true,
    url: paths.gatewayPaymentSession,
  }),

  createPaymentTokens: httpResolver({
    data: ({ payments }: any) => payments,
    enableCookies: true,
    headers: withAuthToken(headers.json),
    method: 'POST',
    url: paths.gatewayTokenizePayment,
  }),

  setOrderFormCustomData: (
    _,
    { appId, field, value },
    { clients: { checkout }, vtex: { orderFormId } }
  ) => {
    if (orderFormId == null) {
      throw new Error('No orderformid in cookies')
    }

    return checkout.setOrderFormCustomData(orderFormId, appId, field, value)
  },

  updateItems: (
    _,
    { orderFormId: paramsOrderFormId, items },
    { clients: { checkout }, vtex }
  ) => {
    const { orderFormId } = vtex

    if (orderFormId == null) {
      throw new Error('No orderformid in cookies')
    }

    if (orderFormId !== paramsOrderFormId) {
      vtex.logger.warn(
        `Different orderFormId found: provided=${paramsOrderFormId} and in cookies=${vtex.orderFormId}`
      )
    }

    return checkout.updateItems(orderFormId, items)
  },

  updateOrderFormIgnoreProfile: (
    _,
    { ignoreProfileData },
    { clients: { checkout }, vtex: { orderFormId } }
  ) => {
    if (orderFormId == null) {
      throw new Error('No orderformid in cookies')
    }

    return checkout.updateOrderFormIgnoreProfile(orderFormId, ignoreProfileData)
  },

  updateOrderFormPayment: (
    _,
    { payments },
    { clients: { checkout }, vtex: { orderFormId } }
  ) => {
    if (orderFormId == null) {
      throw new Error('No orderformid in cookies')
    }

    return checkout.updateOrderFormPayment(orderFormId, payments)
  },

  updateOrderFormProfile: (_, { fields }, ctx) => {
    const {
      clients: { checkout },
      vtex: { orderFormId },
    } = ctx

    if (orderFormId == null) {
      throw new Error('No orderformid in cookies')
    }

    return checkout.updateOrderFormProfile(orderFormId, fields, ctx)
  },

  updateOrderFormShipping: async (_, { address }, ctx) => {
    const {
      clients: { checkout },
      vtex: { orderFormId },
    } = ctx

    if (orderFormId == null) {
      throw new Error('No orderformid in cookies')
    }

    return checkout.updateOrderFormShipping(orderFormId, {
      clearAddressIfPostalCodeNotFound: false,
      selectedAddresses: [address],
    })
  },

  addAssemblyOptions: (
    _,
    { itemId, assemblyOptionsId, options },
    { clients: { checkout }, vtex: { orderFormId } }
  ) => {
    if (orderFormId == null) {
      throw new Error('No orderformid in cookies')
    }

    const body = {
      composition: {
        items: options,
      },
      noSplitItem: true,
    }

    return checkout.addAssemblyOptions(
      orderFormId,
      itemId,
      assemblyOptionsId,
      body
    )
  },

  updateOrderFormCheckin: (
    _,
    { checkin }: any,
    { clients: { checkout }, vtex: { orderFormId } }
  ) => {
    if (orderFormId == null) {
      throw new Error('No orderformid in cookies')
    }

    return checkout.updateOrderFormCheckin(orderFormId, checkin)
  },

  newOrderForm: async (_, { orderFormId }, ctx) => {
    const {
      clients: { checkout },
    } = ctx

    const { data, headers: responseHeaders } = await checkout.newOrderForm(
      orderFormId
    )

    await setCheckoutCookies(responseHeaders, ctx)

    return data
  },
}

```


[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 store-graphql/node/resolvers/checkout/attachmentsHelper.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  all,
  filter,
  find,
  partition,
  propEq,
  omit,
  compose,
  equals,
} from 'ramda'

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

const getNewItemsOnly = (
  previousItems: OrderFormItem[],
  allItems: OrderFormItem[]
) => {
  const idSet = new Set<string>()

  previousItems.forEach((item) => idSet.add(item.uniqueId))

  return allItems.filter((item) => !idSet.has(item.uniqueId))
}

const findRecentlyAddedParent = (
  recentlyAdded: OrderFormItem[],
  id: string,
  assemblyId: string | null
) =>
  recentlyAdded.find(
    (i) => i.id === id && i.parentAssemblyBinding === assemblyId
  )

type OptionItems = Array<Omit<AssemblyOptionInput, 'assemblyId'>>

interface OptionRequestUnit {
  items: OptionItems
  inputValues: Record<string, string | boolean>
}

interface AddOptionsLogicInput {
  checkout: Context['clients']['checkout']
  orderForm: OrderForm
  itemIndex: string | number
  options?: AssemblyOptionInput[]
  oldItems: OrderFormItem[]
}

interface AssemblyOptionBody {
  noSplitItem?: boolean
  composition?: {
    items: Array<{
      id: string
      quantity: number
      seller: string
    }>
  }
  inputValues?: Record<string, string>
}

const addAssemblyBody = (option: OptionRequestUnit) => {
  const body: AssemblyOptionBody = {}

  if (option.items.length > 0) {
    body.noSplitItem = true
    body.composition = {
      items: option.items.map(omit(['options', 'inputValues'])),
    }
  }

  if (option.inputValues) {
    body.noSplitItem = true
    body.inputValues = Object.keys(option.inputValues).reduce<
      Record<string, string>
    >((acc, key) => {
      // Transforming boolean values to string
      acc[key] = `${option.inputValues[key]}`

      return acc
    }, {})
  }

  return body
}

const removeAssemblyBody = (option: OptionRequestUnit) => ({
  composition: {
    items: option.items.map(omit(['quantity', 'options'])),
  },
})

const joinOptionsWithType = (options: AssemblyOptionInput[]) => {
  const result: Record<string, OptionRequestUnit> = {}

  for (const option of options) {
    const { assemblyId, ...rest } = option
    const currentArray = result[assemblyId]?.items || []

    if (rest.id) {
      currentArray.push(rest)
    }

    result[assemblyId] = {
      inputValues: rest.inputValues,
      items: currentArray,
    }
  }

  return result
}

const addOptionsRecursive = async (
  items: OptionItems,
  assemblyId: string,
  orderForm: OrderForm,
  oldItems: OrderFormItem[],
  checkout: Context['clients']['checkout']
) => {
  const recentlyAdded = getNewItemsOnly(oldItems, orderForm.items)
  const results = []

  for (const item of items) {
    const parentItem = findRecentlyAddedParent(
      recentlyAdded,
      item.id!.toString(),
      assemblyId
    )

    const parentIndex =
      parentItem &&
      orderForm.items.findIndex(propEq('uniqueId', parentItem.uniqueId))

    if (parentIndex == null || parentIndex < 0) {
      continue
    }

    results.push(
      addOptionsLogic({
        checkout,
        itemIndex: parentIndex,
        options: item.options,
        orderForm,
        oldItems,
      })
    )
  }

  await Promise.all(results)
}

const addOptionsLogic = async (input: AddOptionsLogicInput) => {
  const { checkout, orderForm, itemIndex, options, oldItems } = input

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

  const isRemove = (option: AssemblyOptionInput) => option.quantity === 0
  const [toRemove, toAdd] = partition<AssemblyOptionInput>(isRemove, options)
  const joinedToAdd = joinOptionsWithType(toAdd)
  const joinedToRemove = joinOptionsWithType(toRemove)
  const idsToAdd = Object.keys(joinedToAdd)
  const idsToRemove = Object.keys(joinedToRemove)
  let recentOrderForm = orderForm

  for (const assemblyId of idsToRemove) {
    const parsedOptions = joinedToRemove[assemblyId]
    // eslint-disable-next-line no-await-in-loop
    const response = await checkout
      .removeAssemblyOptions(
        orderForm.orderFormId,
        itemIndex,
        assemblyId,
        removeAssemblyBody(parsedOptions)
      )
      // eslint-disable-next-line no-loop-func
      .catch(() => ({ data: recentOrderForm }))

    recentOrderForm = response.data
  }

  for (const assemblyId of idsToAdd) {
    const parsedOptions = joinedToAdd[assemblyId]

    // eslint-disable-next-line no-await-in-loop
    recentOrderForm = await checkout
      .addAssemblyOptions(
        orderForm.orderFormId,
        itemIndex,
        assemblyId,
        addAssemblyBody(parsedOptions)
      )
      // eslint-disable-next-line no-loop-func
      .catch(() => recentOrderForm)
  }

  const results = []

  for (const assemblyId of idsToAdd) {
    const parsedOptions = joinedToAdd[assemblyId]
    const itemsWithRecursiveOptions = parsedOptions.items.filter(
      (arg) => !!arg.options
    )

    if (itemsWithRecursiveOptions.length > 0) {
      results.push(
        addOptionsRecursive(
          itemsWithRecursiveOptions,
          assemblyId,
          recentOrderForm,
          oldItems,
          checkout
        )
      )
    }
  }

  await Promise.all(results)
}

/**
 * Goes through each item being added and check if there are any attachments into that item object.
 * If yes, call appropriate checkout API to add assembly option to that item.
 * @param items items coming from the addItem mutation
 * @param checkout checkout datasource
 * @param orderForm order form object with current items and ID
 */
export const addOptionsForItems = async (
  items: OrderFormItemInput[],
  checkout: Context['clients']['checkout'],
  orderForm: OrderForm,
  oldItems: OrderFormItem[]
) => {
  const recentlyAdded =
    items.length > 0 ? getNewItemsOnly(oldItems, orderForm.items) : []

  const results = []

  for (const item of items) {
    if (!item.options || item.options.length === 0) {
      continue
    }

    const parentItem = findRecentlyAddedParent(
      recentlyAdded,
      item.id!.toString(),
      null
    )

    const parentIndex =
      parentItem &&
      orderForm.items.findIndex(propEq('uniqueId', parentItem.uniqueId))

    if (parentIndex == null || parentIndex < 0) {
      continue
    }

    results.push(
      addOptionsLogic({
        checkout,
        itemIndex: parentIndex,
        options: item.options,
        orderForm,
        oldItems,
      })
    )
  }

  await Promise.all(results)
}

const filterCompositionNull = (assemblyOptions: AssemblyOption[]) =>
  assemblyOptions.filter(({ composition }) => !!composition)

export const buildAssemblyOptionsMap = (orderForm: OrderForm) => {
  const metadataItems = orderForm?.itemMetadata?.items ?? []

  return metadataItems
    .filter(
      ({ assemblyOptions }) => assemblyOptions && assemblyOptions.length > 0
    )
    .reduce((prev, curr) => {
      prev[curr.id] = filterCompositionNull(curr.assemblyOptions)

      return prev
    }, {} as Record<string, AssemblyOption[]>)
}

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

  return composition.minQuantity === 1 && composition.maxQuantity === 1
}

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

  return all(propEq('maxQuantity', 1))(composition.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
}

const getItemComposition = (
  id: string,
  childAssemblyData?: AssemblyOption
): CompositionItem | undefined => {
  if (!childAssemblyData) {
    return undefined
  }

  const items = childAssemblyData.composition?.items ?? []

  return find<CompositionItem>(propEq('id', id), items)
}

const isSonOfItem = (parentIndex: number) =>
  propEq('parentItemIndex', parentIndex)

export const isParentItem = ({
  parentItemIndex,
  parentAssemblyBinding,
}: OrderFormItem) => parentItemIndex == null && parentAssemblyBinding == null

export const getPositionInOrderForm = (
  { items }: OrderForm,
  { uniqueId }: OrderFormItem
) => items.findIndex((orderItem) => orderItem.uniqueId === uniqueId)

export const buildAddedOptionsForItem = (
  item: OrderFormItem,
  index: number,
  childs: OrderFormItem[],
  assemblyOptionsMap: Record<string, AssemblyOption[]>,
  orderForm: OrderForm
) => {
  const children = filter<OrderFormItem>(isSonOfItem(index), childs)

  return children.map((childItem) => {
    const parentAssemblyOptions = assemblyOptionsMap[item.id]

    const childAssemblyData = find<AssemblyOption>(
      propEq('id', childItem.parentAssemblyBinding)
    )(parentAssemblyOptions)

    const compositionItem = getItemComposition(
      childItem.id,
      childAssemblyData
    ) ?? { initialQuantity: 0 }

    return {
      choiceType: getItemChoiceType(childAssemblyData),
      compositionItem,
      extraQuantity:
        childItem.quantity / item.quantity - compositionItem.initialQuantity,
      item: {
        ...childItem,
        assemblyOptionsData: {
          index: getPositionInOrderForm(orderForm, childItem),
          assemblyOptionsMap,
          childs,
          orderForm,
        },
      },
      normalizedQuantity: childItem.quantity / item.quantity,
    }
  })
}

const findInitialItemOnCart = (initialItem: InitialItem) => (
  cartItem: OrderFormItem
) =>
  cartItem.parentAssemblyBinding === initialItem.parentAssemblyBinding &&
  initialItem.id === cartItem.id

const isInitialItemMissing = (
  parentCartItem: OrderFormItem,
  orderForm: OrderForm
) => (initialItem: InitialItem): RemovedItem | null => {
  const orderFormItem = find(
    findInitialItemOnCart(initialItem),
    orderForm.items
  )

  const selectedQuantity =
    orderFormItem && orderFormItem.quantity / parentCartItem.quantity

  // If we selected more or same as initialQuantity, item is not missing
  if (selectedQuantity && selectedQuantity >= initialItem.initialQuantity) {
    return null
  }

  const metadataItems = orderForm?.itemMetadata?.items
  const metadataItem =
    metadataItems && find(propEq('id', initialItem.id), metadataItems)

  if (!metadataItem) {
    return null
  }

  return {
    initialQuantity: initialItem.initialQuantity,
    name: metadataItem.name,
    removedQuantity: initialItem.initialQuantity - (selectedQuantity ?? 0),
  }
}

interface InitialItem extends CompositionItem {
  parentAssemblyBinding: string
}

const isAssemblyOptionToggle = compose<AssemblyOption, string, boolean>(
  equals(CHOICE_TYPES.TOGGLE),
  getItemChoiceType
)

export const buildRemovedOptions = (
  item: OrderFormItem,
  orderForm: OrderForm,
  assemblyOptionsMap: Record<string, AssemblyOption[]>
): RemovedItem[] => {
  const assemblyOptions = assemblyOptionsMap[item.id]

  if (!assemblyOptions) {
    return []
  }

  // For now, it makes sense it should only work for toggle type of assembly options
  const onlyToggleAssemblies = assemblyOptions.filter(isAssemblyOptionToggle)

  const itemsWithInitials: InitialItem[] = []

  for (const assemblyOption of onlyToggleAssemblies) {
    if (assemblyOption.composition) {
      for (const compItem of assemblyOption.composition.items) {
        if (compItem.initialQuantity > 0) {
          itemsWithInitials.push({
            ...compItem,
            parentAssemblyBinding: assemblyOption.id,
          })
        }
      }
    }
  }

  const removed = itemsWithInitials
    .map(isInitialItemMissing(item, orderForm))
    .filter(Boolean) as RemovedItem[]

  return removed
}

```


[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 store-graphql/node/resolvers/checkout/checkoutSLA.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

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

export const fieldResolvers = {
  CheckoutSLA: {
    cacheId: prop('id'),
  },
}

```


[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 store-graphql/node/resolvers/profile/fieldResolvers.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

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

import {
  getAddresses,
  getPasswordLastUpdate,
  getPayments,
  pickCustomFieldsFromData,
} from './services'

export default {
  Address: {
    cacheId: prop('addressName'),
    id: prop('addressName'),
    geoCoordinates: ({ geoCoordinate, geoCoordinates }: any) =>
      geoCoordinate || geoCoordinates,
  },
  PaymentProfile: {
    cacheId: prop('id'),
  },
  Profile: {
    address: (obj: any, __: any, context: any) => getAddresses(context, obj),
    addresses: (obj: any, __: any, context: any) => getAddresses(context, obj),
    birthDate: (obj: any) =>
      obj.birthDate ? new Date(obj.birthDate).toISOString() : obj.birthDate,
    cacheId: prop('email'),
    customFields: (obj: any) =>
      typeof obj.customFields === 'string'
        ? pickCustomFieldsFromData(obj.customFields, obj)
        : obj.customFields,
    passwordLastUpdate: (_: any, __: any, context: any) =>
      getPasswordLastUpdate(context),
    payments: (_: any, __: any, context: any) => getPayments(context),
    profilePicture: (obj: any, _: any, context: any) =>
      obj.profilePicture &&
      `https://${context.vtex.account}.vteximg.com.br/assets/vtex.store-graphql/image/${obj.profilePicture}`,
    // the next transformations are necessary since the profile system and
    // this profile graphql query (the same applies to mutations) were built upon different contracts.
    corporateDocument: (obj: any, _: any, __: any) => obj.businessDocument,
    isCorporate: (obj: any, _: any, __: any) => obj.isPJ === 'True',
    tradeName: (obj: any, _: any, __: any) => obj.fancyName,
    pii: (obj: any, _: any, __: any) => obj.pii,
  },
  ProfileCustomField: {
    cacheId: (root: any) => root.key,
  },
}

```


[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 store-graphql/node/resolvers/profile/index.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { path } from 'ramda'
import { MutationSaveAddressArgs } from 'vtex.store-graphql'

import type { DefaultUser, User } from '../../dataSources/identity'
import fieldR from './fieldResolvers'
import {
  createAddress,
  deleteAddress,
  getProfile,
  updateAddress,
  updateProfile,
  updateProfilePicture,
  saveAddress,
} from './services'

const TRUE = 'True'
const FALSE = 'False'

interface CheckUserAuthorizationParams {
  context: Context
  email: string
}

const checkUserAuthorization = async ({
  context,
  email,
}: CheckUserAuthorizationParams): Promise<boolean> => {
  const {
    vtex: { storeUserAuthToken, account },
    dataSources: { identity },
  } = context

  let validUser = !!storeUserAuthToken

  const tokenUser = await identity
    .getUserWithToken(storeUserAuthToken ?? '')
    .catch(() => {
      return { authStatus: '' } as DefaultUser
    })

  validUser =
    validUser &&
    Boolean(tokenUser) &&
    'id' in tokenUser &&
    tokenUser.account === account &&
    tokenUser.user.length === email.length

  for (let i = 0; i < email.length; i++) {
    if (validUser && email[i] !== (tokenUser as User)?.user?.[i]) {
      validUser = false
    }
  }

  return validUser
}

interface SubscribeNewsletterArgs {
  email: string
  fields?: {
    name?: string
    phone?: string
    bindingUrl?: string
    bindingId?: string
  }
  isNewsletterOptIn: boolean
}

export const mutations = {
  createAddress: (_: any, { fields }: any, context: Context) =>
    createAddress(context, fields),

  saveAddress: (_: void, args: MutationSaveAddressArgs, context: Context) =>
    saveAddress(context, args),

  deleteAddress: (_: any, { id }: any, context: Context) =>
    deleteAddress(context, id),

  updateAddress: (_: any, args: any, context: Context) =>
    updateAddress(context, args),

  updateProfile: (_: any, { fields, customFields }: any, context: Context) =>
    updateProfile(context, fields, customFields),

  updateProfilePicture: (_: any, __: any, context: Context) =>
    updateProfilePicture('updateProfilePicture', context),

  uploadProfilePicture: (_: any, __: any, context: Context) =>
    updateProfilePicture('uploadProfilePicture', context),

  subscribeNewsletter: async (
    _: any,
    { email, fields, isNewsletterOptIn }: SubscribeNewsletterArgs,
    context: Context
  ) => {
    const { profile } = context.clients
    const optIn =
      isNewsletterOptIn === undefined || isNewsletterOptIn === true
        ? TRUE
        : FALSE

    const updatedPersonalPreferences: PersonalPreferences = {
      isNewsletterOptIn: optIn,
    }

    const userProfile = await profile
      .getProfileInfo({ email, userId: '' }, context, undefined)
      .catch(() => ({}))

    if (fields) {
      const { name, phone, bindingId, bindingUrl } = fields

      const userHasFirstName = Boolean(userProfile.firstName)
      const userHasPhone = Boolean(userProfile.cellPhone)
      const userHasBindingId = Boolean(userProfile.bindingId)
      const userHasBindingUrl = Boolean(userProfile.bindingUrl)

      // Prevents 'firstName' field from being overridden.
      if (!userHasFirstName && name) {
        updatedPersonalPreferences.firstName = name
      }

      // Prevents 'homePhone' field from being overridden.
      if (!userHasPhone && phone) {
        updatedPersonalPreferences.homePhone = phone
      }

      // Prevents 'bindingId' field from being overridden.
      if (!userHasBindingId && bindingId) {
        updatedPersonalPreferences.bindingId = bindingId
      }

      // Prevents 'bindingUrl' field from being overridden.
      if (!userHasBindingUrl && bindingUrl) {
        updatedPersonalPreferences.bindingUrl = bindingUrl
      }
    }

    let updateData = true

    if (userProfile.createdIn) {
      updateData = await checkUserAuthorization({
        context,
        email,
      })
    }

    if (updateData) {
      await profile.updatePersonalPreferences(
        { email, userId: '' },
        updatedPersonalPreferences,
        context
      )
    }

    return true
  },
}

export const queries = {
  profile: (_: any, { customFields }: any, context: Context) =>
    getProfile(context, customFields),

  checkProfileAllowed: async (_: any, __: any, context: Context) => {
    const {
      clients: { catalog, customSession },
      vtex: { segment },
      cookies,
    } = context

    const salesChannel = segment ? segment.channel : null

    const { sessionData } = await customSession.getSession(
      cookies.get('vtex_session')!,
      ['*']
    )

    const email: string | undefined = path(
      ['namespaces', 'profile', 'email', 'value'],
      sessionData
    )

    const availableSalesChannels = await catalog
      .salesChannelAvailable(email)
      .catch(() => [])

    // Checking with `==` since `sc.Id` is an Integer and salesChannel a string
    // eslint-disable-next-line eqeqeq
    const available = availableSalesChannels.find((sc) => sc.Id == salesChannel)

    return {
      allowed: Boolean(available),
      condition: available
        ? 'authorized'
        : email
        ? 'forbidden' // The user is logged in and not allowed
        : 'unauthorized', // We don't know the user identity
    }
  },
}

export const fieldResolvers = fieldR

```


[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 store-graphql/node/resolvers/profile/services.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { parse } from 'cookie'
import { compose, mapObjIndexed, pick, split, values } from 'ramda'
import { MutationSaveAddressArgs, AddressInput } from 'vtex.store-graphql'

import { generateRandomName } from '../../utils'
import { makeRequest } from '../auth'
import paths from '../paths'

export async function getProfile(context: Context, customFields?: string) {
  const {
    clients: { profile },
    vtex: { currentProfile },
  } = context

  const extraFields = customFields
    ? `${customFields},profilePicture,id`
    : `profilePicture,id`

  return profile
    .getProfileInfo(currentProfile, context, extraFields)
    .then((profileData) => {
      if (profileData) {
        return {
          ...profileData,
          customFields,
        }
      }

      return { email: currentProfile.email }
    })
}

export function getPasswordLastUpdate(context: Context) {
  const {
    request: {
      headers: { cookie },
    },
    vtex: { account },
  } = context

  const url = paths.getUser(account)
  const parsedCookies = parse(cookie)
  const userCookie: string = parsedCookies[`VtexIdclientAutCookie_${account}`]

  if (!userCookie) return null

  return makeRequest({
    ctx: context.vtex,
    url,
    authCookieAdmin: userCookie,
  }).then((response: any) => response.data.passwordLastUpdate)
}

export function getAddresses(context: Context, currentUserProfile?: Profile) {
  const {
    clients: { profile },
    vtex: { currentProfile },
  } = context

  // Filter out temporarily addresses with addressType "pickup" because its also saved at Profile V2
  return profile
    .getUserAddresses(currentProfile, context, currentUserProfile)
    .then((addresses: any[]) => {
      const residentialAddresses = addresses.filter(
        (address: { addressType: string }) => address.addressType !== 'pickup'
      )
      return residentialAddresses
    })
}

export async function getPayments(context: Context) {
  const {
    clients: { profile, licenseManagerExtended },
    vtex: { currentProfile },
  } = context

  const { PIIEnabled } = await licenseManagerExtended.getCurrentAccount()
  const paymentsRawData = await profile.getUserPayments(currentProfile, context)
  const isPaymentDataEmpty = PIIEnabled
    ? !paymentsRawData[0]?.document?.paymentData
    : !paymentsRawData?.paymentData

  if (isPaymentDataEmpty) {
    return null
  }
  const addresses = await getAddresses(context)

  const paymentData = PIIEnabled
    ? paymentsRawData[0]?.document?.paymentData
    : JSON.parse(paymentsRawData.paymentData)

  const { availableAccounts } = paymentData

  return availableAccounts.map((account: any) => {
    const { bin, availableAddresses, accountId, ...cleanAccount } = account
    const accountAddress = addresses.find(
      (addr: Address) => addr.addressName === availableAddresses[0]
    )

    return { ...cleanAccount, id: accountId, address: accountAddress }
  })
}

export async function updateProfile(
  context: Context,
  profile: Profile,
  customFields: CustomField[] | undefined
) {
  const {
    clients,
    vtex: { currentProfile },
  } = context

  const extraFields = customFields && mapCustomFieldsToObjNStr(customFields)

  // Remove email field from profile to prevent sending it in the payload
  const { email, ...profileWithoutEmail } = profile

  const newData = {
    ...profileWithoutEmail,
    // Read the comments in Profile in fieldResolvers.ts files
    // to understand the following transformations
    businessDocument: profile.corporateDocument,
    isPJ: profile.isCorporate ? 'True' : 'False',
    fancyName: profile.tradeName,
    ...extraFields?.customFieldsObj,
  }

  return clients.profile
    .updateProfileInfo(
      currentProfile,
      newData,
      context,
      extraFields?.customFieldsStr
    )
    .then(() => getProfile(context, extraFields?.customFieldsStr))
}

export function updateProfilePicture(mutationsName: string, context: Context) {
  console.warn(
    `The ${mutationsName} mutation is deprecated and no longer supported.`
  )

  return getProfile(context)
}

// CRUD Address

export function createAddress(context: Context, address: AddressInput) {
  const {
    clients: { profile },
    vtex: { currentProfile },
  } = context

  const addressesData = mapNewAddressToProfile(address, currentProfile)

  return profile
    .updateAddress(currentProfile, addressesData, context)
    .then(() => getProfile(context))
}

export function deleteAddress(context: Context, addressName: string) {
  const {
    clients: { profile },
    vtex: { currentProfile },
  } = context

  return profile
    .deleteAddress(currentProfile, addressName, context)
    .then(() => getProfile(context))
}

export function updateAddress(
  context: Context,
  { id, fields: { geoCoordinates, ...addressFields } }: UpdateAddressArgs
) {
  const {
    clients: { profile },
    vtex: { currentProfile },
  } = context

  const addressesData = {} as any

  addressesData[id] = JSON.stringify({
    ...addressFields,
    geoCoordinate: geoCoordinates,
    userId: currentProfile.userId,
  })

  return profile
    .updateAddress(currentProfile, addressesData, context)
    .then(() => getProfile(context))
}

export function pickCustomFieldsFromData(customFields: string, data: any) {
  return (
    customFields &&
    compose(
      values,
      mapObjIndexed((value, key) => ({ key, value })),
      pick(split(',', customFields)) as any
    )(data)
  )
}

export async function saveAddress(
  context: Context,
  args: MutationSaveAddressArgs
): Promise<Address> {
  const {
    clients: { profile },
    vtex: { currentProfile },
  } = context

  const addressesData = mapNewAddressToProfile(args.address, currentProfile)
  const [newId] = Object.keys(addressesData)

  const result = await profile.updateAddress(
    currentProfile,
    addressesData,
    context
  )

  if (result?.document) {
    return result.document
  }

  if (result?.id) {
    return result.id
  }

  const currentAddresses = await profile.getUserAddresses(
    currentProfile,
    context
  )

  return currentAddresses.find(
    (address: Address) => address.addressName === newId
  ) as Address
}

// Aux

function mapCustomFieldsToObjNStr(customFields: CustomField[] = []) {
  let customFieldsStr = ''
  const customFieldsObj = customFields.reduce((acc: any, currentValue, i) => {
    customFieldsStr +=
      i < customFields.length - 1 ? `${currentValue.key},` : currentValue.key

    acc[currentValue.key] = currentValue.value

    return acc
  }, {})

  return {
    customFieldsObj,
    customFieldsStr,
  }
}

function mapNewAddressToProfile(
  address: AddressInput,
  currentProfile: CurrentProfile,
  id: string = generateRandomName()
) {
  const { geoCoordinates, ...addr } = address

  return {
    [id]: JSON.stringify({
      ...addr,
      geoCoordinate: geoCoordinates,
      addressName: id,
      userId: currentProfile.userId,
    }),
  }
}

interface UpdateAddressArgs {
  id: string
  fields: Address
}

interface CustomField {
  key: string
  value: string
}

```


[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 store-graphql/node/resolvers/list/util.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { UserInputError } from '@vtex/api'
import { filter, path } from 'ramda'

import { Clients } from '../../clients'
import { Catalog } from '../../clients/catalog'

export const fields = [
  'name',
  'isPublic',
  'isEditable',
  'owner',
  'createdIn',
  'updatedIn',
  'items',
  'id',
]
export const fieldsListProduct = [
  'id',
  'quantity',
  'skuId',
  'productId',
  'createdIn',
]
export const acronymListProduct = 'LP'
export const acronymList = 'WL'

export interface Item {
  id: string
  skuId: string
  productId: string
  quantity: number
}

const checkListItemQuantity = (quantity: any) => {
  if (!quantity || quantity < 0) {
    throw new UserInputError('The item quantity should be greater than 0')
  }
}

// Make this query to check if the skuId received is valid
// If it isn't, it throws an exception.
const checkProduct = async (item: Item, catalog: Catalog) => {
  const response = await catalog.productBySku([path(['skuId'], item) as string])

  if (!response.length) {
    throw new UserInputError('Cannot add an invalid product')
  }
}

const checkDuplicatedListItem = (items: Item[], item: Item) => {
  const itemDuplicated = filter(
    (i: Item) => path(['skuId'], i) === path(['skuId'], item),
    items
  )

  if (itemDuplicated.length > 1) {
    throw new UserInputError('Cannot add duplicated items.')
  }
}

const validateListItem = (items: Item[], item: Item, clients: Clients) => {
  const { catalog } = clients

  checkListItemQuantity(path(['quantity'], item))
  checkDuplicatedListItem(items, item)
  checkProduct(item, catalog)
}

// eslint-disable-next-line @typescript-eslint/default-param-last
const validateItems = (items: Item[] = [], clients: Clients) => {
  items.forEach((item) => validateListItem(items, item, clients))
}

export { validateItems, validateListItem }

```


[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 store-graphql/node/resolvers/list/index.ts from the store-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { UserInputError } from '@vtex/api'
import { concat, groupBy, last, map, nth, path } from 'ramda'

import { Catalog } from '../../clients/catalog'
import { MasterData } from '../../clients/masterdata'
import {
  acronymList,
  acronymListProduct,
  fields,
  fieldsListProduct,
  Item,
  validateItems,
} from './util'

const getListItemsWithProductInfo = (items: Item[], catalog: Catalog) =>
  Promise.all(
    map(async (item: Item) => {
      const productsResponse = await catalog.productBySku([
        path(['skuId'], item) as string,
      ])

      const product = nth(0, productsResponse)

      return { ...item, product }
    }, items)
  )

const getListItems = async (
  itemsId: string[],
  catalog: Catalog,
  masterdata: MasterData
) => {
  const items: Item[] = itemsId
    ? ((await Promise.all(
        map(
          (id: string) =>
            masterdata.getDocument(acronymListProduct, id, fieldsListProduct),
          itemsId
        )
      )) as Item[])
    : []

  return getListItemsWithProductInfo(items, catalog)
}

const addListItem = async (item: Item, masterdata: MasterData) => {
  const { DocumentId } = (await masterdata.createDocument(
    acronymListProduct,
    item
  )) as DocumentResponse

  return DocumentId
}

const addItems = async (
  // eslint-disable-next-line @typescript-eslint/default-param-last
  items: Item[] = [],
  { clients, clients: { masterdata } }: Context
) => {
  validateItems(items, clients)
  const promises = map(async (item) => addListItem(item, masterdata), items)

  return Promise.all(promises)
}

const deleteItems = (items: Item[], masterdata: MasterData) =>
  items &&
  Promise.all(
    items.map((item: Item) =>
      masterdata.deleteDocument(
        acronymListProduct,
        path(['id'], item) as string
      )
    )
  )

const updateItems = async (items: Item[], masterdata: MasterData) => {
  const itemsWithoutDuplicated = Object.values(
    groupBy(({ skuId }) => skuId, items)
  ).map((itemArray) => last(itemArray)) as Item[]

  const itemsToBeDeleted = itemsWithoutDuplicated.filter(
    (item) => item?.id && item?.quantity === 0
  )

  const itemsToBeAdded = itemsWithoutDuplicated.filter((item) => !item?.id)
  const itemsToBeUpdated = itemsWithoutDuplicated.filter(
    (item) => item?.id && item?.quantity > 0
  )

  deleteItems(itemsToBeDeleted, masterdata)

  const itemsIdAdded = await Promise.all(
    map(async (item: Item) => addListItem(item, masterdata), itemsToBeAdded)
  )

  const itemsIdUpdated = map((item: Item) => {
    masterdata.updateDocument(
      acronymListProduct,
      path(['id'], item) ?? '',
      item
    )

    return path(['id'], item)
  }, itemsToBeUpdated)

  return concat(itemsIdAdded, itemsIdUpdated)
}

export const queries = {
  list: async (
    _: any,
    { id }: any,
    { clients: { masterdata, catalog } }: Context
  ) => {
    const list = await masterdata.getDocument<any>(acronymList, id, fields)
    const items = await getListItems(list.items, catalog, masterdata)

    return { id, ...list, items }
  },

  listsByOwner: async (
    _: any,
    { owner, page, pageSize }: any,
    context: Context
  ) => {
    const {
      clients: { masterdata },
    } = context

    const lists = (await masterdata.searchDocuments<any>(
      acronymList,
      ['id'],
      `owner=${owner}`,
      { page, pageSize }
    )) as any[]

    const listsWithProducts = map(
      (list) => queries.list(_, { id: list.id }, context),
      lists
    )

    return Promise.all(listsWithProducts)
  },
}

export const mutation = {
  createList: async (
    _: any,
    { list, list: { items } }: any,
    context: Context
  ) => {
    const {
      clients: { masterdata },
    } = context

    try {
      const itemsId = await addItems(items, context)
      const { DocumentId } = (await masterdata.createDocument(acronymList, {
        ...list,
        items: itemsId,
      })) as DocumentResponse

      return queries.list(_, { id: DocumentId }, context)
    } catch (error) {
      throw new UserInputError(`Cannot create list: ${error}`)
    }
  },

  deleteList: async (
    _: any,
    { id }: any,
    { clients: { masterdata } }: Context
  ) => {
    const { items } = await masterdata.getDocument<any>(acronymList, id, fields)

    await deleteItems(items, masterdata)

    return masterdata.deleteDocument(acronymList, id)
  },

  /**
   * Update the list informations and its items.
   * If the item given does not have the id, add it as a new item in the list
   * If the item given has got an id, but its quantity is 0, remove it from the list
   * Otherwise, update it.
   */
  updateList: async (
    _: any,
    { id, list, list: { items } }: any,
    context: Context
  ) => {
    const {
      clients: { masterdata },
    } = context

    try {
      const itemsUpdatedId = await updateItems(items, masterdata)

      await masterdata.updateDocument(acronymList, id, {
        ...list,
        items: itemsUpdatedId,
      } as any)

      return queries.list(_, { id }, context)
    } catch (error) {
      throw new UserInputError(`Cannot update the list: ${error}`)
    }
  },
}

```