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

[Assistant]
Here's the implementation:

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

import {
  APP_NAME,
  QUOTE_DATA_ENTITY,
  QUOTE_FIELDS,
  routes,
  SCHEMA_VERSION,
} from '../../constants'
import { sendCreateQuoteMetric } from '../../metrics/createQuote'
import type { UseQuoteMetricsParams } from '../../metrics/useQuote'
import { sendUseQuoteMetric } from '../../metrics/useQuote'
import { isEmail } from '../../utils'
import GraphQLError from '../../utils/GraphQLError'
import message from '../../utils/message'
import {
  checkAndCreateQuotesConfig,
  checkConfig,
  defaultSettings,
} from '../utils/checkConfig'
import {
  checkOperationsForUpdateQuote,
  checkPermissionsForUpdateQuote,
  checkQuoteStatus,
  checkSession,
} from '../utils/checkPermissions'
import {
  createItemComparator,
  createQuoteObject,
  splitItemsBySeller,
} from '../utils/quotes'
import SellerQuotesController from '../utils/sellerQuotesController'

export const Mutation = {
  clearCart: async (_: any, params: any, ctx: Context) => {
    const {
      vtex: { account, logger },
      clients: { hub },
    } = ctx

    try {
      await hub.post(routes.clearCart(account, params.orderFormId), {
        expectedOrderFormSections: ['items'],
      })
    } catch (error) {
      logger.error({
        error,
        message: 'clearCart-error',
      })
      if (error.message) {
        throw new GraphQLError(error.message)
      } else if (error.response?.data?.message) {
        throw new GraphQLError(error.response.data.message)
      } else {
        throw new GraphQLError(error)
      }
    }
  },
  createQuote: async (
    _: any,
    {
      input: { referenceName, items, subtotal, note, sendToSalesRep },
    }: {
      input: {
        referenceName: string
        items: QuoteItem[]
        subtotal: number
        note: string
        sendToSalesRep: boolean
      }
    },
    ctx: Context
  ) => {
    const {
      clients: { masterdata },
      vtex,
      vtex: { logger },
    } = ctx

    const settings = await checkConfig(ctx)
    const { sessionData, storefrontPermissions, segmentData } = vtex as any

    checkSession(sessionData)

    if (!storefrontPermissions?.permissions?.includes('create-quotes')) {
      throw new GraphQLError('operation-not-permitted')
    }

    try {
      let quoteBySeller: SellerQuoteMap = {}

      if (settings?.adminSetup.quotesManagedBy === 'SELLER') {
        const sellerItems = items.filter(
          ({ seller }) => seller && seller !== '1'
        )

        quoteBySeller = await splitItemsBySeller({
          ctx,
          items: sellerItems,
        })
      }

      const sellerQuotesQuantity = Object.keys(quoteBySeller).length

      const remainingItems = items.filter(
        (item) =>
          !Object.values(quoteBySeller).some((quote) =>
            quote.items.some(createItemComparator(item))
          )
      )

      const oneSellerQuoteAndNoRemainingItems =
        sellerQuotesQuantity === 1 && !remainingItems.length

      const isOnlyOneQuote =
        !sellerQuotesQuantity || oneSellerQuoteAndNoRemainingItems

      const [firstSellerQuote] = Object.values(quoteBySeller)

      let parentQuoteItems = sellerQuotesQuantity ? remainingItems : items

      if (isOnlyOneQuote && firstSellerQuote) {
        parentQuoteItems = firstSellerQuote.items
      }

      const quoteCommonFields = {
        sessionData,
        storefrontPermissions,
        segmentData,
        settings,
        referenceName,
        note,
        sendToSalesRep,
        sellerQuotesQuantity,
      }

      const parentQuote = createQuoteObject({
        ...quoteCommonFields,
        items: parentQuoteItems,
        subtotal,
        ...(isOnlyOneQuote &&
          firstSellerQuote && {
            seller: firstSellerQuote.seller,
            sellerName: firstSellerQuote.sellerName,
          }),
      })

      const { DocumentId: parentQuoteId } = await masterdata.createDocument({
        dataEntity: QUOTE_DATA_ENTITY,
        fields: parentQuote,
        schema: SCHEMA_VERSION,
      })

      if (isOnlyOneQuote && firstSellerQuote) {
        await ctx.clients.sellerQuotes.notifyNewQuote(
          firstSellerQuote.seller,
          parentQuoteId,
          parentQuote.creationDate
        )
      }

      if (!isOnlyOneQuote) {
        const childrenQuoteIds: string[] = []

        if (parentQuoteItems.length) {
          const marketplaceSubtotal = parentQuoteItems.reduce(
            (acc, item) => acc + item.sellingPrice * item.quantity,
            0
          )

          const marketplaceSeller = await ctx.clients.seller.getSeller('1')

          const marketplaceQuote = createQuoteObject({
            ...quoteCommonFields,
            items: parentQuoteItems,
            subtotal: marketplaceSubtotal,
            seller: '1',
            sellerName: marketplaceSeller?.name ?? ctx.vtex.account,
            parentQuote: parentQuoteId,
          })

          const {
            DocumentId: markerplaceQuoteId,
          } = await masterdata.createDocument({
            dataEntity: QUOTE_DATA_ENTITY,
            fields: marketplaceQuote,
            schema: SCHEMA_VERSION,
          })

          childrenQuoteIds.push(markerplaceQuoteId)
        }

        const sellerQuoteIds = await Promise.all(
          Object.entries(quoteBySeller).map(async ([seller, sellerQuote]) => {
            const sellerQuoteObject = createQuoteObject({
              ...quoteCommonFields,
              ...sellerQuote,
              parentQuote: parentQuoteId,
            })

            const data = await masterdata.createDocument({
              dataEntity: QUOTE_DATA_ENTITY,
              fields: sellerQuoteObject,
              schema: SCHEMA_VERSION,
            })

            await ctx.clients.sellerQuotes.notifyNewQuote(
              seller,
              data.DocumentId,
              sellerQuoteObject.creationDate
            )

            return data.DocumentId
          })
        )

        childrenQuoteIds.push(...sellerQuoteIds)

        if (childrenQuoteIds.length) {
          await masterdata.updatePartialDocument({
            dataEntity: QUOTE_DATA_ENTITY,
            fields: {
              hasChildren: true,
              childrenQuantity: childrenQuoteIds.length,
            },
            id: parentQuoteId,
            schema: SCHEMA_VERSION,
          })
        }
      }

      if (sendToSalesRep) {
        message(ctx)
          .quoteCreated({
            costCenter: parentQuote.costCenter,
            id: parentQuoteId,
            lastUpdate: {
              email: parentQuote.creatorEmail,
              note,
              status: parentQuote.status.toUpperCase(),
            },
            name: referenceName,
            organization: parentQuote.organization,
          })
          .then(() => {
            logger.info({
              message: `[Quote created] E-mail sent to sales reps`,
            })
          })
      }

      const metricsParam = {
        sessionData,
        userData: {
          orgId: parentQuote.organization,
          costId: parentQuote.costCenter,
          roleId: parentQuote.creatorRole,
        },
        costCenterName: 'costCenterData?.getCostCenterById?.name',
        buyerOrgName: 'organizationData?.getOrganizationById?.name',
        quoteId: parentQuoteId,
        quoteReferenceName: referenceName,
        sendToSalesRep,
        creationDate: parentQuote.creationDate,
      }

      sendCreateQuoteMetric(ctx, metricsParam)

      return parentQuoteId
    } catch (error) {
      logger.error({
        error,
        message: 'createQuote-error ',
      })

      const errorMessage =
        error.message || error.response?.data?.message || error

      throw new GraphQLError(errorMessage)
    }
  },
  updateQuote: async (
    _: any,
    {
      input: { id, items, subtotal, note, decline, expirationDate },
    }: {
      input: {
        id: string
        items: QuoteItem[]
        subtotal: number
        note: string
        decline: boolean
        expirationDate: string
      }
    },
    ctx: Context
  ) => {
    const {
      clients: { masterdata },
      vtex,
      vtex: { logger },
    } = ctx

    const { sessionData, storefrontPermissions } = vtex as any

    checkSession(sessionData)

    const email = sessionData.namespaces.profile.email.value
    const {
      permissions,
      role: { slug },
    } = storefrontPermissions

    const isCustomer = slug.includes('customer')
    const isSales = slug.includes('sales')
    const itemsChanged = items?.length > 0

    checkPermissionsForUpdateQuote({
      permissions,
      itemsChanged,
      decline,
    })

    const {
      organization: { value: userOrganizationId },
      costcenter: { value: userCostCenterId },
    } = sessionData.namespaces['storefront-permissions']

    const now = new Date()
    const nowISO = now.toISOString()

    try {
      const existingQuote: Quote = await masterdata.getDocument({
        dataEntity: QUOTE_DATA_ENTITY,
        fields: QUOTE_FIELDS,
        id,
      })

      checkQuoteStatus(existingQuote)

      const expirationChanged = expirationDate !== existingQuote.expirationDate

      checkOperationsForUpdateQuote({
        permissions,
        expirationChanged,
        itemsChanged,
        existingQuote,
        userCostCenterId,
        userOrganizationId,
        declineQuote: decline,
      })

      const readyOrRevised = itemsChanged ? 'ready' : 'revised'
      const status = decline ? 'declined' : readyOrRevised

      const lastUpdate = nowISO
      const update = {
        date: nowISO,
        email,
        note,
        role: slug,
        status,
      }

      const { updateHistory } = existingQuote

      updateHistory.push(update)

      const updatedQuote: Quote = {
        ...existingQuote,
        expirationDate: expirationChanged
          ? expirationDate
          : existingQuote.expirationDate,
        items: itemsChanged ? items : existingQuote.items,
        lastUpdate,
        status,
        subtotal: subtotal ?? existingQuote.subtotal,
        updateHistory,
        viewedByCustomer: !!(decline || isCustomer),
        viewedBySales: !!(decline || isSales),
      }

      const data = await masterdata
        .updateEntireDocument({
          dataEntity: QUOTE_DATA_ENTITY,
          fields: updatedQuote,
          id,
          schema: SCHEMA_VERSION,
        })
        .then((res: any) => res)

      const sellerQuotesController = new SellerQuotesController(ctx)

      if (existingQuote.parentQuote) {
        sellerQuotesController.handleParentQuoteSubtotalAndStatus(
          existingQuote.parentQuote
        )
      }

      const users = updateHistory.map((anUpdate) => anUpdate.email)
      const uniqueUsers = [
        ...new Set(
          users.filter((userEmail: string) => isEmail.test(userEmail))
        ),
      ]

      message(ctx)
        .quoteUpdated({
          costCenter: existingQuote.costCenter,
          id: existingQuote.id,
          lastUpdate: {
            email,
            note,
            status: status.toUpperCase(),
          },
          name: existingQuote.referenceName,
          organization: existingQuote.organization,
          users: uniqueUsers,
        })
        .then(() => {
          logger.info({
            message: `[Quote updated] E-mail sent ${uniqueUsers.join(', ')}`,
          })
        })

      return data.id
    } catch (error) {
      logger.warn({
        error,
        message: 'updateQuote-warning',
      })
      throw new GraphQLError(error)
    }
  },
  useQuote: async (
    _: any,
    { id, orderFormId }: { id: string; orderFormId: string },
    ctx: Context
  ) => {
    const {
      clients: { masterdata, hub },
      vtex,
      vtex: { account, logger, authToken },
    } = ctx

    const { sessionData, storefrontPermissions } = vtex as any

    checkSession(sessionData)

    const { permissions } = storefrontPermissions

    if (!permissions.includes('use-quotes')) {
      throw new GraphQLError('operation-not-permitted')
    }

    const useHeaders = {
      'Content-Type': 'application/json',
      Cookie: `VtexIdclientAutCookie=${authToken};`,
    }

    try {
      // GET QUOTE DATA
      const mainQuote: Quote = await masterdata.getDocument({
        dataEntity: QUOTE_DATA_ENTITY,
        fields: QUOTE_FIELDS,
        id,
      })

      const quotes: Quote[] = []
      const items: QuoteItem[] = []

      if (mainQuote.hasChildren) {
        const sellerQuotesController = new SellerQuotesController(ctx)
        const childrenQuotes = await sellerQuotesController.getAllChildrenQuotes(
          id
        )

        let errorsCount = 0

        for (const quote of childrenQuotes) {
          try {
            checkQuoteStatus(quote)
            quotes.push(quote)
          } catch (e) {
            if (++errorsCount === childrenQuotes.length) {
              throw e
            }

            continue
          }
        }
      } else {
        checkQuoteStatus(mainQuote)
        quotes.push(mainQuote)
      }

      for (const quote of quotes) {
        items.push(...quote.items)
      }

      const { salesChannel } = mainQuote

      // CLEAR CURRENT CART
      if (orderFormId !== 'default-order-form') {
        await hub.post(
          routes.clearCart(account, orderFormId),
          {
            expectedOrderFormSections: ['items'],
          },
          useHeaders
        )
      }

      // CREATE CART IF IT DOESN'T EXIST YET
      if (orderFormId === 'default-order-form') {
        const newOrderForm = await hub.get(
          routes.orderForm(account),
          useHeaders
        )

        orderFormId = (newOrderForm.data as any).orderFormId
      }

      await checkAndCreateQuotesConfig(ctx)
      await hub
        .put(
          routes.addCustomData({
            account,
            orderFormId,
            appId: APP_NAME,
            property: 'quoteId',
          }),
          {
            value: id,
          },
          useHeaders
        )
        .then((res: any) => {
          return res.data
        })
        .catch((error) =>
          logger.error({
            error,
            message: 'useQuote-addCustomDataError',
          })
        )

      const salesChannelQueryString = salesChannel ? `?sc=${salesChannel}` : ''

      // ADD ITEMS TO CART
      const data = await hub
        .post(
          `${routes.addToCart(account, orderFormId)}${salesChannelQueryString}`,
          {
            expectedOrderFormSections: ['items'],
            orderItems: items.map((item) => {
              return {
                id: item.id,
                quantity: item.quantity,
                seller: item.seller || '1',
              }
            }),
          }
        )
        .then((res: any) => {
          return res.data
        })

      const { items: itemsAdded } = data

      const sellingPriceMap = indexBy(
        prop('id'),
        map(
          (item: any) => ({
            id: item.id,
            price: item.sellingPrice,
          }),
          items
        )
      )

      const orderItems: any[] = []

      itemsAdded.forEach((item: any, key: number) => {
        orderItems.push({
          index: key,
          price: prop(item.id, sellingPriceMap).price,
          quantity: null,
        })
      })

      await hub.post(
        routes.addPriceToItems(account, orderFormId),
        {
          orderItems,
        },
        useHeaders
      )

      for (const quote of quotes) {
        const metricParams: UseQuoteMetricsParams = {
          quote,
          orderFormId,
          account,
          userEmail: sessionData?.namespaces?.profile?.email?.value,
        }

        sendUseQuoteMetric(ctx, metricParams)
      }
    } catch (error) {
      logger.error({
        error,
        message: 'useQuote-error',
      })
      if (error.message) {
        throw new GraphQLError(error.message)
      } else if (error.response?.data?.message) {
        throw new GraphQLError(error.response.data.message)
      } else {
        throw new GraphQLError(error)
      }
    }
  },
  saveAppSettings: async (
    _: void,
    {
      input: { cartLifeSpan, quotesManagedBy = 'MARKETPLACE' },
    }: { input: { cartLifeSpan: number; quotesManagedBy: string } },
    ctx: Context
  ) => {
    const {
      clients: { vbase },
      vtex: { logger },
    } = ctx

    let settings = null
    let noSettingsFound = false

    try {
      settings = await vbase.getJSON<Settings | null>(
        APP_NAME,
        'settings',
        true
      )
    } catch (error) {
      logger.error({
        error,
        message: 'saveAppSettings-getAppSettingsError',
      })

      return null
    }

    if (!settings) {
      settings = defaultSettings
      noSettingsFound = true
    }

    const newSettings = {
      ...settings,
      adminSetup: {
        ...settings.adminSetup,
        cartLifeSpan,
        quotesManagedBy,
      },
    }

    try {
      await vbase.saveJSON(APP_NAME, 'settings', newSettings)
    } catch (error) {
      logger.error({
        error,
        message: 'saveAppSettings-saveAppSettingsError',
      })

      return noSettingsFound ? null : settings
    }

    return newSettings
  },
}

