[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-organizations-graphql/node/resolvers/directives/validateStoreUserAccess.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { SchemaDirectiveVisitor } from 'graphql-tools'
import { AuthenticationError, ForbiddenError } from '@vtex/api'
import type { GraphQLField } from 'graphql'
import { defaultFieldResolver } from 'graphql'

import type { AuthAuditMetric } from '../../utils/metrics/auth'
import sendAuthMetric, { AuthMetric } from '../../utils/metrics/auth'
import { LICENSE_MANAGER_ROLES } from '../../constants'
import {
  validateAdminToken,
  validateAdminTokenOnHeader,
  validateApiToken,
  validateStoreToken,
} from './helper'

export class ValidateStoreUserAccess extends SchemaDirectiveVisitor {
  public visitFieldDefinition(field: GraphQLField<any, any>) {
    const { resolve = defaultFieldResolver } = field

    field.resolve = async (
      root: any,
      args: any,
      context: Context,
      info: any
    ) => {
      const {
        vtex: { adminUserAuthToken, storeUserAuthToken, logger },
      } = context

      // Get the admin permission from directive arguments, defaulting to VIEW permission
      const adminPermissionArg =
        this.args?.adminPermission || 'B2B_ORGANIZATIONS_VIEW'

      const requiredPermission =
        adminPermissionArg === 'B2B_ORGANIZATIONS_VIEW'
          ? LICENSE_MANAGER_ROLES.B2B_ORGANIZATIONS_VIEW
          : LICENSE_MANAGER_ROLES.B2B_ORGANIZATIONS_EDIT

      // get metrics data
      const operation = field?.astNode?.name?.value ?? context?.request?.url
      const userAgent = context?.request?.headers['user-agent'] as string
      const caller = context?.request?.headers['x-vtex-caller'] as string
      const forwardedHost = context?.request?.headers[
        'x-forwarded-host'
      ] as string

      // set metric fields with initial data
      let metricFields: AuthAuditMetric = {
        operation,
        forwardedHost,
        caller,
        userAgent,
      }

      const { hasAdminToken, hasValidAdminToken, hasValidAdminRole } =
        await validateAdminToken(
          context,
          adminUserAuthToken as string,
          requiredPermission
        )

      // add admin token metrics
      metricFields = {
        ...metricFields,
        hasAdminToken,
        hasValidAdminToken,
        hasValidAdminRole,
      }

      // allow access if has valid admin token and valid admin role
      if (hasValidAdminToken && hasValidAdminRole) {
        sendAuthMetric(
          context,
          logger,
          new AuthMetric(
            context?.vtex?.account,
            metricFields,
            'ValidateStoreUserAccessAudit'
          )
        )

        return resolve(root, args, context, info)
      }

      // If there's no valid admin token on context, search for it on header
      const {
        hasAdminTokenOnHeader,
        hasValidAdminTokenOnHeader,
        hasValidAdminRoleOnHeader,
      } = await validateAdminTokenOnHeader(context, requiredPermission)

      // add admin header token metrics
      metricFields = {
        ...metricFields,
        hasAdminTokenOnHeader,
        hasValidAdminTokenOnHeader,
        hasValidAdminRoleOnHeader,
      }

      // allow access if has valid admin token and valid admin role
      if (hasValidAdminTokenOnHeader && hasValidAdminRoleOnHeader) {
        // set adminUserAuthToken on context so it can be used later
        context.vtex.adminUserAuthToken = context?.headers
          .vtexidclientautcookie as string
        sendAuthMetric(
          context,
          logger,
          new AuthMetric(
            context?.vtex?.account,
            metricFields,
            'ValidateStoreUserAccessAudit'
          )
        )

        return resolve(root, args, context, info)
      }

      // If there's no valid admin token on context or header,
      // invalidate the adminUserAuthToken on context
      context.vtex.adminUserAuthToken = undefined

      const { hasApiToken, hasValidApiToken, hasValidApiRole } =
        await validateApiToken(context, requiredPermission)

      // add API token metrics
      metricFields = {
        ...metricFields,
        hasApiToken,
        hasValidApiToken,
        hasValidApiRole,
      }

      // allow access if has valid API token and valid API role
      if (hasValidApiToken && hasValidApiRole) {
        sendAuthMetric(
          context,
          logger,
          new AuthMetric(
            context?.vtex?.account,
            metricFields,
            'ValidateStoreUserAccessAudit'
          )
        )

        return resolve(root, args, context, info)
      }

      const { hasStoreToken, hasValidStoreToken } = await validateStoreToken(
        context,
        storeUserAuthToken as string
      )

      // add store token metrics
      metricFields = {
        ...metricFields,
        hasStoreToken,
        hasValidStoreToken,
      }

      // allow access if has valid store token (store users don't need LM role validation)
      if (hasValidStoreToken) {
        sendAuthMetric(
          context,
          logger,
          new AuthMetric(
            context?.vtex?.account,
            metricFields,
            'ValidateStoreUserAccessAudit'
          )
        )

        return resolve(root, args, context, info)
      }

      // deny access if no tokens were provided
      if (!hasAdminToken && !hasApiToken && !hasStoreToken) {
        logger.warn({
          message: 'ValidateStoreUserAccess: No token provided',
          ...metricFields,
        })
        throw new AuthenticationError('No token was provided')
      }

      // deny access if no valid tokens were provided or no valid role
      logger.warn({
        message: `ValidateStoreUserAccess: Invalid token or insufficient role permissions`,
        ...metricFields,
      })
      throw new ForbiddenError('Unauthorized Access')
    }
  }
}

```


[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-organizations-graphql/node/resolvers/directives/withSession.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-params */
import type { GraphQLField } from 'graphql'
import { defaultFieldResolver } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'

export class WithSession extends SchemaDirectiveVisitor {
  public visitFieldDefinition(field: GraphQLField<any, any>) {
    const { resolve = defaultFieldResolver } = field

    field.resolve = async (root: any, args: any, context: any, info: any) => {
      const {
        clients: { session },
        vtex: { sessionToken },
      } = context

      context.vtex.sessionData = await session
        .getSession(sessionToken as string, ['*'])
        .then((currentSession: any) => {
          return currentSession.sessionData
        })
        .catch(() => null)

      return resolve(root, args, context, info)
    }
  }
}

```


[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-organizations-graphql/node/resolvers/directives/withPermissions.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-params */
import type { GraphQLField } from 'graphql'
import { defaultFieldResolver } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'

import type StorefrontPermissions from '../../clients/storefrontPermissions'

export const getUserPermission = async (
  storefrontPermissions: StorefrontPermissions,
  app = 'vtex.b2b-organizations@3.x'
) => {
  const result = await storefrontPermissions.checkUserPermission(app)

  return result?.data?.checkUserPermission ?? null
}

export class WithPermissions extends SchemaDirectiveVisitor {
  public visitFieldDefinition(field: GraphQLField<any, any>) {
    const { resolve = defaultFieldResolver } = field

    field.resolve = async (root: any, args: any, context: any, info: any) => {
      const {
        clients: { storefrontPermissions },
        vtex: { adminUserAuthToken, logger },
      } = context

      const appClients = context.vtex as any
      const sender =
        context?.graphql?.query?.senderApp ??
        context?.graphql?.query?.extensions?.persistedQuery?.sender ??
        context?.request?.header['x-b2b-senderapp'] ??
        undefined

      appClients.storefrontPermissions = await getUserPermission(
        storefrontPermissions,
        sender
      ).catch((error: any) => {
        if (!adminUserAuthToken) {
          logger.error({
            message: 'getPermissionsError',
            error,
          })
        }

        return null
      })

      return resolve(root, args, context, info)
    }
  }
}

```


[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-organizations-graphql/node/resolvers/repository/CostCenterRepository.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import type { CostCenterInput, Result } from '../../typings'
import {
  COST_CENTER_DATA_ENTITY,
  COST_CENTER_SCHEMA_VERSION,
} from '../../mdSchema'
import MarketingTags from '../Mutations/MarketingTags'

const CostCenterRepository = {
  createCostCenter: async (
    _: void,
    organizationId: string,
    {
      addresses,
      id,
      name,
      paymentTerms,
      phoneNumber,
      businessDocument,
      stateRegistration,
      customFields,
      sellers,
      marketingTags,
    }: CostCenterInput,
    ctx: Context
  ): Promise<Result> => {
    const {
      clients: { masterdata },
      vtex: { logger },
    } = ctx

    const costCenter = {
      addresses,
      id,
      name,
      organization: organizationId,
      ...(paymentTerms && { paymentTerms }),
      ...(phoneNumber && { phoneNumber }),
      ...(businessDocument && { businessDocument }),
      ...(stateRegistration && { stateRegistration }),
      ...(customFields && { customFields }),
      ...(sellers && { sellers }),
    }

    const createCostCenterResult = await masterdata.createDocument({
      dataEntity: COST_CENTER_DATA_ENTITY,
      fields: costCenter,
      schema: COST_CENTER_SCHEMA_VERSION,
    })

    if (marketingTags && marketingTags?.length > 0) {
      MarketingTags.setMarketingTags(
        _,
        { costId: createCostCenterResult.DocumentId, tags: marketingTags },
        ctx
      ).catch((error) => {
        logger.error({
          error,
          message: 'setMarketingTags-error',
        })
      })
    }

    return {
      href: createCostCenterResult.Href,
      id: createCostCenterResult.DocumentId,
      status: '',
    }
  },
}

export default CostCenterRepository

```


[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-organizations-graphql/node/resolvers/Mutations/Users.test.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { randAlphaNumeric, randEmail, randUser, randUuid } from '@ngneat/falso'
import { GraphQLError } from 'graphql'

import type { UserArgs } from '../../typings'
import Users from './Users'

jest.mock('@vtex/api')
jest.mock('@vtex/diagnostics-nodejs', () => ({}))
jest.mock('../config')
jest.mock('../../utils/metrics/impersonate')

const mockContext = (
  impersonateUser = randUser(),
  permissions: string[] = [],
  costId: string = randUuid(),
  orgId: string = randUuid()
) => {
  return {
    clients: {
      storefrontPermissions: {
        getB2BUser: jest.fn().mockResolvedValue({
          data: {
            getB2BUser: {
              costId,
              orgId,
            },
          },
        }),
        getUser: jest.fn().mockResolvedValue({
          data: {
            getUser: {
              costId,
              orgId,
            },
          },
        }),
        impersonateUser: jest.fn().mockResolvedValue({
          data: {
            impersonateUser,
          },
        }),
      },
    },
    vtex: {
      logger: jest.fn(),
      sessionData: {
        namespaces: {
          'storefront-permissions': {
            costcenter: {
              value: costId,
            },
            organization: {
              value: orgId,
            },
            storeUserEmail: randEmail(),
            userId: randUuid(),
            userTargetId: randUuid(),
          },
        },
      },
      storefrontPermissions: {
        permissions,
      },
    },
  } as unknown as Context
}

describe('given an Users Mutation', () => {
  describe('when impersonate an B2B user', () => {
    describe('with invalid permission', () => {
      const user = randUser()

      const mockedContext = mockContext()

      it('should throws GraphQLError with operation-not-permitted.', async () => {
        await expect(
          Users.impersonateB2BUser(jest.fn() as never, user, mockedContext)
        ).rejects.toThrowError(new GraphQLError('operation-not-permitted'))
      })
    })

    describe('with permission impersonate-users-costcenter and the user with same costcenter', () => {
      const user = randUser()
      const impersonateUser = randUser()

      const mockedContext = mockContext(impersonateUser, [
        'impersonate-users-costcenter',
      ])

      let result: any

      beforeEach(async () => {
        result = await Users.impersonateB2BUser(
          jest.fn() as never,
          user,
          mockedContext
        )
      })
      it('should call impersonate user in storefront permission', () => {
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toHaveBeenCalledTimes(1)
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toBeCalledWith({ userId: user.id })
      })

      it('should return impersonate user data', () => {
        expect(result).toEqual(impersonateUser)
      })
    })

    describe('with permission impersonate-users-organization and the user with same organization', () => {
      const user = randUser()
      const impersonateUser = randUser()

      const mockedContext = mockContext(impersonateUser, [
        'impersonate-users-organization',
      ])

      let result: any

      beforeEach(async () => {
        result = await Users.impersonateB2BUser(
          jest.fn() as never,
          user,
          mockedContext
        )
      })
      it('should call impersonate user in storefront permission', () => {
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toHaveBeenCalledTimes(1)
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toBeCalledWith({ userId: user.id })
      })

      it('should return impersonate user data', () => {
        expect(result).toEqual(impersonateUser)
      })
    })

    describe('with permission impersonate-users-all', () => {
      const user = randUser()
      const impersonateUser = randUser()

      const mockedContext = mockContext(impersonateUser, [
        'impersonate-users-all',
      ])

      let result: any

      beforeEach(async () => {
        result = await Users.impersonateB2BUser(
          jest.fn() as never,
          user,
          mockedContext
        )
      })
      it('should call impersonate user in storefront permission', () => {
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toHaveBeenCalledTimes(1)
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toBeCalledWith({ userId: user.id })
      })

      it('should return impersonate user data', () => {
        expect(result).toEqual(impersonateUser)
      })
    })
  })

  describe('when impersonate an user', () => {
    describe('with invalid permission', () => {
      const clId = randAlphaNumeric({ length: 10 }).toString()
      const userId = randUser().id
      const user: Partial<UserArgs> = { clId, userId }
      const mockedContext = mockContext()

      it('should throws GraphQLError with operation-not-permitted.', async () => {
        await expect(
          Users.impersonateUser(
            jest.fn() as never,
            user as UserArgs,
            mockedContext
          )
        ).rejects.toThrowError(new GraphQLError('operation-not-permitted'))
      })
    })

    describe('with permission impersonate-users-costcenter and the user with same costcenter', () => {
      const clId = randAlphaNumeric({ length: 10 }).toString()
      const userId = randUser().id
      const user: Partial<UserArgs> = { clId, userId }
      const impersonateUser = randUser()

      const mockedContext = mockContext(impersonateUser, [
        'impersonate-users-costcenter',
      ])

      let result: any

      beforeEach(async () => {
        result = await Users.impersonateUser(
          jest.fn() as never,
          user as UserArgs,
          mockedContext
        )
      })
      it('should call impersonate user in storefront permission', () => {
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toHaveBeenCalledTimes(1)
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toBeCalledWith({ userId })
      })

      it('should not return error', () => {
        expect(result?.status).not.toEqual('error')
      })
    })

    describe('with permission impersonate-users-organization and the user with same organization', () => {
      const clId = randAlphaNumeric({ length: 10 }).toString()
      const userId = randUser().id
      const user: Partial<UserArgs> = { clId, userId }
      const impersonateUser = randUser()

      const mockedContext = mockContext(impersonateUser, [
        'impersonate-users-organization',
      ])

      let result: any

      beforeEach(async () => {
        result = await Users.impersonateUser(
          jest.fn() as never,
          user as UserArgs,
          mockedContext
        )
      })
      it('should call impersonate user in storefront permission', () => {
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toHaveBeenCalledTimes(1)
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toBeCalledWith({ userId })
      })

      it('should not return error', () => {
        expect(result?.status).not.toEqual('error')
      })
    })

    describe('with permission impersonate-users-all', () => {
      const clId = randAlphaNumeric({ length: 10 }).toString()
      const userId = randUser().id
      const user: Partial<UserArgs> = { clId, userId }
      const impersonateUser = randUser()

      const mockedContext = mockContext(impersonateUser, [
        'impersonate-users-all',
      ])

      let result: any

      beforeEach(async () => {
        result = await Users.impersonateUser(
          jest.fn() as never,
          user as UserArgs,
          mockedContext
        )
      })
      it('should call impersonate user in storefront permission', () => {
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toHaveBeenCalledTimes(1)
        expect(
          mockedContext.clients.storefrontPermissions.impersonateUser
        ).toBeCalledWith({ userId })
      })

      it('should not return error', () => {
        expect(result?.status).not.toEqual('error')
      })
    })
  })
})

```


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

[User] Show me the implementation of b2b-organizations-graphql/node/resolvers/Mutations/Organizations.test.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  randAddress,
  randAirportName,
  randCompanyName,
  randEmail,
  randFirstName,
  randLastName,
  randSuperheroName,
  randUser,
  randUuid,
  randWord,
} from '@ngneat/falso'

import {
  COST_CENTER_DATA_ENTITY,
  COST_CENTER_SCHEMA_VERSION,
  ORGANIZATION_DATA_ENTITY,
  ORGANIZATION_SCHEMA_VERSION,
} from '../../mdSchema'
import type {
  AddressInput,
  NormalizedOrganizationInput,
  OrganizationInput,
  OrganizationRequest,
} from '../../typings'
import { ORGANIZATION_REQUEST_STATUSES } from '../../utils/constants'
import Organizations from './Organizations'

jest.mock('@vtex/api')
jest.mock('@vtex/diagnostics-nodejs', () => ({}))
jest.mock('../config')
jest.mock('../Queries/Settings')

const mockGetDocument = jest.fn().mockResolvedValue({
  b2bCustomerAdmin: {
    email: randEmail(),
    firstName: randFirstName(),
    lastName: randLastName(),
  },
  defaultCostCenter: {
    address: {},
  },
  status: ORGANIZATION_REQUEST_STATUSES.PENDING,
} as OrganizationRequest)

const mockContext = (
  orgId: string = randUuid(),
  roleId: string = randUuid(),
  costId: string = randUuid()
) => {
  return {
    clients: {
      analytics: {
        sendMetric: jest.fn(),
      },
      masterdata: {
        createDocument: jest
          .fn()
          .mockResolvedValueOnce({
            DocumentId: orgId,
          })
          .mockResolvedValue({
            DocumentId: costId,
          }),
        getDocument: mockGetDocument,
        searchDocuments: jest.fn().mockResolvedValue({}),
        updatePartialDocument: jest.fn().mockResolvedValueOnce({}),
      },
      storefrontPermissions: {
        listRoles: jest.fn().mockResolvedValue({
          data: { listRoles: [{ id: roleId, slug: 'customer-admin' }] },
        }),
        saveUser: jest
          .fn()
          .mockResolvedValue({ data: { saveUser: randUser() } }),
      },
    },
    vtex: {
      logger: jest.fn(),
    },
  } as unknown as Context
}

afterEach(() => {
  jest.resetAllMocks()
})

describe('given an Organization Mutation', () => {
  describe('when update an organization', () => {
    const orgId = randUuid()
    const input = {
      id: '1',
      notes: 'OK',
      notifyUsers: true,
      status: ORGANIZATION_REQUEST_STATUSES.APPROVED,
    }

    let result: { id?: string; message: string; status: string }
    let mockedContext: Context

    const createDate = new Date('2023-10-10')

    describe('with status APPROVED and state registration', () => {
      const stateRegistration = randWord()

      const organization = {
        b2bCustomerAdmin: {
          email: randEmail(),
          firstName: randFirstName(),
          lastName: randLastName(),
        },
        defaultCostCenter: {
          address: randAddress(),
          stateRegistration,
        },
        id: orgId,
        name: randCompanyName(),
        stateRegistration,
        status: ORGANIZATION_REQUEST_STATUSES.PENDING,
        tradeName: randAirportName(),
      } as unknown as OrganizationInput

      beforeEach(async () => {
        mockGetDocument.mockResolvedValueOnce(organization)
        jest.useFakeTimers().setSystemTime(createDate)
        mockedContext = mockContext(orgId)
        result = await Organizations.updateOrganizationRequest(
          jest.fn() as never,
          input,
          mockedContext
        )
      })

      it('should return organization id with success', () => {
        expect(result.status).toEqual('success')
        expect(result.id).toEqual(orgId)
      })

      it('should call create organization with data', () => {
        expect(
          mockedContext.clients.masterdata.createDocument
        ).toHaveBeenNthCalledWith(2, {
          dataEntity: COST_CENTER_DATA_ENTITY,
          fields: {
            addresses: [organization.defaultCostCenter?.address],
            organization: orgId,
            stateRegistration,
          },
          schema: COST_CENTER_SCHEMA_VERSION,
        })
      })

      it('should save the user in store front permission', () => {
        expect(
          mockedContext.clients.storefrontPermissions.saveUser
        ).toHaveBeenCalledTimes(1)
      })
    })

    describe('with status APPROVED and without state registration', () => {
      const organization = {
        b2bCustomerAdmin: {
          email: randEmail(),
          firstName: randFirstName(),
          lastName: randLastName(),
        },
        defaultCostCenter: {},
        id: orgId,
        name: randCompanyName(),
        status: ORGANIZATION_REQUEST_STATUSES.PENDING,
        tradeName: randAirportName(),
      } as unknown as OrganizationInput

      beforeEach(async () => {
        mockGetDocument.mockResolvedValueOnce(organization)
        mockedContext = mockContext(orgId)

        result = await Organizations.updateOrganizationRequest(
          jest.fn() as never,
          input,
          mockedContext
        )
      })

      it('should return organization id with success', () => {
        expect(result.status).toEqual('success')
        expect(result.id).toEqual(orgId)
      })

      it('should call create organization without state registration', () => {
        expect(
          mockedContext.clients.masterdata.createDocument
        ).toHaveBeenNthCalledWith(2, {
          dataEntity: COST_CENTER_DATA_ENTITY,
          fields: {
            addresses: [],
            organization: orgId,
          },
          schema: COST_CENTER_SCHEMA_VERSION,
        })
      })

      it('should save the user in store front permission', () => {
        expect(
          mockedContext.clients.storefrontPermissions.saveUser
        ).toHaveBeenCalledTimes(1)
      })
    })
  })
  describe('when create an organization', () => {
    const orgId = randUuid()

    describe('with organization id and without cost center id', () => {
      const costCenter = {
        address: randAddress() as unknown as AddressInput,
        name: randLastName(),
      }

      const defaultCostCenter = {
        address: randAddress() as unknown as AddressInput,
        name: randSuperheroName(),
      }

      const input = {
        b2bCustomerAdmin: {
          email: randEmail(),
          firstName: randFirstName(),
        },
        costCenters: [costCenter],
        defaultCostCenter,
        id: orgId,
        name: randCompanyName(),
        tradeName: randAirportName(),
      } as NormalizedOrganizationInput

      let result: { href: any; id: any }
      let mockedContext: Context

      const roleId = randUuid()
      const costId = randUuid()

      const createDate = new Date('2020-01-01')

      beforeEach(async () => {
        jest.useFakeTimers().setSystemTime(createDate)
        mockedContext = mockContext(orgId, roleId, costId)
        result = await Organizations.createOrganizationAndCostCentersWithId(
          jest.fn() as never,
          { input },
          mockedContext
        )
      })

      it('should create the organization with the data expected', () => {
        expect(
          mockedContext.clients.masterdata.createDocument
        ).toHaveBeenNthCalledWith(1, {
          dataEntity: ORGANIZATION_DATA_ENTITY,
          fields: {
            collections: [],
            created: createDate,
            customFields: [],
            id: orgId,
            name: input.name,
            permissions: {
              createQuote: true,
            },
            sellers: [],
            status: 'active',
            tradeName: input.tradeName,
          },
          schema: ORGANIZATION_SCHEMA_VERSION,
        })
      })
      it('should create the cost center specified without set id', () => {
        expect(
          mockedContext.clients.masterdata.createDocument
        ).toHaveBeenNthCalledWith(2, {
          dataEntity: COST_CENTER_DATA_ENTITY,
          fields: {
            addresses: [costCenter.address],
            id: undefined,
            name: costCenter.name,
            organization: orgId,
          },
          schema: COST_CENTER_SCHEMA_VERSION,
        })
      })
      it('should create the default cost center', () => {
        expect(
          mockedContext.clients.masterdata.createDocument
        ).toHaveBeenNthCalledWith(3, {
          dataEntity: COST_CENTER_DATA_ENTITY,
          fields: {
            addresses: [defaultCostCenter.address],
            name: defaultCostCenter.name,
            organization: orgId,
          },
          schema: COST_CENTER_SCHEMA_VERSION,
        })
      })
      it('should set admin user for each cost center', () => {
        expect(
          mockedContext.clients.storefrontPermissions.saveUser
        ).toBeCalledWith({
          clId: null,
          costId,
          email: input.b2bCustomerAdmin.email,
          name: `${input.b2bCustomerAdmin.firstName} ${input.b2bCustomerAdmin.lastName}`,
          orgId,
          roleId,
        })
      })
      it('should create the organization with the id specified', () => {
        expect(result.id).toEqual(orgId)
      })
    })
    describe('with organization id and cost center id', () => {
      const costId = randUuid()
      const costCenter = {
        id: costId,
        name: randLastName(),
      }

      const defaultCostCenter = {
        id: costId,
        name: randSuperheroName(),
      }

      const input = {
        b2bCustomerAdmin: {
          email: randEmail(),
          firstName: randFirstName(),
        },
        costCenters: [costCenter],
        defaultCostCenter,
        id: orgId,
        name: randCompanyName(),
        tradeName: randAirportName(),
      } as NormalizedOrganizationInput

      let result: { href: any; id: any }
      let mockedContext: Context

      const createDate = new Date('2020-01-01')

      const roleId = randUuid()

      beforeEach(async () => {
        jest.useFakeTimers().setSystemTime(createDate)
        mockedContext = mockContext(orgId, roleId, costId)
        result = await Organizations.createOrganizationAndCostCentersWithId(
          jest.fn() as never,
          { input },
          mockedContext
        )
      })

      it('should create the organization with the data expected', () => {
        expect(
          mockedContext.clients.masterdata.createDocument
        ).toHaveBeenNthCalledWith(1, {
          dataEntity: ORGANIZATION_DATA_ENTITY,
          fields: {
            collections: [],
            created: createDate,
            customFields: [],
            id: orgId,
            name: input.name,
            permissions: {
              createQuote: true,
            },
            sellers: [],
            status: 'active',
            tradeName: input.tradeName,
          },
          schema: ORGANIZATION_SCHEMA_VERSION,
        })
      })
      it('should create the cost center specified without set id', () => {
        expect(
          mockedContext.clients.masterdata.createDocument
        ).toHaveBeenNthCalledWith(2, {
          dataEntity: COST_CENTER_DATA_ENTITY,
          fields: {
            addresses: [],
            organization: orgId,
            ...costCenter,
          },
          schema: COST_CENTER_SCHEMA_VERSION,
        })
      })
      it('should create the default cost center', () => {
        expect(
          mockedContext.clients.masterdata.createDocument
        ).toHaveBeenNthCalledWith(3, {
          dataEntity: COST_CENTER_DATA_ENTITY,
          fields: {
            addresses: [],
            organization: orgId,
            ...defaultCostCenter,
          },
          schema: COST_CENTER_SCHEMA_VERSION,
        })
      })
      it('should set admin user for each cost center', () => {
        expect(
          mockedContext.clients.storefrontPermissions.saveUser
        ).toHaveBeenNthCalledWith(1, {
          clId: null,
          costId: costCenter.id,
          email: input.b2bCustomerAdmin.email,
          name: `${input.b2bCustomerAdmin.firstName} ${input.b2bCustomerAdmin.lastName}`,
          orgId,
          roleId,
        })
        expect(
          mockedContext.clients.storefrontPermissions.saveUser
        ).toHaveBeenNthCalledWith(2, {
          clId: null,
          costId: defaultCostCenter.id,
          email: input.b2bCustomerAdmin.email,
          name: `${input.b2bCustomerAdmin.firstName} ${input.b2bCustomerAdmin.lastName}`,
          orgId,
          roleId,
        })
      })
      it('should create the organization with the id specified', () => {
        expect(result.id).toEqual(orgId)
      })
    })
  })
})

```


[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-organizations-graphql/node/resolvers/Mutations/Settings.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import GraphQLError from '../../utils/GraphQLError'
import checkConfig, { getAppId } from '../config'
import type { B2BSettingsInput } from '../../typings'

export const B2B_SETTINGS_DOCUMENT_ID = 'b2bSettings'

const Settings = {
  saveAppSettings: async (_: any, __: any, ctx: Context) => {
    const {
      clients: { vbase },
      vtex: { logger },
    } = ctx

    const app: string = getAppId()

    const newSettings = {}

    try {
      await vbase.saveJSON('b2borg', app, newSettings)

      return { status: 'success', message: '' }
    } catch (error) {
      logger.error({
        error,
        message: 'saveAppSettings-error',
      })

      return { status: 'error', message: error }
    }
  },
  saveB2BSettings: async (
    _: void,
    {
      input: {
        autoApprove,
        businessReadOnly,
        stateReadOnly,
        defaultPaymentTerms,
        defaultPriceTables,
        uiSettings,
        organizationCustomFields,
        costCenterCustomFields,
        transactionEmailSettings,
      },
    }: {
      input: B2BSettingsInput
      page: number
      pageSize: number
    },
    ctx: Context
  ) => {
    const {
      clients: { vbase },
      vtex: { logger },
    } = ctx

    // create schema if it doesn't exist
    await checkConfig(ctx)

    const B2B_SETTINGS_DATA_ENTITY = 'b2b_settings'

    // Get current settings to save them depending on where the saveB2BSettings is coming from. Custom fields come without autoApprove settings and vice versa.

    let currentB2BSettings: B2BSettingsInput | undefined

    try {
      const getSettingsObj: B2BSettingsInput = await vbase.getJSON(
        B2B_SETTINGS_DATA_ENTITY,
        'settings'
      )

      currentB2BSettings = getSettingsObj
    } catch (e) {
      if (e.response?.status === 404) {
        // when run for the first time on the project - returns 404. ignore in that case
      } else {
        logger.error({
          message: 'saveB2BSettings-currentB2BSettings-error',
          error: e,
        })
        if (e.message) {
          throw new GraphQLError(e.message)
        } else if (e.response?.data?.message) {
          throw new GraphQLError(e.response.data.message)
        } else {
          throw new GraphQLError(e)
        }
      }
    }

    try {
      const b2bSettings = {
        autoApprove,
        businessReadOnly,
        stateReadOnly,
        costCenterCustomFields:
          costCenterCustomFields ?? currentB2BSettings?.costCenterCustomFields,
        defaultPaymentTerms,
        defaultPriceTables,
        organizationCustomFields:
          organizationCustomFields ??
          currentB2BSettings?.organizationCustomFields,
        transactionEmailSettings:
          transactionEmailSettings ??
          currentB2BSettings?.transactionEmailSettings,
        uiSettings: {
          showModal:
            uiSettings?.showModal ?? currentB2BSettings?.uiSettings?.showModal,
          clearCart:
            uiSettings?.clearCart ?? currentB2BSettings?.uiSettings?.clearCart,
          topBar: uiSettings?.topBar ?? currentB2BSettings?.uiSettings?.topBar,
        },
      }

      await vbase.saveJSON(B2B_SETTINGS_DATA_ENTITY, 'settings', b2bSettings)

      return {
        status: 'success',
      }
    } catch (e) {
      logger.error({
        error: e,
        message: 'saveB2BSettings-error',
      })
      if (e.message) {
        throw new GraphQLError(e.message)
      } else if (e.response?.data?.message) {
        throw new GraphQLError(e.response.data.message)
      } else {
        throw new GraphQLError(e)
      }
    }
  },

  saveSalesChannels: async (
    _: void,
    { channels }: { channels: any[] },
    ctx: Context
  ) => {
    const {
      clients: { vbase },
      vtex: { logger },
    } = ctx

    try {
      await vbase.saveJSON('b2borg', 'salesChannels', channels)
    } catch (error) {
      logger.error({
        error,
        message: 'saveSalesChannels-Error',
      })

      return { status: 'error', message: error }
    }

    return { status: 'success', message: '' }
  },
}

export default 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-organizations-graphql/node/resolvers/Mutations/Organizations.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import type { Seller } from '../../clients/sellers'
import {
  ORGANIZATION_DATA_ENTITY,
  ORGANIZATION_FIELDS,
  ORGANIZATION_REQUEST_DATA_ENTITY,
  ORGANIZATION_REQUEST_FIELDS,
  ORGANIZATION_REQUEST_SCHEMA_VERSION,
  ORGANIZATION_SCHEMA_VERSION,
} from '../../mdSchema'
import type {
  B2BSettingsInput,
  Collection,
  DefaultCostCenterInput,
  NormalizedOrganizationInput,
  Organization,
  OrganizationInput,
  OrganizationRequest,
  PaymentTerm,
  Price,
} from '../../typings'
import {
  ORGANIZATION_REQUEST_STATUSES,
  ORGANIZATION_STATUSES,
} from '../../utils/constants'
import GraphQLError, { getErrorMessage } from '../../utils/GraphQLError'
import {
  sendOrganizationStatusMetric,
  sendUpdateOrganizationMetric,
} from '../../utils/metrics/organization'
import checkConfig from '../config'
import message from '../message'
import B2BSettings from '../Queries/Settings'
import CostCenterRepository from '../repository/CostCenterRepository'

const createUserAndAttachToOrganization = async ({
  storefrontPermissions,
  email,
  costCenterId,
  organizationId,
  firstName,
  lastName,
  logger,
}: any) => {
  // get roleId of org admin
  const roles = await storefrontPermissions.listRoles().then((result: any) => {
    return result.data.listRoles
  })

  const roleId = roles.find(
    (roleItem: any) => roleItem.slug === 'customer-admin'
  ).id

  // grant user org admin role, assign org and cost center
  return storefrontPermissions
    .saveUser({
      clId: null,
      costId: costCenterId,
      email,
      name: `${firstName} ${lastName}`,
      orgId: organizationId,
      roleId,
    })
    .then((result: any) => {
      return result.data.saveUser
    })
    .catch((error: any) => {
      logger.error({
        error,
        message: 'addUser-error',
      })
    })
}

const createOrganization = async (
  _: void,
  {
    id,
    name,
    tradeName,
    paymentTerms,
    priceTables,
    salesChannel,
    sellers,
    customFields,
    collections,
  }: OrganizationInput,
  ctx: Context
): Promise<{
  Id: string
  Href: string
  DocumentId: string
}> => {
  const {
    clients: { masterdata },
  } = ctx

  const organization = {
    id,
    name,
    ...(tradeName && { tradeName }),
    collections: [],
    created: new Date(),
    ...(paymentTerms?.length && { paymentTerms }),
    ...(priceTables?.length && { priceTables }),
    ...(salesChannel && { salesChannel }),
    ...(sellers && { sellers }),
    ...(collections && { collections }),
    customFields: customFields ?? [],
    status: ORGANIZATION_STATUSES.ACTIVE,
    permissions: { createQuote: true },
  }

  return masterdata.createDocument({
    dataEntity: ORGANIZATION_DATA_ENTITY,
    fields: organization,
    schema: ORGANIZATION_SCHEMA_VERSION,
  })
}

const findPaymentTerms = async (paymentTermNames: string[], ctx: Context) => {
  const {
    clients: { payments },
  } = ctx

  const paymentRules = await payments.getPaymentTerms()
  const paymentTerms = paymentTermNames
    ?.map((paymentTermName: string) => {
      const paymentTerm = paymentRules.find(
        (paymentRule: any) => paymentRule.name === paymentTermName
      )

      if (paymentTerm) {
        return {
          id: paymentTerm.id.toString(),
          name: paymentTerm.name,
        } as PaymentTerm
      }

      return null
    })
    .filter((paymentTerm: any) => paymentTerm !== null)

  return paymentTerms
}

const findSellers = async (sellerNames: string[], ctx: Context) => {
  const {
    clients: { sellers },
  } = ctx

  const availableSellers = (await sellers.getSellers())?.items
  const sellersFound = sellerNames
    ?.map((sellerName: string) => {
      const seller = availableSellers.find((s: any) => s.name === sellerName)

      if (seller) {
        return {
          id: seller.id,
          name: seller.name,
        } as Seller
      }

      return null
    })
    .filter((seller: any) => seller !== null)

  return sellersFound
}

const findCollections = async (collectionsNames: string[], ctx: Context) => {
  const {
    clients: { catalog },
  } = ctx

  const collectionsFound = collectionsNames
    .map(async (collectionName: string) => {
      const availableCollections = (
        await catalog.collectionsAvailable(collectionName)
      )?.items

      const collection = availableCollections.find(
        (c: any) => c.name === collectionName
      )

      if (collection) {
        return {
          id: collection.id.toString(),
          name: collection.name,
        } as Collection
      }

      return null
    })
    .filter((collection: any) => collection !== null)

  return Promise.all(collectionsFound)
}

const createOrganizationAndCostCenterWithAdminUser = async (
  _: void,
  organization: NormalizedOrganizationInput,
  ctx: Context
) => {
  const {
    clients: { storefrontPermissions, mail },
    vtex: { logger },
  } = ctx

  // create schema if it doesn't exist
  await checkConfig(ctx)

  try {
    // copy fields from NormalizedOrganizationInput to OrganizationInput
    // and transform fields that need transformation
    const organizationInput = {
      id: organization.id,
      name: organization.name,
      tradeName: organization.tradeName,
      b2bCustomerAdmin: organization.b2bCustomerAdmin,
      defaultCostCenter: organization.defaultCostCenter,
      costCenters: organization.costCenters,
      customFields: organization.customFields,
      permissions: { createQuote: true },
      paymentTerms: organization.paymentTerms
        ? await findPaymentTerms(organization.paymentTerms, ctx)
        : [],
      priceTables: organization.priceTables,
      salesChannel: organization.salesChannel,
      sellers: organization.sellers
        ? await findSellers(organization.sellers, ctx)
        : [],
      collections: organization.collections
        ? await findCollections(organization.collections, ctx)
        : [],
    } as OrganizationInput

    // create organization
    const createOrganizationResult = await createOrganization(
      _,
      organizationInput,
      ctx
    )

    const organizationId = createOrganizationResult.DocumentId

    const { defaultCostCenter, costCenters } = organization
    const { email, firstName, lastName } = organization.b2bCustomerAdmin

    const settings = (await B2BSettings.getB2BSettings(
      undefined,
      undefined,
      ctx
    )) as B2BSettingsInput

    if (costCenters?.length) {
      await Promise.all(
        costCenters?.map(async (costCenter: DefaultCostCenterInput) => {
          const addresses = costCenter.address ? [costCenter.address] : []

          const { id: costCenterId } =
            await CostCenterRepository.createCostCenter(
              _,
              organizationId,
              {
                addresses,
                ...costCenter,
              },
              ctx
            )

          const userToAdd = costCenter.user ?? {
            email,
            firstName,
            lastName,
          }

          createUserAndAttachToOrganization({
            costCenterId,
            email: userToAdd.email,
            firstName: userToAdd.firstName,
            lastName: userToAdd.lastName,
            logger,
            organizationId,
            storefrontPermissions,
          })
        })
      )
    }

    if (defaultCostCenter) {
      const addresses = defaultCostCenter.address
        ? [defaultCostCenter.address]
        : []

      const { id: costCenterId } = await CostCenterRepository.createCostCenter(
        _,
        organizationId,
        {
          addresses,
          ...defaultCostCenter,
        },
        ctx
      )

      await createUserAndAttachToOrganization({
        costCenterId,
        email,
        firstName,
        lastName,
        logger,
        organizationId,
        storefrontPermissions,
      })
    }

    if (settings?.transactionEmailSettings?.organizationCreated) {
      message({
        logger,
        mail,
        storefrontPermissions,
      }).organizationCreated(organizationInput.name)
    }

    return {
      href: createOrganizationResult.Href,
      id: organizationId,
      status: '',
    }
  } catch (error) {
    logger.error({
      error,
      message: 'createOrganization-error',
    })
    throw new GraphQLError(getErrorMessage(error))
  }
}

const Organizations = {
  createOrganization: async (
    _: void,
    {
      input: {
        name,
        tradeName,
        defaultCostCenter,
        costCenters,
        customFields,
        paymentTerms,
        priceTables,
        salesChannel,
        sellers,
      },
      notifyUsers = true,
    }: { input: OrganizationInput; notifyUsers?: boolean },
    ctx: Context
  ) => {
    const {
      clients: { storefrontPermissions, mail },
      vtex: { logger },
    } = ctx

    // create schema if it doesn't exist
    await checkConfig(ctx)

    try {
      const organization = {
        customFields,
        name,
        paymentTerms,
        priceTables,
        salesChannel,
        sellers,
        tradeName,
      } as OrganizationInput

      // create organization
      const createOrganizationResult = await createOrganization(
        _,
        organization,
        ctx
      )

      const organizationId = createOrganizationResult.DocumentId

      let costCenterResult: any[] = []

      if (!defaultCostCenter && costCenters?.length) {
        costCenterResult = await Promise.all(
          costCenters?.map(async (costCenter: any) => {
            const addresses = costCenter.address ? [costCenter.address] : []

            CostCenterRepository.createCostCenter(
              _,
              organizationId,
              {
                addresses,
                ...costCenter,
              },
              ctx
            )
          })
        )
      } else if (defaultCostCenter) {
        const addresses = defaultCostCenter.address
          ? [defaultCostCenter.address]
          : []

        costCenterResult = [
          await CostCenterRepository.createCostCenter(
            _,
            organizationId,
            {
              addresses,
              ...defaultCostCenter,
            },
            ctx
          ),
        ]
      }

      const settings = (await B2BSettings.getB2BSettings(
        undefined,
        undefined,
        ctx
      )) as B2BSettingsInput

      if (
        notifyUsers &&
        settings?.transactionEmailSettings?.organizationCreated
      ) {
        message({
          storefrontPermissions,
          logger,
          mail,
        }).organizationCreated(name)
      }

      return {
        costCenterId: costCenterResult[0].id,
        href: createOrganizationResult.Href,
        id: createOrganizationResult.DocumentId,
        status: '',
      }
    } catch (error) {
      logger.error({
        error,
        message: 'createOrganization-error',
      })
      throw new GraphQLError(getErrorMessage(error))
    }
  },
  createOrganizationRequest: async (
    _: void,
    {
      input: {
        b2bCustomerAdmin,
        costCenters,
        defaultCostCenter,
        customFields,
        name,
        tradeName,
        priceTables,
        salesChannel,
        paymentTerms,
        sellers,
      },
      notifyUsers = true,
    }: { input: OrganizationInput; notifyUsers?: boolean },
    ctx: Context
  ) => {
    const {
      clients: { masterdata, storefrontPermissions, mail },
      vtex: { logger },
    } = ctx

    // create schema if it doesn't exist
    await checkConfig(ctx)

    const now = new Date()

    const settings = (await B2BSettings.getB2BSettings(
      undefined,
      undefined,
      ctx
    )) as B2BSettingsInput

    const status = ORGANIZATION_REQUEST_STATUSES.PENDING

    paymentTerms =
      paymentTerms ??
      (settings?.defaultPaymentTerms as unknown as PaymentTerm[])
    priceTables =
      priceTables ?? (settings?.defaultPriceTables as unknown as Price[])

    if (!defaultCostCenter && costCenters?.length) {
      defaultCostCenter = costCenters.shift()
    }

    const organizationRequest = {
      name,
      ...(tradeName && { tradeName }),
      ...(priceTables && { priceTables }),
      ...(salesChannel && { salesChannel }),
      ...(paymentTerms && { paymentTerms }),
      ...(sellers && { sellers }),
      b2bCustomerAdmin,
      costCenters,
      created: now,
      customFields: customFields ?? [],
      defaultCostCenter,
      notes: '',
      status,
    }

    try {
      const result = (await masterdata.createDocument({
        dataEntity: ORGANIZATION_REQUEST_DATA_ENTITY,
        fields: organizationRequest,
        schema: ORGANIZATION_REQUEST_SCHEMA_VERSION,
      })) as any

      if (settings?.autoApprove) {
        await Organizations.updateOrganizationRequest(
          _,
          {
            id: result.DocumentId,
            notes: 'Auto approved',
            notifyUsers,
            status: ORGANIZATION_REQUEST_STATUSES.APPROVED,
          },
          ctx
        )
      }

      if (settings?.transactionEmailSettings?.organizationRequestCreated) {
        message({
          logger,
          mail,
          storefrontPermissions,
        }).organizationRequestCreated(
          organizationRequest.name,
          b2bCustomerAdmin.firstName,
          b2bCustomerAdmin.email,
          ''
        )
      }

      return {
        href: result.Href,
        id: result.DocumentId,
        status: organizationRequest.status,
      }
    } catch (error) {
      logger.error({
        error,
        message: 'createOrganizationRequest-error',
      })
      throw new GraphQLError(getErrorMessage(error))
    }
  },

  createOrganizationAndCostCentersWithId: async (
    _: void,
    { input }: { input: NormalizedOrganizationInput },
    ctx: Context
  ): Promise<{
    href: string
    id: string
  }> => {
    const {
      vtex: { logger },
    } = ctx

    // create schema if it doesn't exist
    await checkConfig(ctx)

    try {
      // create organization
      const { href, id } = await createOrganizationAndCostCenterWithAdminUser(
        _,
        input,
        ctx
      )

      return {
        href,
        id,
      }
    } catch (error) {
      logger.error({
        error,
        message: 'createOrganizationWitId-error',
      })
      throw new GraphQLError(getErrorMessage(error))
    }
  },

  deleteOrganizationRequest: async (
    _: void,
    { id }: { id: string },
    ctx: Context
  ) => {
    const {
      clients: { masterdata },
    } = ctx

    try {
      await masterdata.deleteDocument({
        dataEntity: ORGANIZATION_REQUEST_DATA_ENTITY,
        id,
      })

      return { status: 'success', message: '' }
    } catch (e) {
      throw new GraphQLError(getErrorMessage(e))
    }
  },
  updateOrganization: async (
    _: void,
    {
      id,
      name,
      tradeName,
      status,
      collections,
      paymentTerms,
      priceTables,
      customFields,
      salesChannel,
      sellers,
      notifyUsers = true,
      permissions,
    }: {
      id: string
      name: string
      tradeName?: string
      status: string
      collections: any[]
      paymentTerms: any[]
      priceTables: any[]
      customFields: any[]
      salesChannel?: string
      sellers?: any[]
      notifyUsers?: boolean
      permissions?: Permissions
    },
    ctx: Context
  ) => {
    const {
      clients: { storefrontPermissions, mail, masterdata },
      vtex: { logger },
    } = ctx

    // create schema if it doesn't exist
    await checkConfig(ctx)

    const settings = (await B2BSettings.getB2BSettings(
      undefined,
      undefined,
      ctx
    )) as B2BSettingsInput

    let currentOrganizationData: Organization | undefined

    try {
      currentOrganizationData = await masterdata.getDocument({
        dataEntity: ORGANIZATION_DATA_ENTITY,
        fields: ORGANIZATION_FIELDS,
        id,
      })

      if (
        currentOrganizationData?.status !== status &&
        notifyUsers &&
        settings?.transactionEmailSettings?.organizationStatusChanged
      ) {
        await message({
          logger,
          mail,
          storefrontPermissions,
        }).organizationStatusChanged(name, id, status)
      }
    } catch (error) {
      logger.warn({
        error,
        message: 'updateOrganization-emailOnStatusChangeError',
      })
    }

    try {
      const fields = {
        collections,
        ...((tradeName || tradeName === '') && { tradeName }),
        customFields,
        name,
        paymentTerms,
        priceTables,
        ...(salesChannel && { salesChannel }),
        ...(sellers && { sellers }),
        status,
        permissions,
      }

      await masterdata.updatePartialDocument({
        dataEntity: ORGANIZATION_DATA_ENTITY,
        fields,
        id,
      })

      sendUpdateOrganizationMetric(ctx, logger, {
        account: ctx.vtex.account,
        currentOrganizationData,
        updatedProperties: fields,
      })

      return { status: 'success', message: '' }
    } catch (error) {
      logger.error({
        error,
        message: 'updateOrganization-error',
      })
      throw new GraphQLError(getErrorMessage(error))
    }
  },

  updateOrganizationRequest: async (
    _: void,
    {
      id,
      status,
      notes,
      notifyUsers = true,
    }: { id: string; status: string; notes: string; notifyUsers: boolean },
    ctx: Context
  ) => {
    const {
      clients: { masterdata, mail, storefrontPermissions },
      vtex: { logger },
    } = ctx

    const settings = (await B2BSettings.getB2BSettings(
      undefined,
      undefined,
      ctx
    )) as B2BSettingsInput

    if (
      status !== ORGANIZATION_REQUEST_STATUSES.APPROVED &&
      status !== ORGANIZATION_REQUEST_STATUSES.DECLINED
    ) {
      throw new GraphQLError('Invalid status')
    }

    // create schema if it doesn't exist
    await checkConfig(ctx)

    let organizationRequest: OrganizationRequest

    try {
      // get organization request
      organizationRequest = await masterdata.getDocument({
        dataEntity: ORGANIZATION_REQUEST_DATA_ENTITY,
        fields: ORGANIZATION_REQUEST_FIELDS,
        id,
      })
    } catch (error) {
      logger.error({
        error,
        message: 'getOrganizationRequest-error',
      })
      throw new GraphQLError(getErrorMessage(error))
    }

    // don't allow update if status is already approved or declined
    if (organizationRequest.status !== ORGANIZATION_REQUEST_STATUSES.PENDING) {
      throw new GraphQLError('Organization request already processed')
    }

    const { email, firstName } = organizationRequest.b2bCustomerAdmin

    // if we reach this block, status is declined
    try {
      if (status === ORGANIZATION_REQUEST_STATUSES.APPROVED) {
        try {
          // the following copy is fine as organizationRequest does not contain fields that need transformation
          const normalizedOrganizationRequest = {
            ...organizationRequest,
          } as unknown as NormalizedOrganizationInput

          const { id: organizationId } =
            await createOrganizationAndCostCenterWithAdminUser(
              _,
              normalizedOrganizationRequest,
              ctx
            )

          if (
            notifyUsers &&
            settings?.transactionEmailSettings?.organizationApproved
          ) {
            message({
              logger,
              mail,
              storefrontPermissions,
            }).organizationApproved(
              organizationRequest.name,
              firstName,
              email,
              notes
            )
          }

          await masterdata.updatePartialDocument({
            dataEntity: ORGANIZATION_REQUEST_DATA_ENTITY,
            fields: { status, notes },
            id,
          })

          sendOrganizationStatusMetric(ctx, logger, {
            account: ctx.vtex.account,
            status: ORGANIZATION_REQUEST_STATUSES.APPROVED,
          })

          return { status: 'success', message: '', id: organizationId }
        } catch (e) {
          logger.error({
            error: e,
            message: 'updateOrganizationRequest-error',
          })
          if (e.message) {
            throw new GraphQLError(e.message)
          } else if (e.response?.data?.message) {
            throw new GraphQLError(e.response.data.message)
          } else {
            throw new GraphQLError(e)
          }
        }
      }

      // update request status to declined
      await masterdata.updatePartialDocument({
        dataEntity: ORGANIZATION_REQUEST_DATA_ENTITY,
        fields: { status, notes },
        id,
      })

      if (
        notifyUsers &&
        settings?.transactionEmailSettings?.organizationDeclined
      ) {
        message({ storefrontPermissions, logger, mail }).organizationDeclined(
          organizationRequest.name,
          firstName,
          email,
          notes
        )
      }

      sendOrganizationStatusMetric(ctx, logger, {
        account: ctx.vtex.account,
        status: ORGANIZATION_REQUEST_STATUSES.DECLINED,
      })

      return { status: 'success', message: '' }
    } catch (e) {
      throw new GraphQLError(getErrorMessage(e))
    }
  },
}

export default Organizations

```


[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-organizations-graphql/node/resolvers/Mutations/CostCenters.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import {
  COST_CENTER_DATA_ENTITY,
  ORGANIZATION_DATA_ENTITY,
} from '../../mdSchema'
import type {
  AddressInput,
  CostCenterInput,
  CostCenterInputWithId,
} from '../../typings'
import GraphQLError, { getErrorMessage } from '../../utils/GraphQLError'
import Organizations from '../Queries/Organizations'
import costCenters from '../Queries/CostCenters'
import checkConfig from '../config'
import CostCenterRepository from '../repository/CostCenterRepository'

const CostCenters = {
  createCostCenter: async (
    _: void,
    {
      organizationId,
      input: {
        id,
        name,
        addresses,
        phoneNumber,
        businessDocument,
        stateRegistration,
        customFields,
        marketingTags,
        sellers,
        paymentTerms,
      },
    }: { organizationId: string; input: CostCenterInput },
    ctx: Context
  ) => {
    const {
      vtex,
      vtex: { logger },
    } = ctx

    // create schema if it doesn't exist
    await checkConfig(ctx)

    if (!organizationId) {
      // get user's organization from session
      const { sessionData } = vtex as any

      if (!sessionData?.namespaces['storefront-permissions']) {
        throw new GraphQLError('organization-data-not-found')
      }

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

      organizationId = userOrganizationId
    }

    try {
      const costCenter: CostCenterInput = {
        addresses,
        businessDocument,
        customFields,
        marketingTags,
        id,
        name,
        phoneNumber,
        sellers,
        stateRegistration,
        paymentTerms,
      }

      return await CostCenterRepository.createCostCenter(
        _,
        organizationId,
        costCenter,
        ctx
      )
    } catch (error) {
      logger.error({
        error,
        message: 'createCostCenter-error',
      })
      throw new GraphQLError(getErrorMessage(error))
    }
  },

  createCostCenterWithId: async (
    _: void,
    {
      organizationId,
      input: {
        id,
        name,
        addresses,
        phoneNumber,
        businessDocument,
        stateRegistration,
        customFields,
        marketingTags,
        sellers,
        paymentTerms,
      },
    }: { organizationId: string; input: CostCenterInputWithId },
    ctx: Context
  ) => {
    // create schema if it doesn't exist
    await checkConfig(ctx)

    // check if organization exists
    const organization = (await Organizations.getOrganizationById(
      _,
      { id: organizationId },
      ctx
    )) as {
      status: string
    }

    if (!organization) {
      throw new Error('Organization not found')
    }

    // check if cost center id already exists
    let costCenter = null

    try {
      costCenter = await costCenters.getCostCenterById(_, { id }, ctx)
    } catch (error) {
      costCenter = null // cost center does not exist so we don't need to do anything
    }

    if (costCenter) {
      throw new Error('Cost Center already exists')
    }

    // create cost center
    const newCostCenter: CostCenterInput = {
      addresses,
      businessDocument,
      customFields,
      marketingTags,
      id,
      name,
      phoneNumber,
      sellers,
      stateRegistration,
      paymentTerms,
    }

    const { id: costCenterId } = await CostCenterRepository.createCostCenter(
      _,
      organizationId,
      newCostCenter,
      ctx
    )

    return { id: costCenterId }
  },

  createCostCenterAddress: async (
    _: void,
    { costCenterId, address }: { costCenterId: string; address: AddressInput },
    ctx: Context
  ) => {
    const {
      clients: { masterdata },
      vtex: { logger },
    } = ctx

    // create schema if it doesn't exist
    await checkConfig(ctx)

    try {
      const costCenter: CostCenterInput = await masterdata.getDocument({
        dataEntity: COST_CENTER_DATA_ENTITY,
        fields: ['addresses'],
        id: costCenterId,
      })

      const addresses = costCenter.addresses ?? []

      addresses.push(address)

      await masterdata.updatePartialDocument({
        dataEntity: COST_CENTER_DATA_ENTITY,
        fields: {
          addresses,
        },
        id: costCenterId,
      })

      return { status: 'success', message: '' }
    } catch (error) {
      logger.error({
        error,
        message: 'createCostCenterAddress-error',
      })
      throw new GraphQLError(getErrorMessage(error))
    }
  },

  deleteCostCenter: async (_: void, { id }: { id: string }, ctx: Context) => {
    const {
      clients: { masterdata },
    } = ctx

    try {
      await masterdata.deleteDocument({
        dataEntity: COST_CENTER_DATA_ENTITY,
        id,
      })

      return { status: 'success', message: '' }
    } catch (e) {
      throw new GraphQLError(getErrorMessage(e))
    }
  },

  deleteOrganization: async (_: void, { id }: { id: string }, ctx: Context) => {
    const {
      clients: { masterdata },
    } = ctx

    try {
      await masterdata.deleteDocument({
        dataEntity: ORGANIZATION_DATA_ENTITY,
        id,
      })

      return { status: 'success', message: '' }
    } catch (e) {
      throw new GraphQLError(getErrorMessage(e))
    }
  },

  updateCostCenter: async (
    _: void,
    {
      id,
      input: {
        name,
        addresses,
        paymentTerms,
        phoneNumber,
        businessDocument,
        stateRegistration,
        customFields,
      },
    }: { id: string; input: CostCenterInput },
    ctx: Context
  ) => {
    const {
      clients: { masterdata },
      vtex: { logger },
    } = ctx

    // create schema if it doesn't exist
    await checkConfig(ctx)

    try {
      await masterdata.updatePartialDocument({
        dataEntity: COST_CENTER_DATA_ENTITY,
        fields: {
          name,
          ...(addresses?.length && { addresses }),
          ...(paymentTerms?.length && { paymentTerms }),
          ...((phoneNumber || phoneNumber === '') && { phoneNumber }),
          ...((stateRegistration || stateRegistration === '') && {
            stateRegistration,
          }),
          ...((businessDocument || businessDocument === '') && {
            businessDocument,
          }),
          ...((customFields || customFields === '') && {
            customFields,
          }),
        },
        id,
      })

      return { status: 'success', message: '' }
    } catch (error) {
      logger.error({
        error,
        message: 'updateCostCenter-error',
      })
      throw new GraphQLError(getErrorMessage(error))
    }
  },

  updateCostCenterAddress: async (
    _: void,
    { costCenterId, address }: { costCenterId: string; address: AddressInput },
    ctx: Context
  ) => {
    const {
      clients: { masterdata },
      vtex: { logger },
    } = ctx

    // create schema if it doesn't exist
    await checkConfig(ctx)
    try {
      const costCenter: CostCenterInput = await masterdata.getDocument({
        dataEntity: COST_CENTER_DATA_ENTITY,
        fields: ['addresses'],
        id: costCenterId,
      })

      let addresses = costCenter.addresses ?? []

      addresses = addresses.map((current: AddressInput) => {
        if (address.addressId === current.addressId) {
          return address
        }

        return current
      })

      await masterdata.updatePartialDocument({
        dataEntity: COST_CENTER_DATA_ENTITY,
        fields: {
          addresses,
        },
        id: costCenterId,
      })

      return { status: 'success', message: '' }
    } catch (error) {
      logger.error({
        error,
        message: 'updateCostCenterAddress-error',
      })
      throw new GraphQLError(getErrorMessage(error))
    }
  },
}

export default CostCenters

```


[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-organizations-graphql/node/resolvers/Mutations/Users.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { MessageSFPUserAddError, StatusAddUserError } from '../../constants'
import GraphQLError, { getErrorMessage } from '../../utils/GraphQLError'
import type { ImpersonateMetricParams } from '../../utils/metrics/impersonate'
import {
  sendImpersonateB2BUserMetric,
  sendImpersonateUserMetric,
} from '../../utils/metrics/impersonate'
import {
  sendAddUserMetric,
  sendRemoveUserMetric,
  sendUpdateUserMetric,
} from '../../utils/metrics/user'
import type { UserArgs } from '../../typings'

export const getUserRoleSlug: (
  id: string,
  ctx: Context
) => Promise<string> = async (id, ctx) => {
  const {
    clients: { storefrontPermissions },
    vtex: { logger },
  } = ctx

  return storefrontPermissions
    .getUser(id)
    .then((result: any) => {
      return result.data.getUser
    })
    .then((userData: any) => {
      return storefrontPermissions.getRole(userData.roleId)
    })
    .then((result: any) => {
      return result?.data?.getRole?.slug ?? ''
    })
    .catch((error: any) => {
      logger.warn({
        error,
        message: 'getUserRoleSlug-error',
      })

      return ''
    })
}

const isUserPermitted = ({
  storefrontPermissions,
  sessionData,
  orgId,
  roleSlug,
}: any) =>
  (storefrontPermissions?.permissions?.includes('add-users-organization') &&
    sessionData.namespaces['storefront-permissions']?.organization?.value ===
      orgId &&
    !roleSlug.includes('sales')) ||
  (storefrontPermissions?.permissions?.includes('add-sales-users-all') &&
    roleSlug.includes('sales'))

const getRoleSlug = async ({
  clId,
  ctx,
  logger,
  roleId,
  storefrontPermissionsClient,
}: any) =>
  clId
    ? getUserRoleSlug(clId, ctx)
    : storefrontPermissionsClient
        .getRole(roleId)
        .then((result: any) => {
          return result?.data?.getRole?.slug ?? ''
        })
        .catch((error: any) => {
          logger.warn({
            error,
            message: 'saveUser-getRoleError',
          })

          return ''
        })

const getUser = async ({ masterdata, logger, userId, clId }: any) =>
  (await masterdata
    .getDocument({
      dataEntity: 'CL',
      fields: ['userId'],
      id: clId,
    })
    .then((res: any) => {
      return res?.userId ?? undefined
    })
    .catch((error: any) => {
      logger.error({
        error,
        message: 'saveUser-getDocumentError',
      })

      return error
    })) ?? userId

const checkUserIsAllowed = async ({
  adminUserAuthToken,
  clId,
  ctx,
  logger,
  orgId,
  roleId,
  sessionData,
  storefrontPermissionsClient,
  storefrontPermissions,
}: any) => {
  if (!adminUserAuthToken) {
    if (!sessionData?.namespaces['storefront-permissions']?.organization) {
      throw new GraphQLError('organization-data-not-found')
    }

    const roleSlug = await getRoleSlug({
      clId,
      ctx,
      logger,
      roleId,
      storefrontPermissionsClient,
    })

    const permitted = isUserPermitted({
      orgId,
      roleSlug,
      sessionData,
      storefrontPermissions,
    })

    if (!permitted) {
      throw new GraphQLError('operation-not-permitted')
    }
  }
}

const getUserFromStorefrontPermissions = ({
  clId: clientId,
  storefrontPermissionsClient,
  logger,
}: {
  clId: string
  storefrontPermissionsClient: any
  logger: any
}) =>
  storefrontPermissionsClient
    .getUser(clientId)
    .then((result: any) => {
      return result?.data?.getUser ?? {}
    })
    .catch((error: any) => {
      logger.warn({
        error,
        message: 'impersonateUser-getUserError',
      })

      return error
    })

const Users = {
  impersonateB2BUser: async (_: void, { id }: { id: string }, ctx: Context) => {
    const {
      clients: { storefrontPermissions: storefrontPermissionsClient },

      vtex: { adminUserAuthToken, logger, sessionData, storefrontPermissions },
    } = ctx as Context | any

    const getB2BUserFromStorefrontPermissions = ({
      id: b2bId,
    }: {
      id: string
    }) =>
      storefrontPermissionsClient
        .getB2BUser(b2bId)
        .then((result: any) => result?.data?.getB2BUser ?? {})

    const user = await getB2BUserFromStorefrontPermissions({ id })

    if (!adminUserAuthToken) {
      if (!sessionData?.namespaces['storefront-permissions']?.organization) {
        throw new GraphQLError('organization-data-not-found')
      }

      const canImpersonateUsersWithCostCenterPermission =
        storefrontPermissions?.permissions?.includes(
          'impersonate-users-costcenter'
        ) &&
        user?.costId ===
          sessionData?.namespaces['storefront-permissions']?.costcenter?.value

      const canImpersonateUsersWithOrganizationPermission =
        storefrontPermissions?.permissions?.includes(
          'impersonate-users-organization'
        ) &&
        user?.orgId ===
          sessionData?.namespaces['storefront-permissions']?.organization?.value

      const canImpersonateUsersWithAllPermission =
        storefrontPermissions?.permissions?.includes('impersonate-users-all')

      if (
        !canImpersonateUsersWithCostCenterPermission &&
        !canImpersonateUsersWithOrganizationPermission &&
        !canImpersonateUsersWithAllPermission
      ) {
        throw new GraphQLError('operation-not-permitted')
      }
    }

    const impersonation = await storefrontPermissionsClient
      .impersonateUser({ userId: id })
      .catch((error: any) => {
        logger.error({
          error,
          message: 'impersonateUser-impersonateUserError',
        })

        return { status: 'error', message: error }
      })

    if (impersonation?.errors) {
      throw new GraphQLError(impersonation?.errors)
    }

    const {
      orgId: targetOrganizationId,
      costId: targetCostCenterId,
      id: targetId,
      email: targetEmail,
    } = user

    const metricParams: ImpersonateMetricParams = {
      account: sessionData?.namespaces?.account?.accountName,
      target: {
        costCenterId: targetCostCenterId,
        organizationId: targetOrganizationId,
        email: targetEmail,
        id: targetId,
      },
      user: {
        costCenterId:
          sessionData?.namespaces['storefront-permissions']?.costcenter.value,
        organizationId:
          sessionData?.namespaces['storefront-permissions']?.organization.value,
        email: sessionData?.namespaces?.profile?.email.value,
        id: sessionData?.namespaces['storefront-permissions']?.userId.value,
      },
    }

    sendImpersonateB2BUserMetric(ctx, metricParams)

    return (
      impersonation?.data?.impersonateUser ?? {
        status: 'error',
      }
    )
  },
  /**
   *
   * Mutation to impersonate a user
   *
   * @param _
   * @param clId
   * @param userId
   * @param ctx
   */
  impersonateUser: async (
    _: void,
    { clId, userId }: UserArgs,
    ctx: Context
  ) => {
    const {
      clients: {
        masterdata,
        storefrontPermissions: storefrontPermissionsClient,
      },

      vtex: { adminUserAuthToken, logger, sessionData, storefrontPermissions },
    } = ctx as Context | any

    if (!adminUserAuthToken && clId) {
      if (!sessionData?.namespaces['storefront-permissions']?.organization) {
        throw new GraphQLError('organization-data-not-found')
      }

      const userInfo = await getUserFromStorefrontPermissions({
        clId,
        logger,
        storefrontPermissionsClient,
      })

      const canImpersonateUsersWithCostCenterPermission =
        storefrontPermissions?.permissions?.includes(
          'impersonate-users-costcenter'
        ) &&
        userInfo?.costId ===
          sessionData?.namespaces['storefront-permissions']?.costcenter?.value

      const canImpersonateUsersWithOrganizationPermission =
        storefrontPermissions?.permissions?.includes(
          'impersonate-users-organization'
        ) &&
        userInfo?.orgId ===
          sessionData?.namespaces['storefront-permissions']?.organization?.value

      const canImpersonateUsersWithAllPermission =
        storefrontPermissions?.permissions?.includes('impersonate-users-all')

      if (
        !canImpersonateUsersWithCostCenterPermission &&
        !canImpersonateUsersWithOrganizationPermission &&
        !canImpersonateUsersWithAllPermission
      ) {
        throw new GraphQLError('operation-not-permitted')
      }
    }

    if (!userId && clId) {
      const userIdFromCl = await masterdata
        .getDocument({
          dataEntity: 'CL',
          fields: ['userId'],
          id: clId,
        })
        .then((res: any) => {
          return res?.userId ?? undefined
        })
        .catch((error: any) => {
          logger.warn({
            error,
            message: 'impersonateUser-getUserIdError',
          })

          return error
        })

      if (!userIdFromCl) {
        logger.warn({
          message: `userId ${userIdFromCl} not found in CL`,
        })

        return { status: 'error', message: 'userId not found in CL' }
      }

      userId = userIdFromCl

      const userData = await getUserFromStorefrontPermissions({
        clId,
        logger,
        storefrontPermissionsClient,
      })

      if (userData && !userData.userId) {
        await storefrontPermissionsClient.saveUser({
          ...userData,
          userId,
        })
      }
    }

    return impersonateUser(
      ctx,
      storefrontPermissionsClient,
      userId,
      logger,
      sessionData?.namespaces?.account?.accountName,
      sessionData?.namespaces['storefront-permissions']
    )
  },

  removeUserWithEmail: async (
    _: void,
    { orgId, costId, email }: UserArgs,
    ctx: Context
  ) => {
    const {
      clients: { events, storefrontPermissions: storefrontPermissionsClient },
      vtex: { logger },
    } = ctx as any

    return storefrontPermissionsClient
      .getUsersByEmail(email, orgId, costId)
      .then((result: any) => {
        const user = result.data.getUsersByEmail[0]

        if (!user) {
          logger.error({ message: 'User not found' })

          return { status: 'error', message: 'User not found' }
        }

        const { id } = user
        const { userId } = user

        const fields = {
          email,
          id,
          userId,
        }

        return storefrontPermissionsClient
          .deleteUser(fields)
          .then((response: any) => {
            events.sendEvent('', 'b2b-organizations-graphql.removeUser', {
              id,
              email,
            })
            sendRemoveUserMetric(ctx, logger, ctx.vtex.account, fields)

            return response.data.deleteUser
          })
          .catch((error: any) => {
            logger.error({
              error,
              message: 'removeUser-deleteUserError',
            })

            return { status: 'error', message: error }
          })
      })
      .catch((error: any) => {
        logger.error({
          error,
          message: 'getUsers-error',
        })
        throw new GraphQLError(getErrorMessage(error))
      })
  },

  /**
   *
   * Mutation to remove a user
   *
   * @param _
   * @param id
   * @param userId
   * @param email
   * @param clId
   * @param ctx
   */
  removeUser: async (
    _: void,
    { id, userId, email, clId }: UserArgs,
    ctx: Context
  ) => {
    const {
      clients: { events, storefrontPermissions: storefrontPermissionsClient },
      vtex: { adminUserAuthToken, logger, sessionData, storefrontPermissions },
    } = ctx as Context | any

    if (!id || !clId || !email) {
      throw new GraphQLError('user-information-not-provided')
    }

    if (!adminUserAuthToken) {
      if (!sessionData?.namespaces['storefront-permissions']?.organization) {
        throw new GraphQLError('organization-data-not-found')
      }

      const roleSlug = await getUserRoleSlug(clId, ctx)

      const permitted =
        (storefrontPermissions?.permissions?.includes(
          'remove-users-organization'
        ) &&
          !roleSlug.includes('sales')) ||
        storefrontPermissions?.permissions?.includes('remove-sales-users-all')

      if (!permitted) {
        throw new GraphQLError('operation-not-permitted')
      }
    }

    const fields = {
      email,
      id,
      userId,
    }

    return storefrontPermissionsClient
      .deleteUser(fields)
      .then((result: any) => {
        events.sendEvent('', 'b2b-organizations-graphql.removeUser', {
          id,
          email,
        })

        sendRemoveUserMetric(ctx, logger, ctx.vtex.account, fields)

        return result.data.deleteUser
      })
      .catch((error: any) => {
        logger.error({
          error,
          message: 'removeUser-deleteUserError',
        })

        return { status: 'error', message: error }
      })
  },

  addUser: async (
    _: void,
    { id, roleId, userId, orgId, costId, clId, name, email }: UserArgs,
    ctx: Context
  ) => {
    const {
      clients: { storefrontPermissions: storefrontPermissionsClient },
      vtex: { adminUserAuthToken, logger, sessionData, storefrontPermissions },
    } = ctx as any

    try {
      await checkUserIsAllowed({
        adminUserAuthToken,
        clId,
        ctx,
        logger,
        orgId,
        roleId,
        sessionData,
        storefrontPermissions,
        storefrontPermissionsClient,
      })
    } catch (error) {
      logger.error({
        error,
        message: 'addUser-checkUserIsAllowedError',
      })
      throw error
    }

    const fields = {
      costId,
      email,
      id,
      name,
      orgId,
      roleId,
      userId,
    }

    return storefrontPermissionsClient
      .addUser(fields)
      .then((result: any) => {
        sendAddUserMetric(ctx, logger, ctx.vtex.account, fields)

        return result.data.addUser
      })
      .catch((error: any) => {
        logger.error({
          error,
          message: 'addUser-error',
        })

        const message = error.graphQLErrors[0]?.message ?? error.message
        let status = ''

        if (message.includes(MessageSFPUserAddError.DUPLICATED)) {
          status = StatusAddUserError.DUPLICATED
        } else if (
          message.includes(MessageSFPUserAddError.DUPLICATED_ORGANIZATION)
        ) {
          status = StatusAddUserError.DUPLICATED_ORGANIZATION
        } else {
          status = StatusAddUserError.ERROR
        }

        return { status, message }
      })
  },

  createUserWithEmail: async (
    _: void,
    { orgId, costId, roleId, name, email, canImpersonate }: UserArgs,
    ctx: Context
  ) => {
    const {
      clients: { storefrontPermissions: storefrontPermissionsClient },
      vtex: { logger },
    } = ctx as any

    try {
      const result = await storefrontPermissionsClient.addUser({
        orgId,
        costId,
        roleId,
        name,
        email,
        canImpersonate,
      })

      return result.data.addUser
    } catch (error) {
      logger.error({
        error,
        message: 'createUserWithEmail-error',
      })
      const message = error.graphQLErrors?.[0]?.message ?? error.message
      let status = ''

      if (message.includes(MessageSFPUserAddError.DUPLICATED)) {
        status = StatusAddUserError.DUPLICATED
      } else if (
        message.includes(MessageSFPUserAddError.DUPLICATED_ORGANIZATION)
      ) {
        status = StatusAddUserError.DUPLICATED_ORGANIZATION
      } else {
        status = StatusAddUserError.ERROR
      }

      return { status, message }
    }
  },

  updateUser: async (
    _: void,
    { id, roleId, userId, orgId, costId, clId, name, email }: UserArgs,
    ctx: Context
  ) => {
    const {
      clients: {
        masterdata,
        storefrontPermissions: storefrontPermissionsClient,
      },
      vtex: { adminUserAuthToken, logger, sessionData, storefrontPermissions },
    } = ctx as any

    try {
      await checkUserIsAllowed({
        adminUserAuthToken,
        clId,
        ctx,
        logger,
        orgId,
        roleId,
        sessionData,
        storefrontPermissions,
        storefrontPermissionsClient,
      })
    } catch (error) {
      logger.error({
        error,
        message: 'addUser-checkUserIsAllowedError',
      })
      throw error
    }

    if (clId && !userId) {
      userId = await getUser({
        clId,
        logger,
        masterdata,
        userId,
      })
    }

    const fields = {
      clId,
      costId,
      email,
      id,
      name,
      orgId,
      roleId,
      userId,
    }

    return storefrontPermissionsClient
      .updateUser(fields)
      .then((result: any) => {
        sendUpdateUserMetric(ctx, logger, ctx.vtex.account, fields)

        return result.data.updateUser
      })
      .catch((error: any) => {
        logger.error({
          error,
          message: 'updateUser-error',
        })

        return { status: 'error', message: error }
      })
  },

  /**
   *
   * Create and update a user
   *
   * @deprecated
   *
   * @param _
   * @param id
   * @param roleId
   * @param userId
   * @param orgId
   * @param costId
   * @param clId
   * @param name
   * @param email
   * @param ctx
   */
  saveUser: async (
    _: void,
    { id, roleId, userId, orgId, costId, clId, name, email }: UserArgs,
    ctx: Context
  ) => {
    const {
      clients: {
        masterdata,
        storefrontPermissions: storefrontPermissionsClient,
      },
      vtex: { adminUserAuthToken, logger, sessionData, storefrontPermissions },
    } = ctx as any

    try {
      await checkUserIsAllowed({
        adminUserAuthToken,
        clId,
        ctx,
        logger,
        orgId,
        roleId,
        sessionData,
        storefrontPermissions,
        storefrontPermissionsClient,
      })
    } catch (error) {
      logger.error({
        error,
        message: 'addUser-checkUserIsAllowedError',
      })
      throw error
    }

    if (clId && !userId) {
      userId = await getUser({
        clId,
        logger,
        masterdata,
        userId,
      })
    }

    return storefrontPermissionsClient
      .saveUser({
        clId,
        costId,
        email,
        id,
        name,
        orgId,
        roleId,
        userId,
      })
      .then((result: any) => {
        return result.data.saveUser
      })
      .catch((error: any) => {
        logger.error({
          error,
          message: 'addUser-error',
        })

        return { status: 'error', message: error }
      })
  },
}

export default Users

async function impersonateUser(
  ctx: Context,
  storefrontPermissionsClient: any,
  userId: string | undefined,
  logger: any,
  accountName: string,
  storefrontPermissions: {
    costcenter: { value: string }
    userId: { value: string }
    organization: { value: string }
    storeUserEmail: { value: string }
  }
) {
  return storefrontPermissionsClient
    .impersonateUser({ userId })
    .then((_: any) => {
      const {
        costcenter,
        userId: userTargetId,
        organization,
        storeUserEmail,
      } = storefrontPermissions

      const metricParams: ImpersonateMetricParams = {
        account: accountName,
        target: {
          costCenterId: costcenter.value,
          organizationId: organization.value,
          email: storeUserEmail.value,
          id: userTargetId.value,
        },
        user: undefined, // no information about original user at this point
      }

      sendImpersonateUserMetric(ctx, metricParams)
    })
    .catch((error: any) => {
      logger.error({
        error,
        message: 'impersonateUser-impersonateUserError',
      })

      return { status: 'error', message: error }
    })
}

```


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

[User] Show me the implementation of b2b-organizations-graphql/node/resolvers/Mutations/MarketingTags.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { MARKETING_TAGS } from '../../utils/constants'

const MarketingTags = {
  setMarketingTags: async (
    _: void,
    { costId, tags }: { costId: string; tags: string[] },
    ctx: Context
  ) => {
    const {
      clients: { vbase },
      vtex: { logger },
    } = ctx

    if (!costId || !tags) {
      throw new Error('Invalid parameters')
    }

    try {
      await vbase.saveJSON(MARKETING_TAGS.VBASE_BUCKET, costId, {
        tags,
      })

      return { status: 'success', message: '', id: costId }
    } catch (error) {
      logger.error({
        error,
        message: 'setMarketingTags.error',
      })

      return { status: 'error', message: error }
    }
  },
}

export default MarketingTags

```


[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-organizations-graphql/node/resolvers/Queries/Settings.ts from the b2b-organizations-graphql VTEX app.

[Assistant]
Here's the implementation:

```typescript
import GraphQLError from '../../utils/GraphQLError'
import checkConfig from '../config'
import type { B2BSettingsInput } from '../../typings'
import type { GetSellersOpts } from '../../clients/sellers'

const B2BSettings = {
  getB2BSettings: async (_: void, __: void, ctx: Context) => {
    const {
      clients: { vbase },
    } = ctx

    const B2B_SETTINGS_DATA_ENTITY = 'b2b_settings'

    // create schema if it doesn't exist
    await checkConfig(ctx)

    let settings: Partial<B2BSettingsInput> | null = null

    try {
      settings = await vbase.getJSON<B2BSettingsInput | null>(
        B2B_SETTINGS_DATA_ENTITY,
        'settings',
        true
      )

      settings = {
        ...settings,
        // if custom fields are null, set to an empty array
        costCenterCustomFields: settings?.costCenterCustomFields ?? [],
        organizationCustomFields: settings?.organizationCustomFields ?? [],
        transactionEmailSettings: settings?.transactionEmailSettings ?? {
          organizationApproved: true,
          organizationCreated: true,
          organizationDeclined: true,
          organizationRequestCreated: false,
          organizationStatusChanged: true,
        },
      }
    } catch (e) {
      if (e.message) {
        throw new GraphQLError(e.message)
      } else if (e.response?.data?.message) {
        throw new GraphQLError(e.response.data.message)
      } else {
        throw new GraphQLError(e)
      }
    }

    return settings
  },
  getSellers: async (_: void, __: void, ctx: Context) => {
    const {
      clients: { sellers },
    } = ctx

    return (await sellers.getSellers())?.items
  },
  getSellersPaginated: async (
    _: void,
    options: GetSellersOpts,
    ctx: Context
  ) => {
    const {
      clients: { sellers },
    } = ctx

    try {
      return await sellers.getSellersPaginated(options)
    } catch (e) {
      if (e.message) {
        throw new GraphQLError(e.message)
      } else if (e.response?.data?.message) {
        throw new GraphQLError(e.response.data.message)
      } else {
        throw new GraphQLError(e)
      }
    }
  },
  getAccount: async (_: void, __: void, ctx: Context) => {
    const {
      clients: { lm },
    } = ctx

    return lm.getAccount().catch((e) => {
      if (e.message) {
        throw new GraphQLError(e.message)
      } else if (e.response?.data?.message) {
        throw new GraphQLError(e.response.data.message)
      } else {
        throw new GraphQLError(e)
      }
    })
  },
}

export default B2BSettings

```