```


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

[Assistant]
Here's the implementation:

```typescript
import {
  APP_NAME,
  B2B_USER_DATA_ENTITY,
  B2B_USER_SCHEMA_VERSION,
  QUOTE_DATA_ENTITY,
  QUOTE_FIELDS,
  SCHEMA_VERSION,
} from '../../constants'
import GraphQLError from '../../utils/GraphQLError'
import { checkConfig } from '../utils/checkConfig'
import SellerQuotesController from '../utils/sellerQuotesController'

// This function checks if given email is an user part of a buyer org.
export const isUserPartOfBuyerOrg = async (email: string, ctx: Context) => {
  const {
    clients: { masterdata },
  } = ctx

  const where = `email=${email}`
  const resp = await masterdata.searchDocumentsWithPaginationInfo({
    dataEntity: B2B_USER_DATA_ENTITY,
    fields: ['id'], // we don't need to fetch all fields, only if there is an entry or not
    pagination: {
      page: 1,
      pageSize: 1, // we only need to know if there is at least one user entry
    },
    schema: B2B_USER_SCHEMA_VERSION,
    ...(where ? { where } : {}),
  })

  const { data } = (resp as unknown) as {
    data: any
  }

  if (data.length > 0) {
    return true
  }

  return false
}

const buildWhereStatement = async ({
  permissions,
  organization,
  costCenter,
  status,
  search,
  userOrganizationId,
  userCostCenterId,
  userSalesChannel,
}: {
  permissions: string[]
  organization?: string[]
  costCenter?: string[]
  status?: string[]
  search?: string
  userOrganizationId: string
  userCostCenterId: string
  userSalesChannel?: string
}) => {
  // only the main quotes must be fetched
  const whereArray = ['(parentQuote is null)']

  // if user only has permission to access their organization's quotes,
  // hard-code that organization into the masterdata search
  if (!permissions.includes('access-quotes-all')) {
    whereArray.push(`organization=${userOrganizationId}`)
  } else if (organization?.length) {
    const orgArray = organization.map((org) => `organization=${org}`)
    const organizationsStatement = `(${orgArray.join(' OR ')})`

    whereArray.push(organizationsStatement)
  }

  // similarly, if user only has permission to see their cost center's quotes,
  // hard-code its ID into the search
  if (
    !permissions.includes('access-quotes-all') &&
    !permissions.includes('access-quotes-organization')
  ) {
    whereArray.push(`costCenter=${userCostCenterId}`)
  } else if (costCenter?.length) {
    const ccArray = costCenter.map((cc) => `costCenter=${cc}`)
    const costCenters = `(${ccArray.join(' OR ')})`

    whereArray.push(costCenters)
  }

  // similarly, if user only has permission to see their quotes from their sales channel,
  // hard-code its value into the search
  // allow all users to view quotes without a sales channel
  if (
    !permissions.includes('access-quotes-all') &&
    !permissions.includes('access-quotes-all-saleschannel') &&
    userSalesChannel
  ) {
    whereArray.push(
      `((salesChannel is null) OR salesChannel="${userSalesChannel}")`
    )
  }

  if (status?.length) {
    const statusArray = status.map((stat) => `status=${stat}`)
    const statuses = `(${statusArray.join(' OR ')})`

    whereArray.push(statuses)
  }

  if (search) {
    const searchArray = [] as string[]

    searchArray.push(`referenceName="*${search}*"`)
    searchArray.push(`creatorEmail="*${search}*"`)
    const searches = `(${searchArray.join(' OR ')})`

    whereArray.push(searches)
  }

  return whereArray.join(' AND ')
}

export const Query = {
  getQuote: async (_: any, { id }: { id: string }, ctx: Context) => {
    const {
      clients: { masterdata },
      vtex,
      vtex: { logger },
    } = ctx

    const { sessionData, storefrontPermissions, segmentData } = vtex as any

    if (
      !storefrontPermissions?.permissions?.length ||
      !sessionData?.namespaces['storefront-permissions']?.organization?.value ||
      !sessionData?.namespaces['storefront-permissions']?.costcenter?.value
    ) {
      return null
    }

    const { permissions } = storefrontPermissions
    const userOrganizationId =
      sessionData.namespaces['storefront-permissions'].organization.value

    const userCostCenterId =
      sessionData.namespaces['storefront-permissions'].costcenter.value

    const userSalesChannel = segmentData?.channel

    if (
      !permissions.some(
        (permission: string) => permission.indexOf('access-quotes') >= 0
      )
    ) {
      return null
    }

    await checkConfig(ctx)

    try {
      const quote: Quote = await masterdata.getDocument({
        dataEntity: QUOTE_DATA_ENTITY,
        fields: QUOTE_FIELDS,
        id,
      })

      // if user only has permission to view their organization's quotes, check that the org matches
      if (
        !permissions.includes('access-quotes-all') &&
        permissions.includes('access-quotes-organization') &&
        userOrganizationId !== quote.organization
      ) {
        return null
      }

      // if user only has permission to view their cost center's quotes, check that the cost center matches
      if (
        !permissions.includes('access-quotes-all') &&
        !permissions.includes('access-quotes-organization') &&
        userCostCenterId !== quote.costCenter
      ) {
        return null
      }

      // if user only has permission to view quotes from their sales channel, check that the sales channel matches or is null
      if (
        !permissions.includes('access-quotes-all') &&
        !permissions.includes('access-quotes-all-saleschannel') &&
        quote.salesChannel &&
        userSalesChannel !== quote.salesChannel
      ) {
        return null
      }

      return quote
    } catch (error) {
      logger.error({
        error,
        message: 'getQuote-error',
      })
      if (error.message) {
        throw new GraphQLError(error.message)
      } else if (error.response?.data?.message) {
        throw new GraphQLError(error.response.data.message)
      } else {
        throw new GraphQLError(error)
      }
    }
  },
  getQuotes: async (
    _: any,
    {
      organization,
      costCenter,
      status,
      search,
      page,
      pageSize,
      sortOrder,
      sortedBy,
    }: {
      organization: string[]
      costCenter: string[]
      status: string[]
      search: string
      page: number
      pageSize: number
      sortOrder: string
      sortedBy: string
    },
    ctx: Context
  ) => {
    const {
      clients: { masterdata },
      vtex,
      vtex: { logger },
    } = ctx

    const { sessionData, storefrontPermissions, segmentData } = vtex as any

    if (
      !storefrontPermissions?.permissions?.length ||
      !sessionData?.namespaces['storefront-permissions']?.organization?.value ||
      !sessionData?.namespaces['storefront-permissions']?.costcenter?.value
    ) {
      return null
    }

    const { permissions } = storefrontPermissions
    const userOrganizationId =
      sessionData.namespaces['storefront-permissions'].organization.value

    const userCostCenterId =
      sessionData.namespaces['storefront-permissions'].costcenter.value

    if (
      !permissions.some(
        (permission: string) => permission.indexOf('access-quotes') >= 0
      )
    ) {
      return null
    }

    const userSalesChannel = segmentData?.channel

    await checkConfig(ctx)

    const where = await buildWhereStatement({
      permissions,
      organization,
      costCenter,
      status,
      search,
      userOrganizationId,
      userCostCenterId,
      userSalesChannel,
    })

    try {
      return await masterdata.searchDocumentsWithPaginationInfo({
        dataEntity: QUOTE_DATA_ENTITY,
        fields: QUOTE_FIELDS,
        pagination: { page, pageSize },
        schema: SCHEMA_VERSION,
        sort: `${sortedBy} ${sortOrder}`,
        ...(where && { where }),
      })
    } catch (error) {
      logger.error({
        error,
        message: 'getQuotes-error',
      })
      throw new GraphQLError(error)
    }
  },
  getChildrenQuotes: async (
    _: any,
    {
      id,
      sortOrder,
      sortedBy,
    }: {
      id: string
      sortOrder: string
      sortedBy: string
    },
    ctx: Context
  ) => {
    const {
      vtex: { logger },
    } = ctx

    await checkConfig(ctx)
    const sellerQuotesController = new SellerQuotesController(ctx)

    try {
      return await sellerQuotesController.getAllChildrenQuotes(
        id,
        sortOrder,
        sortedBy
      )
    } catch (error) {
      logger.error({
        error,
        message: 'getQuotes-error',
      })
      throw new GraphQLError(error)
    }
  },
  getQuoteEnabledForUser: async (
    _: any,
    { email }: { email: string },
    ctx: Context
  ) => {
    const {
      vtex: { logger },
    } = ctx

    try {
      // if user is part of a buyer org, quote functionality is enabled
      return await isUserPartOfBuyerOrg(email, ctx)
    } catch (error) {
      logger.error({
        error,
        message: 'getQuoteEnabledForUser-error',
      })
      throw new GraphQLError(error)
    }
  },
  getAppSettings: async (_: void, __: void, ctx: Context) => {
    const {
      clients: { vbase },
      vtex: { logger },
    } = ctx

    await checkConfig(ctx)
    let settings = null

    try {
      settings = await vbase.getJSON<Settings | null>(
        APP_NAME,
        'settings',
        true
      )
    } catch (error) {
      logger.error({
        error,
        message: 'getAppSettings-getVbaseError',
      })

      return null
    }

    if (settings && !settings?.adminSetup.quotesManagedBy) {
      settings.adminSetup.quotesManagedBy = 'MARKETPLACE'
    }

    return settings
  },
  checkSellerQuotes: async (
    _: void,
    { sellers }: { sellers: string[] },
    ctx: Context
  ) => {
    // guarantee at least the marketplace seller to use your name if necessary
    const allSellers = sellers.filter((seller) => seller !== '1')

    allSellers.push('1')

    const verifiedSellers = await Promise.all(
      allSellers.map(async (seller) => {
        if (seller === '1') {
          return ctx.clients.seller.getSeller(seller)
        }

        const verifyResponse = await ctx.clients.sellerQuotes
          .verifyQuoteSettings(seller)
          .catch(() => null)

        if (verifyResponse?.receiveQuotes) {
          return ctx.clients.seller.getSeller(seller)
        }

        return null
      })
    )

    return verifiedSellers.filter(Boolean)
  },
}

```


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

[User] Show me the implementation of b2b-quotes-graphql/node/resolvers/utils/quotes.ts from the b2b-quotes-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
export async function splitItemsBySeller({
  ctx,
  items,
  quoteBySeller = {},
  index = 0,
}: {
  ctx: Context
  items: QuoteItem[]
  quoteBySeller?: SellerQuoteMap
  index?: number
}): Promise<SellerQuoteMap> {
  if (index >= items.length) return quoteBySeller

  const item = items[index]
  const { seller } = item

  const next = async () =>
    splitItemsBySeller({
      ctx,
      items,
      quoteBySeller,
      index: index + 1,
    })

  // The ternary check is to not request again from the same seller
  const verifyResponse = quoteBySeller[seller]
    ? { receiveQuotes: true }
    : await ctx.clients.sellerQuotes
        .verifyQuoteSettings(seller)
        .catch(() => null)

  if (!verifyResponse?.receiveQuotes) {
    await next()

    return quoteBySeller
  }

  if (!quoteBySeller[seller]) {
    const sellerData = await ctx.clients.seller.getSeller(seller)

    quoteBySeller[seller] = {
      items: [],
      subtotal: 0,
      seller,
      sellerName: sellerData?.name,
    }
  }

  quoteBySeller[seller].items.push(item)
  quoteBySeller[seller].subtotal += item.sellingPrice * item.quantity

  await next()

  return quoteBySeller
}

export function createItemComparator<T extends QuoteItem>(item: T) {
  return ({ id, seller }: T) => item.id === id && item.seller === seller
}

export const createQuoteObject = ({
  sessionData,
  storefrontPermissions,
  segmentData,
  settings,
  items,
  referenceName,
  subtotal,
  note,
  sendToSalesRep,
  seller,
  sellerName,
  parentQuote,
  hasChildren,
  sellerQuotesQuantity,
}: {
  sessionData: SessionData
  storefrontPermissions: { role: { slug: string } }
  segmentData?: { channel?: string }
  settings?: Settings | null
  items: QuoteItem[]
  referenceName: string
  subtotal: number
  note: string
  sendToSalesRep: boolean
  seller?: string
  sellerName?: string
  parentQuote?: string | null
  hasChildren?: boolean | null
  sellerQuotesQuantity?: number
}): Omit<Quote, 'id'> => {
  const { email, firstName, lastName } = sessionData.namespaces.profile
  const { value: creatorEmail } = email
  const creatorName = `${firstName?.value ?? ''}${
    lastName?.value ? ` ${lastName.value}` : ''
  }`.trim()

  const {
    role: { slug },
  } = storefrontPermissions

  const {
    organization: { value: organizationId },
    costcenter: { value: costCenterId },
  } = sessionData.namespaces['storefront-permissions']

  const now = new Date()
  const nowISO = now.toISOString()
  const expirationDate = new Date()

  expirationDate.setDate(
    expirationDate.getDate() + (settings?.adminSetup?.cartLifeSpan ?? 30)
  )
  const expirationDateISO = expirationDate.toISOString()

  const isSellerQuote = seller && seller !== '1'
  const status =
    sendToSalesRep || isSellerQuote || (!seller && sellerQuotesQuantity)
      ? 'pending'
      : 'ready'

  const lastUpdate = nowISO
  const updateHistory = [
    {
      date: nowISO,
      email: creatorEmail,
      note,
      role: slug,
      status,
    },
  ]

  const salesChannel: string = segmentData?.channel ?? ''

  return {
    costCenter: costCenterId,
    creationDate: nowISO,
    creatorEmail,
    creatorName,
    creatorRole: slug,
    expirationDate: expirationDateISO,
    items,
    lastUpdate,
    organization: organizationId,
    referenceName,
    status,
    subtotal,
    updateHistory,
    viewedByCustomer: !!sendToSalesRep,
    viewedBySales: !sendToSalesRep,
    salesChannel,
    seller,
    sellerName,
    parentQuote,
    hasChildren,
  }
}

```


[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 b2b-quotes-graphql/node/resolvers/utils/checkPermissions.ts from the b2b-quotes-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import GraphQLError from '../../utils/GraphQLError'

export const checkSession = (sessionData: any) => {
  if (!sessionData?.namespaces['storefront-permissions']) {
    throw new GraphQLError('organization-data-not-found')
  }

  if (!sessionData?.namespaces?.profile?.email?.value) {
    throw new GraphQLError('email-not-found')
  }
}

export const checkPermissionsForUpdateQuote = ({
  permissions,
  itemsChanged,
  decline,
}: {
  permissions: string[]
  itemsChanged: boolean
  decline: boolean
}) => {
  if (
    (itemsChanged &&
      !permissions.some((permission: string) =>
        permission.includes('edit-quotes')
      )) ||
    (!itemsChanged &&
      !permissions.some((permission: string) =>
        permission.includes('access-quotes')
      )) ||
    (decline && !permissions.includes('decline-quotes'))
  ) {
    throw new GraphQLError('operation-not-permitted')
  }
}

export const checkQuoteStatus = (existingQuote: Quote) => {
  if (!existingQuote) {
    throw new GraphQLError('quote-not-found')
  }

  if (
    existingQuote.status === 'expired' ||
    existingQuote.status === 'declined'
  ) {
    throw new GraphQLError('quote-cannot-be-updated-or-used')
  }
}

const checkIfOrgMatches = ({
  permissions,
  existingQuote,
  itemsOrExpirationChanged,
  neitherItemsNorExpirationChanged,
  userOrganizationId,
}: {
  permissions: string[]
  itemsOrExpirationChanged: boolean
  neitherItemsNorExpirationChanged: boolean
  existingQuote: Quote
  userOrganizationId: string
}) => {
  // if user only has permission to edit their organization's quotes, check that the org matches

  if (
    (itemsOrExpirationChanged &&
      !permissions.includes('edit-quotes-all') &&
      permissions.includes('edit-quotes-organization')) ||
    (neitherItemsNorExpirationChanged &&
      !permissions.includes('access-quotes-all') &&
      permissions.includes('access-quotes-organization'))
  ) {
    if (userOrganizationId !== existingQuote.organization) {
      throw new GraphQLError('operation-not-permitted')
    }
  }
}

const checkIfCostCenterMatches = ({
  permissions,
  existingQuote,
  itemsOrExpirationChanged,
  neitherItemsNorExpirationChanged,
  userCostCenterId,
}: {
  permissions: string[]
  itemsOrExpirationChanged: boolean
  neitherItemsNorExpirationChanged: boolean
  existingQuote: Quote
  userCostCenterId: string
}) => {
  // if user only has permission to edit their organization's quotes, check that the org matches

  if (
    (itemsOrExpirationChanged &&
      !permissions.includes('edit-quotes-all') &&
      !permissions.includes('edit-quotes-organization')) ||
    (neitherItemsNorExpirationChanged &&
      !permissions.includes('access-quotes-all') &&
      !permissions.includes('access-quotes-organization'))
  ) {
    if (userCostCenterId !== existingQuote.costCenter) {
      throw new GraphQLError('operation-not-permitted')
    }
  }
}

export const checkOperationsForUpdateQuote = ({
  permissions,
  expirationChanged,
  itemsChanged,
  existingQuote,
  userOrganizationId,
  userCostCenterId,
  declineQuote,
}: {
  permissions: string[]
  expirationChanged: boolean
  itemsChanged: boolean
  existingQuote: Quote
  userOrganizationId: string
  userCostCenterId: string
  declineQuote: boolean
}) => {
  const itemsOrExpirationChanged = itemsChanged || expirationChanged
  const neitherItemsNorExpirationChanged = !itemsChanged && !expirationChanged

  if (
    expirationChanged &&
    !declineQuote &&
    !permissions.some((permission: string) =>
      permission.includes('edit-quotes')
    )
  ) {
    throw new GraphQLError('operation-not-permitted')
  }

  checkIfOrgMatches({
    permissions,
    existingQuote,
    itemsOrExpirationChanged,
    neitherItemsNorExpirationChanged,
    userOrganizationId,
  })

  checkIfCostCenterMatches({
    permissions,
    existingQuote,
    itemsOrExpirationChanged,
    neitherItemsNorExpirationChanged,
    userCostCenterId,
  })

  // if user only has permission to edit their cost center's quotes, check that the cost center matches
}

```


[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 b2b-quotes-graphql/node/resolvers/utils/sellerQuotesController.ts from the b2b-quotes-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { NotFoundError } from '@vtex/api'
import pLimit from 'p-limit'

import {
  QUOTE_DATA_ENTITY,
  QUOTE_FIELDS,
  SCHEMA_VERSION,
} from '../../constants'
import {
  costCenterName as getCostCenterName,
  organizationName as getOrganizationName,
} from '../fieldResolvers'

type GetQuotesArgs = {
  page?: number
  pageSize?: number
  where?: string
  sort?: string
}

export default class SellerQuotesController {
  constructor(
    private readonly ctx: Context | EventBroadcastContext,
    private readonly seller?: string
  ) {}

  private async getSellerQuotes({
    page = 1,
    pageSize = 1,
    where = '',
    sort = '',
  }: GetQuotesArgs) {
    return this.ctx.clients.masterdata.searchDocumentsWithPaginationInfo<Quote>(
      {
        dataEntity: QUOTE_DATA_ENTITY,
        fields: QUOTE_FIELDS,
        schema: SCHEMA_VERSION,
        pagination: { page, pageSize },
        where: `seller=${this.seller} AND (${where})`,
        sort,
      }
    )
  }

  private async getSellerQuote(id: string) {
    const { data } = await this.getSellerQuotes({ where: `id=${id}` })
    const [quote] = data

    if (!quote) {
      throw new NotFoundError('seller-quote-not-found')
    }

    return quote
  }

  private async getOrganizationData(quote: Quote) {
    const [organizationName, costCenterName] = await Promise.all([
      getOrganizationName({ organization: quote.organization }, null, this.ctx),
      getCostCenterName({ costCenter: quote.costCenter }, null, this.ctx),
    ])

    return { organizationName, costCenterName }
  }

  public async getAllChildrenQuotes(
    parentQuote: string,
    sortOrder = 'DESC',
    sortedBy = 'lastUpdate'
  ) {
    const result: Quote[] = []

    const getQuotes = async (page = 1) => {
      const quotes = await this.ctx.clients.masterdata.searchDocuments<Quote>({
        dataEntity: QUOTE_DATA_ENTITY,
        schema: SCHEMA_VERSION,
        pagination: { page, pageSize: 100 },
        fields: QUOTE_FIELDS,
        where: `parentQuote=${parentQuote}`,
        sort: `${sortedBy} ${sortOrder}`,
      })

      if (quotes.length) {
        result.push(...quotes)

        await getQuotes(page + 1)
      }
    }

    await getQuotes()

    return result
  }

  public async getFullSellerQuote(id: string) {
    const quote = await this.getSellerQuote(id)
    const { organizationName, costCenterName } = await this.getOrganizationData(
      quote
    )

    return { ...quote, organizationName, costCenterName }
  }

  public async getSellerQuotesPaginated({
    page,
    pageSize,
    where,
    sort = 'lastUpdate DESC',
  }: {
    page: number
    pageSize: number
    where?: string
    sort?: string
  }) {
    const { data, pagination } = await this.getSellerQuotes({
      page,
      pageSize,
      where,
      sort,
    })

    const limit = pLimit(15)
    const enrichedQuotes = await Promise.all(
      data.map((quote) =>
        limit(async () => {
          const {
            organizationName,
            costCenterName,
          } = await this.getOrganizationData(quote)

          return { ...quote, organizationName, costCenterName }
        })
      )
    )

    return {
      data: enrichedQuotes,
      pagination,
    }
  }

  public async handleParentQuoteSubtotalAndStatus(parentQuote: string) {
    const childrenQuotes = await this.getAllChildrenQuotes(parentQuote)

    if (!childrenQuotes?.length) return

    const sumSubtotal = childrenQuotes.reduce(
      (acc, quote) => acc + quote.subtotal,
      0
    )

    const isEverySameStatus = childrenQuotes.every(
      (quote) => quote.status === childrenQuotes[0].status
    )

    const quotesExceptDeclined = childrenQuotes.filter(
      (quote) => quote.status !== 'declined'
    )

    const isEverySameStatusExceptDeclined =
      !!quotesExceptDeclined.length &&
      quotesExceptDeclined.every(
        (quote) => quote.status === quotesExceptDeclined[0].status
      )

    const isSomeRevised = childrenQuotes.some(
      (quote) => quote.status === 'revised'
    )

    const isSomePending = childrenQuotes.some(
      (quote) => quote.status === 'pending'
    )

    let status: string | undefined

    if (isEverySameStatus) {
      status = childrenQuotes[0].status
    } else if (isEverySameStatusExceptDeclined) {
      status = quotesExceptDeclined[0].status
    } else if (isSomePending) {
      status = 'pending'
    } else if (isSomeRevised) {
      status = 'revised'
    }

    const lastUpdate = new Date().toISOString()

    this.ctx.clients.masterdata.updatePartialDocument({
      dataEntity: QUOTE_DATA_ENTITY,
      schema: SCHEMA_VERSION,
      fields: { subtotal: sumSubtotal, status, lastUpdate },
      id: parentQuote,
    })
  }

  public async saveSellerQuote(id: string, fields: Partial<Quote>) {
    await this.ctx.clients.masterdata.updatePartialDocument({
      dataEntity: QUOTE_DATA_ENTITY,
      schema: SCHEMA_VERSION,
      fields,
      id,
    })

    const { parentQuote } = fields

    if (parentQuote) {
      this.handleParentQuoteSubtotalAndStatus(parentQuote)
    }
  }
}

```


[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 b2b-quotes-graphql/node/resolvers/utils/checkConfig.ts from the b2b-quotes-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import templates from '../../templates'
import { toHash } from '../../utils'
import {
  APP_NAME,
  CRON_EXPRESSION,
  QUOTE_DATA_ENTITY,
  SCHEMA_VERSION,
  routes,
  schema,
} from '../../constants'

export const defaultSettings: Settings = {
  adminSetup: {
    allowManualPrice: false,
    cartLifeSpan: 30,
    quotesManagedBy: 'MARKETPLACE',
    hasCron: false,
  },
  schemaVersion: '',
  schemaHash: null,
  templateHash: null,
}

const defaultHeaders = (authToken: string) => ({
  Accept: 'application/vnd.vtex.ds.v10+json',
  'Content-Type': 'application/json',
  'Proxy-Authorization': authToken,
  VtexIdclientAutCookie: authToken,
})

// checks and configure the OrderForm based on quoteId
export const checkAndCreateQuotesConfig = async (
  ctx: Context
): Promise<any> => {
  const {
    clients: { checkout, vbase },
    vtex: { logger, authToken },
  } = ctx

  let hasQuoteConfig = false

  const saveQuotesConfig = async (flag: boolean) => {
    return vbase
      .saveJSON(APP_NAME, 'quotes', { quote: flag })
      .then(() => true)
      .catch((error) =>
        logger.error({
          error,
          message: 'vBaseSaveJson-error',
        })
      )
  }

  try {
    hasQuoteConfig =
      (await vbase.getJSON<{ quote: boolean }>(APP_NAME, 'quotes'))?.quote ??
      false
  } catch (error) {
    const errStr = error.toString()

    if (errStr.match(/404/gm)) {
      await saveQuotesConfig(false)
    } else {
      logger.error({
        error,
        message: 'vBaseSaveGet-error',
      })
    }
  }

  if (!hasQuoteConfig) {
    const checkoutConfig: any = await checkout
      .getOrderFormConfiguration()
      .catch((error) => {
        logger.error({
          error,
          message: 'getOrderFormConfiguration-error',
        })
      })

    if (
      checkoutConfig?.apps.findIndex(
        (currApp: any) => currApp.id === APP_NAME
      ) === -1
    ) {
      checkoutConfig.apps.push({
        fields: ['quoteId'],
        id: APP_NAME,
        major: 1,
      })
      const setCheckoutConfig: any = await checkout
        .setOrderFormConfiguration(checkoutConfig, authToken)
        .then(() => true)
        .catch((error) => {
          logger.error({
            error,
            message: 'setOrderFormConfiguration-error',
          })

          return false
        })

      if (setCheckoutConfig) {
        await saveQuotesConfig(true)
      }
    } else {
      saveQuotesConfig(true)
    }
  }
}

const initializeCron = async (settings: Settings, ctx: Context) => {
  const {
    vtex: { account, workspace },
    clients: { scheduler },
  } = ctx

  const cronQueue = await scheduler
    .getQueue()
    .then((data: any) => {
      return data
    })
    .catch(() => {
      return null
    })

  if (cronQueue?.expression === CRON_EXPRESSION) return settings

  const time = new Date().getTime()
  const QueueSchedule = {
    id: 'b2b-quotes-graphql-queue-schedule',
    request: {
      body: null,
      headers: {
        'cache-control': 'no-cache',
        pragma: 'no-cache',
      },
      method: 'GET',
      uri: `https://${workspace}--${account}.myvtex.com/b2b-quotes-graphql/_v/0/process-queue?v=${time}`,
    },
    scheduler: {
      endDate: '2031-12-30T23:29:00',
      expression: CRON_EXPRESSION,
    },
  }

  await scheduler
    .createOrUpdate(QueueSchedule)
    .then(() => {
      if (!settings) {
        return
      }

      settings.adminSetup.hasCron = true
      settings.adminSetup.cronExpression = CRON_EXPRESSION
      settings.adminSetup.cronWorkspace = workspace
    })
    .catch((error) => {
      if (error.response.status !== 304) {
        settings.adminSetup.hasCron = false
      } else {
        settings.adminSetup.hasCron = true
        settings.adminSetup.cronExpression = CRON_EXPRESSION
        settings.adminSetup.cronWorkspace = workspace
      }
    })

  return settings
}

const initializeSchema = async (settings: Settings, ctx: Context) => {
  const {
    clients: { masterdata },
  } = ctx

  try {
    await masterdata.createOrUpdateSchema({
      dataEntity: QUOTE_DATA_ENTITY,
      schemaBody: schema,
      schemaName: SCHEMA_VERSION,
    })

    settings.schemaVersion = SCHEMA_VERSION
    settings.schemaHash = toHash(schema)
  } catch (error) {
    if (error.response.status >= 400) {
      settings.schemaVersion = ''
    } else {
      settings.schemaVersion = SCHEMA_VERSION
    }
  }

  return settings
}

const initializeManualPrice = async (settings: Settings, ctx: Context) => {
  const {
    vtex: { account, authToken, logger },
    clients: { hub },
  } = ctx

  try {
    const url = routes.checkoutConfig(account)
    const headers = defaultHeaders(authToken)

    const { data: checkoutConfig } = await hub.get(url, headers, schema)

    if (checkoutConfig.allowManualPrice !== true) {
      await hub.post(
        url,
        JSON.stringify({
          ...checkoutConfig,
          allowManualPrice: true,
        }),
        headers
      )
      settings.adminSetup.allowManualPrice = true
    }
  } catch (error) {
    settings.adminSetup.allowManualPrice = false
    logger.warn({
      error,
      message: 'initializeManualPrice-error',
    })
  }

  return settings
}

const initializeTemplates = async (
  settings: Settings,
  currTemplateHash: string,
  ctx: Context
) => {
  const {
    vtex: { logger },
    clients: { mail },
  } = ctx

  const updates: Array<Promise<any>> = []

  templates.forEach((template) => {
    updates.push(mail.publishTemplate(template))
  })

  await Promise.all(updates)
    .then(() => {
      settings.templateHash = currTemplateHash
    })
    .catch((error) => {
      logger.error({
        error,
        message: 'checkConfig-publishTemplateError',
      })
      throw new Error(error)
    })

  return settings
}

const checkInitializations = async ({
  settings,
  currTemplateHash,
  changed,
  ctx,
}: {
  settings: Settings
  currTemplateHash: string
  changed: boolean
  ctx: Context
}) => {
  const {
    vtex: { workspace },
  } = ctx

  if (
    !settings?.adminSetup?.hasCron ||
    settings?.adminSetup?.cronExpression !== CRON_EXPRESSION ||
    settings?.adminSetup?.cronWorkspace !== workspace
  ) {
    if (!settings?.adminSetup?.hasCron) {
      settings.adminSetup = {
        ...defaultSettings.adminSetup,
      }
    }

    const oldCronSetting = settings.adminSetup.hasCron

    settings = await initializeCron(settings, ctx)

    if (settings.adminSetup.hasCron !== oldCronSetting) {
      changed = true
    }
  }

  const currentSchemaHash = toHash(schema)

  if (
    settings?.schemaVersion !== SCHEMA_VERSION ||
    settings?.schemaHash !== currentSchemaHash
  ) {
    const oldSchemaVersion = settings?.schemaVersion
    const oldSchemaHash = settings?.schemaHash

    settings = await initializeSchema(settings, ctx)

    const mustUpdateSettings =
      settings.schemaVersion !== oldSchemaVersion ||
      settings.schemaHash !== oldSchemaHash

    if (mustUpdateSettings) {
      changed = true
    }
  }

  if (!settings?.adminSetup?.allowManualPrice) {
    const oldManualPriceSetting = settings?.adminSetup?.allowManualPrice

    settings = await initializeManualPrice(settings, ctx)

    if (settings.adminSetup.allowManualPrice !== oldManualPriceSetting) {
      changed = true
    }
  }

  if (settings?.templateHash !== currTemplateHash) {
    const oldTemplateHash = settings?.templateHash

    settings = await initializeTemplates(settings, currTemplateHash, ctx)

    if (settings.templateHash !== oldTemplateHash) {
      changed = true
    }
  }

  return { settings, changed }
}

export const checkConfig = async (ctx: Context) => {
  const {
    vtex: { logger },
    clients: { vbase },
  } = ctx

  let settings: Settings | null = null
  let changed = false

  const currTemplateHash = toHash(templates)

  try {
    settings = await vbase.getJSON<Settings | null>(APP_NAME, 'settings', true)
  } catch (error) {
    logger.error({
      error,
      message: 'checkConfig-getAppSettingsError',
    })

    return null
  }

  if (
    !settings?.adminSetup?.cartLifeSpan &&
    !settings?.adminSetup?.quotesManagedBy
  ) {
    settings = defaultSettings
    changed = true
  }

  const initializationResult = await checkInitializations({
    settings,
    currTemplateHash,
    changed,
    ctx,
  })

  settings = initializationResult.settings
  changed = initializationResult.changed

  if (changed) {
    await vbase.saveJSON(APP_NAME, 'settings', settings)
  }

  await checkAndCreateQuotesConfig(ctx)

  return settings
}

```


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

[Assistant]
Here's the implementation:

```typescript
import { method } from '@vtex/api'

import { getAppId } from '../../constants'
import { processQueue } from '../../utils/Queue'
import { getSellerQuote } from './seller/getSellerQuote'
import { getSellerQuotesPaginated } from './seller/getSellerQuotesPaginated'
import { saveSellerQuote } from './seller/saveSellerQuote'
import {
  setSellerResponseMetadata,
  validateSellerRequest,
} from './seller/utils'

function createSellerHandlers(
  mainHandler: (ctx: Context, next: NextFn) => Promise<void>
) {
  return [validateSellerRequest, mainHandler, setSellerResponseMetadata]
}

export const Routes = {
  host: async (ctx: Context) => {
    ctx.set('Content-Type', 'application/json')
    ctx.set('Cache-Control', 'no-cache, no-store')
    ctx.response.body = {
      host: ctx.vtex.host,
    }
  },
  queueHandler: async (ctx: Context) => {
    const date = new Date().toISOString()

    processQueue(ctx)
    ctx.set('Content-Type', 'application/json')
    ctx.set('Cache-Control', 'no-cache, no-store')
    ctx.response.body = { date, appId: getAppId() }
    ctx.response.status = 200
  },
  getSellerQuote: method({
    GET: createSellerHandlers(getSellerQuote),
  }),
  getSellerQuotesPaginated: method({
    GET: createSellerHandlers(getSellerQuotesPaginated),
  }),
  saveSellerQuote: method({
    POST: createSellerHandlers(saveSellerQuote),
  }),
}

```


[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 b2b-quotes-graphql/node/resolvers/routes/seller/utils.ts from the b2b-quotes-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { ForbiddenError } from '@vtex/api'

import SellerQuotesController from '../../utils/sellerQuotesController'

const USER_AGENT_REGEX = /^vtex\.b2b-seller-quotes@\d+\.\d+\.\d+$/

export function invalidParam(
  param?: string | string[]
): param is string[] | undefined {
  return !param || Array.isArray(param)
}

export async function validateSellerRequest(ctx: Context, next: NextFn) {
  const { seller } = ctx.vtex.route.params

  if (
    invalidParam(seller) ||
    seller !== ctx.headers['x-vtex-origin-account'] ||
    !ctx.headers['user-agent']?.match(USER_AGENT_REGEX)
  ) {
    throw new ForbiddenError('request-not-allowed')
  }

  ctx.vtex.sellerQuotesController = new SellerQuotesController(ctx, seller)

  await next()
}

export async function setSellerResponseMetadata(ctx: Context) {
  ctx.set('Access-Control-Allow-Origin', '*')
  ctx.set('Content-Type', 'application/json')
  ctx.status = 200
}

```


[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 b2b-quotes-graphql/node/resolvers/routes/seller/getSellerQuote.ts from the b2b-quotes-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { UserInputError } from '@vtex/api'

import { invalidParam } from './utils'

export async function getSellerQuote(ctx: Context, next: NextFn) {
  const { id } = ctx.vtex.route.params

  if (invalidParam(id)) {
    throw new UserInputError('get-seller-quote-invalid-params')
  }

  const quote = await ctx.vtex.sellerQuotesController?.getFullSellerQuote(id)

  ctx.body = quote

  await next()
}

```


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

[User] Show me the implementation of b2b-quotes-graphql/node/resolvers/routes/seller/getSellerQuotesPaginated.ts from the b2b-quotes-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { invalidParam } from './utils'

export async function getSellerQuotesPaginated(ctx: Context, next: NextFn) {
  const { query } = ctx
  const {
    page,
    pageSize,
    search,
    status,
    where: customWhere = '',
    sort,
  } = query

  const pageNumber = parseInt(Array.isArray(page) ? page[0] : page || '1', 10)
  const pageSizeNumber = parseInt(
    Array.isArray(pageSize) ? pageSize[0] : pageSize || '25',
    10
  )

  const filters: string[] = []

  if (!invalidParam(search)) {
    const searchTerm = `*${search.replace(/'/g, '').split(/\s+/).join('*')}*`

    filters.push(
      `(referenceName='${searchTerm}' OR creatorEmail='${searchTerm}' OR creatorName='${searchTerm}')`
    )
  }

  if (!invalidParam(status)) {
    filters.push(`(status=${status})`)
  }

  if (!invalidParam(customWhere)) {
    filters.push(`(${customWhere})`)
  }

  const where = filters.join(' AND ')
  const validPage = pageNumber >= 0 ? pageNumber : 1
  const validPageSize = pageSizeNumber >= 0 ? pageSizeNumber : 25

  ctx.body = await ctx.vtex.sellerQuotesController?.getSellerQuotesPaginated({
    page: validPage,
    pageSize: validPageSize,
    where,
    ...(!invalidParam(sort) && { sort }),
  })

  await next()
}

```


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

[User] Show me the implementation of b2b-quotes-graphql/node/resolvers/routes/seller/saveSellerQuote.ts from the b2b-quotes-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { UserInputError } from '@vtex/api'
import { json } from 'co-body'

import { invalidParam } from './utils'

function throwsInputError(): never {
  throw new UserInputError('save-seller-quote-invalid-params')
}

export async function saveSellerQuote(ctx: Context, next: NextFn) {
  const { id } = ctx.vtex.route.params

  if (invalidParam(id)) {
    throwsInputError()
  }

  const payload: Partial<Quote> = await json(ctx.req)

  if (!payload) {
    throwsInputError()
  }

  await ctx.vtex.sellerQuotesController?.saveSellerQuote(id, payload)

  await next()
}

```


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

[User] Show me the implementation of product-quantity/messages/pt.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "Quantidade",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# disponível} other {# disponíveis}}",
  "admin/editor.product-quantity.title": "Componente da quantidade do produto",
  "admin/editor.product-quantity.description": "Componente que permite aos usuários selecionar o número de itens a serem adicionados ao carrinho",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "Limite para aviso de quantidade disponível",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "Exibir apenas a quantidade de itens restantes em estoque se a quantidade disponível for menor ou igual ao valor informado nesta propriedade",
  "admin/editor.product-quantity.showUnit.title": "Unidade de medida no seletor de quantidade",
  "admin/editor.product-quantity.showUnit.description": "Mostrar ou ocultar a unidade de medida no seletor de quantidade"
}

```


[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 product-quantity/messages/en.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "Quantity",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# available} other {# available}}",
  "admin/editor.product-quantity.title": "Product quantity component",
  "admin/editor.product-quantity.description": "A component that allows users to select the number of items to add to cart",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "Threshold for available quantity warning",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "Only show the quantity of remaining items in stock if available item quantity is less than or equal to the value passed in this property",
  "admin/editor.product-quantity.showUnit.title": "Unit of measure in quantity selector",
  "admin/editor.product-quantity.showUnit.description": "Show or hide unit of measure in quantity selector"
}

```


[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 product-quantity/messages/fr.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "Quantité",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# disponible} other {# disponibles}}",
  "admin/editor.product-quantity.title": "Composant de la quantité de produit",
  "admin/editor.product-quantity.description": "Un composant qui permet aux utilisateurs de sélectionner le nombre d’articles à ajouter au panier",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "Seuil d'alerte pour la quantité disponible",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "N'afficher la quantité d'articles restants en stock que si la quantité d'articles disponibles est inférieure ou égale à la valeur indiquée dans cette propriété",
  "admin/editor.product-quantity.showUnit.title": "Unité de mesure dans le sélecteur de quantité",
  "admin/editor.product-quantity.showUnit.description": "Afficher ou masquer l'unité de mesure dans le sélecteur de quantité"
}

```


[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 product-quantity/messages/context.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "Quantity",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# available} other {# available}}",
  "admin/editor.product-quantity.title": "Title of Product Quantity component",
  "admin/editor.product-quantity.description": "Description of Product Quantity component",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "Title of warningQuantityThreshold property",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "Description of warningQuantityThreshold property",
  "admin/editor.product-quantity.showUnit.title": "Title of showUnit property",
  "admin/editor.product-quantity.showUnit.description": "Description of showUnit property"
}

```


[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 product-quantity/messages/id.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "Jumlah",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# tersedia} other {# tersedia}}",
  "admin/editor.product-quantity.title": "Komponen Jumlah Produk",
  "admin/editor.product-quantity.description": "Komponen yang memungkinkan pengguna untuk memilih jumlah item untuk ditambahkan ke troli",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "Peringatan ambang batas jumlah yang tersedia",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "Hanya tampilkan jumlah item yang stoknya tersedia jika jumlah item yang tersedia kurang atau sama dengan nilai yang diteruskan dalam properti ini",
  "admin/editor.product-quantity.showUnit.title": "Satuan ukuran dalam pemilih jumlah",
  "admin/editor.product-quantity.showUnit.description": "Tampilkan atau sembunyikan satuan ukuran dalam pemilih jumlah"
}

```


[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 product-quantity/messages/no.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "Antall",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# tilgjengelig} other {# tilgjengelig}}",
  "admin/editor.product-quantity.title": "Komponent for produktmengde",
  "admin/editor.product-quantity.description": "En komponent som lar brukeren velge antall varer å legge i handlekurven",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "Terskeladvarsel for tilgjengelig mengde",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "Vis kun antall gjenværende varer på lager dersom tilgjengelige antall er mindre enn eller lik verdien som passeres i denne egenskapen",
  "admin/editor.product-quantity.showUnit.title": "Måleenhet i mengdevelgeren",
  "admin/editor.product-quantity.showUnit.description": "Vis eller skjul måleenhet i mengdevelgeren"
}

```


[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 product-quantity/messages/th.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "จำนวน",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# พร้อมจำหน่่าย} other {# พร้อมจำหน่่าย}}",
  "admin/editor.product-quantity.title": "คอมโพเนนต์ของจำนวนผลิตภัณฑ์",
  "admin/editor.product-quantity.description": "คอมโพเนนต์ที่ทำให้ผู้ใช้สามารถเลือกจำนวนสินค้าเพื่อเพิ่มลงในรถเข็น",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "เกณฑ์การเตือนจำนวนสินค้าพร้อมจำหน่าย",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "แสดงเฉพาะจำนวนสินค้าคงเหลือในคลังสินค้า หากจำนวนสินค้าพร้อมจำหน่ายน้อยกว่าหรือเท่ากับค่าที่ส่งผ่านในคุณสมบัตินี้",
  "admin/editor.product-quantity.showUnit.title": "หน่วยวัดในตัวเลือกจำนวน",
  "admin/editor.product-quantity.showUnit.description": "แสดงหรือซ่อนหน่วยวัดในตัวเลือกจำนวน"
}

```


[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 product-quantity/messages/nn.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "Antall",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# tilgjengelig} other {# tilgjengelig}}",
  "admin/editor.product-quantity.title": "Komponent for produktmengde",
  "admin/editor.product-quantity.description": "En komponent som lar brukeren velge antall varer å legge i handlekurven",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "Terskeladvarsel for tilgjengelig mengde",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "Vis kun antall gjenværende varer på lager dersom tilgjengelige antall er mindre enn eller lik verdien som passeres i denne egenskapen",
  "admin/editor.product-quantity.showUnit.title": "Måleenhet i mengdevelgeren",
  "admin/editor.product-quantity.showUnit.description": "Vis eller skjul måleenhet i mengdevelgeren"
}

```


[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 product-quantity/messages/es.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "Cantidad",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# disponible} other {# disponibles}}",
  "admin/editor.product-quantity.title": "Componente de cantidad de producto",
  "admin/editor.product-quantity.description": "Componente que permite a los usuarios seleccionar el número de ítems a agregar al carrito",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "Límite para aviso de cantidad disponible",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "Mostrar la cantidad de ítems restantes en stock solo si la cantidad disponible es menor o igual al valor definido en esta propiedad",
  "admin/editor.product-quantity.showUnit.title": "Unidad de medida en el selector de cantidad",
  "admin/editor.product-quantity.showUnit.description": "Mostrar u ocultar la unidad de medida en el selector de cantidad"
}

```


[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 product-quantity/messages/ar.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "store/product-quantity.quantity": "الكمية",
  "store/product-quantity.quantity-available": "{availableQuantity, plural, one {# متاح} other {# متاحة}}",
  "admin/editor.product-quantity.title": "مكون كمية المنتج",
  "admin/editor.product-quantity.description": "مكون يسمح للمستخدم بتحديد كمية العناصر لإضافتها إلى سلة التسوق",
  "admin/editor.product-quantity.warningQuantityThreshold.title": "تحذير بعتبة الكمية المتاحة",
  "admin/editor.product-quantity.warningQuantityThreshold.description": "اعرض فقط كمية العناصر المتبقية في المخزون إذا كانت الكمية المتاحة للعنصر أقل من القيمة التي تم تمريرها في هذه المؤسسة أو تساويها",
  "admin/editor.product-quantity.showUnit.title": "وحدة القياس في منتقي الكمية",
  "admin/editor.product-quantity.showUnit.description": "إظهار أو إخفاء وحدة القياس في منتقي الكمية"
}

```


[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 product-quantity/docs/README.md from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```markdown
📢 Don't fork this project. Use, [contribute](https://github.com/vtex-apps/product-quantity), or open issues through [Store Discussion](https://github.com/vtex-apps/store-discussion).

# Product Quantity

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->

The Product Quantity allows users to a add a chosen amount of the displayed product in their cart.

![product-quantity](https://user-images.githubusercontent.com/52087100/70237475-0f4bd900-1746-11ea-9af2-38f794f4a3dd.png)

## Configuration

1. Add the Product Quantity app to your dependencies in the theme's `manifest.json` file:

```json
  "dependencies": {
    "vtex.product-quantity": "1.x"
  }
```

You are now able to use all blocks that are exported by the Product Quantity app. Check out the full list below:

| Block name | Description |
| --------- | ------------ |
| `product-quantity` | Displays a quantity selector on the product details page. This block must be declared in the theme's `store.product` page template. |
| `product-summary-quantity` | Displays a quantity selector on [Product Summary](https://vtex.io/docs/components/all/vtex.product-summary/)'s blocks. This block must be declared as a children of the `product-summary.shelf` block. |

2. According to your desired scenario, add the `product-quantity`/`product-summary-quantity` blocks to your theme. For example:

```diff
  "flex-layout.col#product-price": {
    "props": {
      "preventVerticalStretch": true,
      "rowGap": 0
    },
    "children": [
      "product-name",
      "product-price#product-details",
      "product-separator",
+     "product-quantity",
      "sku-selector",
      "flex-layout.row#buy-button",
      "availability-subscriber",
      "shipping-simulator",
      "share"
    ]
  },
  "product-quantity": {
    "props": {
      "warningQuantityThreshold": 9999999,
      "showUnit": true
    }
  },
```

*In the example above a Product Details Page is built using Flex Layout and the `product-quantity` block.*

### `product-quantity` and `product-summary-quantity` props

| Prop name | Type | Description | Default Value |
| --- | --- | --- | --- |
| `warningQuantityThreshold` | `number` | Displays the quantity of remaining items in stock if the available quantity is less than or equal to the value given to this property. | `0` (the quantity is not displayed) |
| `showUnit` | `boolean` |Whether the unit of measurement should be displayed (`true`) or not (`false`). | `true` |
| `size` | `enum`| Preset size values for `font-size` and `padding`. You can check these value measures by accessing the [VTEX Styleguide](https://styleguide.vtex.com/#/Components/Forms/NumericStepper). Possible values are: `small`, `regular`, and `large`. | `small` |
| `showLabel` | `boolean` | Whether a label should be displayed (`true`) or not (`false`). | `true` |
| `selectorType` | `enum` | Defines how the quantity selector should initially behave. Possible values are: `stepper` (displays an input field where the quantity can be directly defined, in addition to side buttons to increase or decrease the value) and `dropdown` (shows a list of predefined-quantity options. In case the last quantity option is selected by users, the component is replaced with an input). | `stepper` |
| `quantitySelectorStep` | `enum` | Defines how the number of products that have unitMultiplier will works. Possible values are: `singleUnit` (the quantity will be not affected with the unitMultiplier) and `unitMultiplier` (the quantity will be affected with the unitMultiplier). | `unitMultiplier` |

## Customization

In order to apply CSS customizations in this and other blocks, follow the instructions given in the recipe on [Using CSS Handles for store customization](https://vtex.io/docs/recipes/style/using-css-handles-for-store-customization).

| CSS Handles                                |
| ------------------------------------------ |
| `availableQuantityContainer`               |
| `quantitySelectorContainer`                |
| `quantitySelectorDropdownContainer`        |
| `quantitySelectorDropdownMobileContainer`  |
| `quantitySelectorInputContainer`           |
| `quantitySelectorInputMobileContainer`     |
| `quantitySelectorStepper`                  |
| `quantitySelectorTitle`                    |
| `summaryContainer`                         |

<!-- DOCS-IGNORE:start -->

## Contributors ✨

Thanks goes to these wonderful people:

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
  <tr>
    <td align="center"><a href="https://github.com/regis-samurai"><img src="https://avatars0.githubusercontent.com/u/38638226?v=4" width="100px;" alt=""/><br /><sub><b>Reginaldo</b></sub></a><br /><a href="https://github.com/vtex-apps/product-quantity/commits?author=regis-samurai" title="Code">💻</a></td>
  </tr>
</table>

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome!

<!-- DOCS-IGNORE:end -->

```


[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 product-quantity/react/ProductSummaryQuantity.tsx from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { useCssHandles } from 'vtex.css-handles'
import useProduct from 'vtex.product-context/useProduct'
import { useProductDispatch } from 'vtex.product-context/ProductDispatchContext'

import BaseProductQuantity, {
  BaseProps,
} from './components/BaseProductQuantity'

const CSS_HANDLES = ['summaryContainer'] as const

const ProductSummaryQuantity: StorefrontFunctionComponent<BaseProps> = props => {
  const {
    warningQuantityThreshold,
    showLabel,
    size,
    selectorType,
    showUnit,
    quantitySelectorStep,
  } = props
  const handles = useCssHandles(CSS_HANDLES)
  const { selectedItem, selectedQuantity } = useProduct()
  const dispatch = useProductDispatch()

  const handleClick: React.MouseEventHandler<HTMLDivElement> = e => {
    e.preventDefault()
    // Stop propagation so it doesn't trigger the Link component above
    e.stopPropagation()
  }

  return (
    // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
    <div
      onClick={handleClick}
      className={`${handles.summaryContainer} center mw-100`}>
      <BaseProductQuantity
        showUnit={showUnit}
        size={size}
        dispatch={dispatch}
        showLabel={showLabel}
        selectedItem={selectedItem}
        selectedQuantity={selectedQuantity}
        selectorType={selectorType}
        warningQuantityThreshold={warningQuantityThreshold}
        quantitySelectorStep={quantitySelectorStep}
      />
    </div>
  )
}

ProductSummaryQuantity.schema = {
  title: 'admin/editor.product-quantity.title',
  description: 'admin/editor.product-quantity.description',
}

export default ProductSummaryQuantity

```


[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 product-quantity/react/package.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "scripts": {
    "test": "vtex-test-tools test",
    "lint": "tsc --noEmit && eslint --ext ts ."
  },
  "dependencies": {
    "@vtex/css-handles": "^1.0.0",
    "classnames": "^2.2.6",
    "ramda": "^0.26.1",
    "react": "^16.8.3",
    "react-intl": "^2.7.2",
    "react-dom": "^16.8.3"
  },
  "devDependencies": {
    "@types/classnames": "^2.2.7",
    "@types/graphql": "^14.2.0",
    "@types/jest": "^24.0.11",
    "@types/node": "^11.13.0",
    "@types/prop-types": "^15.7.0",
    "@types/ramda": "^0.26.5",
    "@types/react": "^16.8.10",
    "@types/react-intl": "^2.3.17",
    "@vtex/test-tools": "^0.2.0",
    "apollo-client": "^2.5.1",
    "typescript": "3.9.7",
    "vtex.css-handles": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.css-handles@0.4.4/public/@types/vtex.css-handles",
    "vtex.device-detector": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.device-detector@0.2.5/public/@types/vtex.device-detector",
    "vtex.native-types": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.native-types@0.7.1/public/@types/vtex.native-types",
    "vtex.product-context": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-context@0.9.2/public/@types/vtex.product-context",
    "vtex.product-summary-context": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-summary-context@0.5.0/public/@types/vtex.product-summary-context",
    "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.122.2/public/@types/vtex.render-runtime",
    "vtex.styleguide": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.styleguide@9.133.0/public/@types/vtex.styleguide"
  },
  "version": "1.11.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 product-quantity/react/ProductQuantity.tsx from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import useProduct from 'vtex.product-context/useProduct'
import { useProductDispatch } from 'vtex.product-context/ProductDispatchContext'

import BaseProductQuantity, {
  BaseProps,
} from './components/BaseProductQuantity'

const ProductQuantity: StorefrontFunctionComponent<BaseProps> = props => {
  const {
    warningQuantityThreshold,
    showLabel,
    size,
    selectorType,
    showUnit,
    quantitySelectorStep,
  } = props
  const { selectedItem, selectedQuantity } = useProduct()
  const dispatch = useProductDispatch()

  return (
    <BaseProductQuantity
      showUnit={showUnit}
      size={size}
      dispatch={dispatch}
      showLabel={showLabel}
      selectedItem={selectedItem}
      selectedQuantity={selectedQuantity}
      selectorType={selectorType}
      warningQuantityThreshold={warningQuantityThreshold}
      quantitySelectorStep={quantitySelectorStep}
    />
  )
}

ProductQuantity.schema = {
  title: 'admin/editor.product-quantity.title',
  description: 'admin/editor.product-quantity.description',
}

export default ProductQuantity

```


[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 product-quantity/store/contentSchemas.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "definitions": {
    "ProductQuantity": {
      "type": "object",
      "properties": {
        "warningQuantityThreshold": {
          "title": "admin/editor.product-quantity.warningQuantityThreshold.title",
          "description": "admin/editor.product-quantity.warningQuantityThreshold.title",
          "type": "number",
          "default": 0
        },
        "showUnit": {
          "title": "admin/editor.product-quantity.showUnit.title",
          "description": "admin/editor.product-quantity.showUnit.title",
          "type": "boolean",
          "default": true
        }
      }
    },
    "ProductSummaryQuantity": {
      "type": "object",
      "properties": {
        "warningQuantityThreshold": {
          "title": "admin/editor.product-quantity.warningQuantityThreshold.title",
          "description": "admin/editor.product-quantity.warningQuantityThreshold.title",
          "type": "number",
          "default": 0
        },
        "showUnit": {
          "title": "admin/editor.product-quantity.showUnit.title",
          "description": "admin/editor.product-quantity.showUnit.description",
          "type": "boolean",
          "default": true
        }
      }
    }
  }
}

```


[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 product-quantity/store/interfaces.json from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "product-quantity": {
    "component": "ProductQuantity",
    "content": {
      "$ref": "#/definitions/ProductQuantity"
    }
  },
  "product-summary-quantity": {
    "component": "ProductSummaryQuantity",
    "content": {
      "$ref": "#/definitions/ProductSummaryQuantity"
    }
  }
}

```


[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 product-quantity/react/typings/storefront.d.ts from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { FunctionComponent } from 'react'

declare global {
  interface StorefrontFunctionComponent<P = {}> extends FunctionComponent<P> {
    getSchema?(props: P): object
    schema?: object
  }

  interface StorefrontComponent<P = {}, S = {}> extends Component<P, S> {
    getSchema?(props: P): object
    schema: object
  }
}

```


[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 product-quantity/react/typings/vtex.css-handles.d.ts from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module 'vtex.css-handles' {
  type CssHandlesInput = readonly string[]
  type ValueOf<T extends readonly any[]> = T[number]
  type CssHandles<T extends CssHandlesInput> = Record<ValueOf<T>, string>
  interface CssHandlesOptions {
    migrationFrom?: string | string[]
  }

  export const useCssHandles: <T extends CssHandlesInput>(
    handles: T,
    options: CssHandlesOptions = {}
  ) => CssHandles<T>
  export const applyModifiers: (
    handles: string,
    modifier: string | string[]
  ) => 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 product-quantity/react/typings/vtex.product-context.d.ts from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module 'vtex.product-context/useProduct' {
  const useProduct: () => ProductContext
  export default useProduct
}

declare module 'vtex.product-context' {
  export interface ProductContext {
    selectedQuantity: number
    selectedItem?: SelectedItem
  }

  interface SelectedItem {
    itemId: string
    unitMultiplier: number
    measurementUnit: string
    sellers: Array<{
      commertialOffer: {
        AvailableQuantity: number
      }
      sellerDefault: boolean
    }>
  }
}

declare module 'vtex.product-context/ProductDispatchContext' {
  type DispatchFunction = (payload: { type: string; args?: object }) => void
  export const useProductDispatch: () => DispatchFunction
}

```


[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 product-quantity/react/typings/css.d.ts from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module '*.css' {
  const css: any
  export default css
}

```


[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 product-quantity/react/typings/vtex.product-summary-context.d.ts from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module 'vtex.product-summary-context/ProductSummaryContext' {
  export const useProductSummary: () => ProductSummaryContext
  interface ProductSummaryContext {
    selectedQuantity: number
    selectedItem?: SelectedItem
  }

  interface SelectedItem {
    unitMultiplier: number
    measurementUnit: string
    sellers: Array<{
      commertialOffer: {
        AvailableQuantity: number
      }
    }>
  }

  type DispatchFunction = (payload: { type: string; args?: object }) => void
  export const useProductSummaryDispatch: () => DispatchFunction
}

```


[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 product-quantity/react/typings/graphql.d.ts from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module '*.graphql' {
  import { DocumentNode } from 'graphql'

  const value: DocumentNode
  export default value
}

```


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

[User] Show me the implementation of product-quantity/react/typings/vtex.styleguide.d.ts from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```typescript
declare module 'vtex.styleguide' {
  import { ComponentType } from 'react'

  export const NumericStepper: ComponentType<NumericStepperProps>
  export const Dropdown: ComponentType<DropdownProps>
  export const Input: ComponentType<InputProps>

  type NumericSize = 'small' | 'regular' | 'large'

  interface NumericStepperProps {
    size: NumericSize
    value: number
    minValue: number
    maxValue?: number
    unitMultiplier: number
    suffix?: string
    onChange: (e: any) => void
    useSemanticHtml?: any
  }

  interface DropdownProps {
    id: string
    testId: string
    onChange: (e: any) => void
    placeholder: string
    options: Array<{
      value: number
      label: string
    }>
    size: NumericSize
    value: number
  }

  interface InputProps {
    id: string
    size: NumericSize
    value: string
    maxLength: number
    onChange: (e: any) => void
    onBlur: () => void
    placeholder: 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 product-quantity/react/components/DropdownProductQuantity.tsx from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FunctionComponent, useState } from 'react'
import { Dropdown, Input } from 'vtex.styleguide'
import { useCssHandles } from 'vtex.css-handles'
import { SelectedItem } from 'vtex.product-context'
import { useDevice } from 'vtex.device-detector'

import { OnChangeCallback, BaseProps } from './BaseProductQuantity'

const MAX_DROPDOWN_VALUE = 10
const MAX_INPUT_LENGTH = 5

const DESKTOP_DROPDOWN_ID = 'quantity-dropdown'
const MOBILE_DROPDOWN_ID = 'quantity-dropdown-mobile'
const DESKTOP_INPUT_ID = 'quantity-input'
const MOBILE_INPUT_ID = 'quantity-input-mobile'

interface DropdownProps {
  itemId: SelectedItem['itemId']
  selectedQuantity: BaseProps['selectedQuantity']
  availableQuantity: number
  onChange: (e: OnChangeCallback) => void
  size: BaseProps['size']
}

type InternalBehavior = 'dropdown' | 'input'

const normalizeValue = (value: number, maxValue: number) =>
  value > maxValue ? maxValue : value

const validateValue = (value: string, maxValue: number) => {
  const parsedValue = parseInt(value, 10)

  if (Number.isNaN(parsedValue)) {
    return 1
  }

  return normalizeValue(parseInt(value, 10), maxValue)
}

const validateDisplayValue = (value: string, maxValue: number) => {
  const parsedValue = parseInt(value, 10)

  if (Number.isNaN(parsedValue) || parsedValue < 1) {
    return ''
  }

  return `${normalizeValue(parsedValue, maxValue)}`
}

const getDropdownOptions = (maxValue: number) => {
  const limit = Math.min(MAX_DROPDOWN_VALUE - 1, maxValue)
  const options = []

  for (let idx = 1; idx <= limit; ++idx) {
    options.push({ value: idx, label: `${idx}` })
  }

  if (maxValue >= MAX_DROPDOWN_VALUE) {
    options.push({
      value: MAX_DROPDOWN_VALUE,
      label: `${MAX_DROPDOWN_VALUE}+`,
    })
  }

  return options
}

const CSS_HANDLES = [
  'quantitySelectorDropdownMobileContainer',
  'quantitySelectorDropdownContainer',
  'quantitySelectorInputMobileContainer',
  'quantitySelectorInputContainer',
] as const

const DropdownProductQuantity: FunctionComponent<DropdownProps> = ({
  itemId,
  selectedQuantity,
  size = 'small',
  onChange,
  availableQuantity,
}) => {
  const [internalBehavior, setInternalBehavior] = useState<InternalBehavior>(
    'dropdown'
  )
  const [displayValue, setDisplayValue] = useState(`${selectedQuantity}`)
  const { isMobile } = useDevice()
  const handles = useCssHandles(CSS_HANDLES)
  const dropdownOptions = getDropdownOptions(availableQuantity)

  const handleChange = (value: string) => {
    const newValidatedValue = validateValue(value, availableQuantity)
    const newDisplayValue = validateDisplayValue(value, availableQuantity)

    if (
      internalBehavior === 'dropdown' &&
      newValidatedValue >= Math.min(availableQuantity + 1, MAX_DROPDOWN_VALUE)
    ) {
      setInternalBehavior('input')
    }

    setDisplayValue(newDisplayValue)
    onChange({ value: newValidatedValue })
  }

  const handleInputBlur = () => {
    if (displayValue === '') {
      setDisplayValue('1')
    }

    const validatedValue = validateValue(displayValue, availableQuantity)

    if (validatedValue < Math.min(availableQuantity, MAX_DROPDOWN_VALUE)) {
      setInternalBehavior('dropdown')
    }

    onChange({ value: validatedValue })
  }

  if (internalBehavior === 'dropdown') {
    return (
      <div
        className={
          isMobile
            ? handles.quantitySelectorDropdownMobileContainer
            : handles.quantitySelectorDropdownContainer
        }>
        <Dropdown
          id={`${
            isMobile ? MOBILE_DROPDOWN_ID : DESKTOP_DROPDOWN_ID
          }-${itemId}`}
          testId={`${
            isMobile ? MOBILE_DROPDOWN_ID : DESKTOP_DROPDOWN_ID
          }-${itemId}`}
          options={dropdownOptions}
          size={size}
          value={selectedQuantity}
          onChange={event => handleChange(event.target.value)}
          placeholder=" "
        />
      </div>
    )
  }

  return (
    <div
      className={
        isMobile
          ? handles.quantitySelectorInputMobileContainer
          : handles.quantitySelectorInputContainer
      }>
      <Input
        id={`${isMobile ? MOBILE_INPUT_ID : DESKTOP_INPUT_ID}-${itemId}`}
        size={size}
        value={displayValue}
        maxLength={MAX_INPUT_LENGTH}
        onChange={event => handleChange(event.target.value)}
        onBlur={handleInputBlur}
        placeholder=""
      />
    </div>
  )
}

export default DropdownProductQuantity

```


[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 product-quantity/react/components/BaseProductQuantity.tsx from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { useCallback } from 'react'
import { FormattedMessage } from 'react-intl'
import { useCssHandles } from 'vtex.css-handles'
import { useRuntime } from 'vtex.render-runtime'
import { DispatchFunction } from 'vtex.product-context/ProductDispatchContext'
import { ProductContext } from 'vtex.product-context'

import DropdownProductQuantity from './DropdownProductQuantity'
import StepperProductQuantity from './StepperProductQuantity'

export type NumericSize = 'small' | 'regular' | 'large'
export type SelectorType = 'stepper' | 'dropdown'
export type QuantitySelectorStepType = 'unitMultiplier' | 'singleUnit'

export interface BaseProps {
  dispatch: DispatchFunction
  selectedItem?: ProductContext['selectedItem']
  showLabel?: boolean
  selectedQuantity: number
  selectorType?: SelectorType
  size?: NumericSize
  warningQuantityThreshold: number
  showUnit: boolean
  quantitySelectorStep?: QuantitySelectorStepType
}

const CSS_HANDLES = [
  'quantitySelectorContainer',
  'quantitySelectorTitle',
  'availableQuantityContainer',
] as const

export type OnChangeCallback = {
  value: number
}

const BaseProductQuantity: StorefrontFunctionComponent<BaseProps> = ({
  dispatch,
  selectedItem,
  size = 'small',
  showLabel = true,
  selectedQuantity,
  warningQuantityThreshold = 0,
  selectorType = 'stepper',
  showUnit = true,
  quantitySelectorStep = 'unitMultiplier',
}) => {
  const { getSettings } = useRuntime()
  const settings = getSettings('vtex.store')
  const useSemanticHtml = settings?.advancedSettings?.a11ySemanticHtmlMigration
  const handles = useCssHandles(CSS_HANDLES)

  const onChange = useCallback(
    (e: OnChangeCallback) => {
      dispatch({ type: 'SET_QUANTITY', args: { quantity: e.value } })
    },
    [dispatch]
  )

  const availableQuantity =
    selectedItem?.sellers?.find(({ sellerDefault }) => sellerDefault === true)
      ?.commertialOffer?.AvailableQuantity ?? 0

  if (availableQuantity < 1 || !selectedItem) {
    return null
  }

  const showAvailable = availableQuantity <= warningQuantityThreshold
  const unitMultiplier =
    quantitySelectorStep === 'singleUnit' ? 1 : selectedItem.unitMultiplier

  const LabelComponent = useSemanticHtml ? 'label' : 'div'
  const labelProps = useSemanticHtml
    ? {
        htmlFor: 'vtex-product-quantity-input',
      }
    : {}

  return (
    <div
      className={`${handles.quantitySelectorContainer} flex flex-column mb4`}>
      {showLabel && (
        <LabelComponent
          className={`${handles.quantitySelectorTitle} mb3 c-muted-2 t-body`}
          {...labelProps}>
          <FormattedMessage id="store/product-quantity.quantity" />
        </LabelComponent>
      )}
      {selectorType === 'stepper' && (
        <StepperProductQuantity
          showUnit={showUnit}
          size={size}
          unitMultiplier={unitMultiplier}
          measurementUnit={selectedItem.measurementUnit}
          selectedQuantity={selectedQuantity}
          availableQuantity={availableQuantity}
          onChange={onChange}
          useSemanticHtml={useSemanticHtml}
        />
      )}
      {selectorType === 'dropdown' && (
        <DropdownProductQuantity
          itemId={selectedItem.itemId}
          selectedQuantity={selectedQuantity}
          availableQuantity={availableQuantity}
          onChange={onChange}
          size={size}
        />
      )}
      {showAvailable && (
        <div
          className={`${handles.availableQuantityContainer} mv4 c-muted-2 t-small`}>
          <FormattedMessage
            id="store/product-quantity.quantity-available"
            values={{ availableQuantity }}
          />
        </div>
      )}
    </div>
  )
}

export default BaseProductQuantity

```


[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 product-quantity/react/components/StepperProductQuantity.tsx from the product-quantity VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { FunctionComponent } from 'react'
import { NumericStepper } from 'vtex.styleguide'
import { useCssHandles } from 'vtex.css-handles'
import { SelectedItem } from 'vtex.product-context'

import { OnChangeCallback, BaseProps } from './BaseProductQuantity'

const DEFAULT_UNIT = 'un'

interface StepperProps {
  unitMultiplier: SelectedItem['unitMultiplier']
  measurementUnit: SelectedItem['measurementUnit']
  selectedQuantity: BaseProps['selectedQuantity']
  availableQuantity: number
  onChange: (e: OnChangeCallback) => void
  size: BaseProps['size']
  showUnit: boolean
  useSemanticHtml?: boolean
}

const CSS_HANDLES = ['quantitySelectorStepper'] as const

const StepperProductQuantity: FunctionComponent<StepperProps> = ({
  unitMultiplier = 1,
  measurementUnit = DEFAULT_UNIT,
  size = 'small',
  selectedQuantity,
  availableQuantity,
  onChange,
  showUnit,
  useSemanticHtml = false,
}) => {
  const handles = useCssHandles(CSS_HANDLES)

  return (
    <div className={handles.quantitySelectorStepper}>
      <NumericStepper
        size={size}
        minValue={1}
        unitMultiplier={unitMultiplier}
        suffix={
          showUnit && measurementUnit !== DEFAULT_UNIT
            ? measurementUnit
            : undefined
        }
        onChange={onChange}
        value={selectedQuantity}
        maxValue={availableQuantity || undefined}
        useSemanticHtml={useSemanticHtml}
      />
    </div>
  )
}

export default StepperProductQuantity

```