diff --git a/CHANGELOG.md b/CHANGELOG.md index 28772b67..80a83a89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Add metrics on updating organization data + ## [0.37.0] - 2023-09-19 diff --git a/node/package.json b/node/package.json index 9f2ca670..6c26f855 100644 --- a/node/package.json +++ b/node/package.json @@ -2,6 +2,7 @@ "name": "vtex.b2b-organizations", "version": "0.37.0", "dependencies": { + "@types/lodash": "4.14.74", "@vtex/api": "6.45.20", "atob": "^2.1.2", "co-body": "^6.0.0", @@ -29,8 +30,7 @@ "tslint-config-prettier": "^1.18.0", "tslint-config-vtex": "^2.1.0", "typescript": "3.9.7", - "vtex.b2b-organizations-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.b2b-organizations-graphql@0.36.0/public/@types/vtex.b2b-organizations-graphql", - "vtex.storefront-permissions": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.storefront-permissions@1.36.0/public/@types/vtex.storefront-permissions" + "vtex.storefront-permissions": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.storefront-permissions@1.35.3/public/@types/vtex.storefront-permissions" }, "scripts": { "lint": "tsc --noEmit && tslint -c tslint.json './**/*.ts'", diff --git a/node/resolvers/Mutations/Organizations.ts b/node/resolvers/Mutations/Organizations.ts index b5ca04e9..d3014641 100644 --- a/node/resolvers/Mutations/Organizations.ts +++ b/node/resolvers/Mutations/Organizations.ts @@ -18,6 +18,7 @@ import B2BSettings from '../Queries/Settings' import CostCenters from './CostCenters' import MarketingTags from './MarketingTags' import checkConfig from '../config' +import { sendUpdateOrganizationMetric } from '../../utils/metrics/organization' const createUserAndAttachToOrganization = async ({ storefrontPermissions, @@ -372,15 +373,17 @@ const Organizations = { ctx )) as B2BSettingsInput + let currentOrganizationData: Organization | undefined + try { - const currentData: Organization = await masterdata.getDocument({ + currentOrganizationData = await masterdata.getDocument({ dataEntity: ORGANIZATION_DATA_ENTITY, fields: ORGANIZATION_FIELDS, id, }) if ( - currentData.status !== status && + currentOrganizationData?.status !== status && notifyUsers && settings?.transactionEmailSettings?.organizationStatusChanged ) { @@ -398,22 +401,30 @@ const Organizations = { } try { + const fields = { + collections, + ...((tradeName || tradeName === '') && { tradeName }), + customFields, + name, + paymentTerms, + priceTables, + ...(salesChannel && { salesChannel }), + ...(sellers && { sellers }), + status, + } + await masterdata.updatePartialDocument({ dataEntity: ORGANIZATION_DATA_ENTITY, - fields: { - collections, - ...((tradeName || tradeName === '') && { tradeName }), - customFields, - name, - paymentTerms, - priceTables, - ...(salesChannel && { salesChannel }), - ...(sellers && { sellers }), - status, - }, + fields, id, }) + sendUpdateOrganizationMetric(logger, { + account: ctx.vtex.account, + currentOrganizationData, + updatedProperties: fields, + }) + return { status: 'success', message: '' } } catch (error) { logger.error({ diff --git a/node/typings.d.ts b/node/typings.d.ts index 0c2974c9..693daefa 100644 --- a/node/typings.d.ts +++ b/node/typings.d.ts @@ -119,11 +119,20 @@ interface Organization { id: string name: string tradeName?: string + collections: Collection[] costCenters: string[] paymentTerms: PaymentTerm[] + priceTables?: string[] status: string created: string customFields?: CustomField[] + salesChannel?: string + sellers?: Seller[] +} + +interface Collection { + id: string + name: string } interface CostCenter { diff --git a/node/utils/metrics/metrics.ts b/node/utils/metrics/metrics.ts index 55d5c0fe..fdeb17f1 100644 --- a/node/utils/metrics/metrics.ts +++ b/node/utils/metrics/metrics.ts @@ -12,10 +12,19 @@ type ImpersonateB2BUserMetric = { description: 'Impersonate B2B User Action - Graphql' } +interface UpdateOrganizationMetric { + kind: 'update-organization-graphql-event' + description: 'Update Organization Action - Graphql' +} + export type Metric = { name: 'b2b-suite-buyerorg-data' account: string -} & (ImpersonateUserMetric | ImpersonateB2BUserMetric) +} & ( + | ImpersonateUserMetric + | ImpersonateB2BUserMetric + | UpdateOrganizationMetric +) export const sendMetric = async (metric: Metric) => { try { diff --git a/node/utils/metrics/organization.test.ts b/node/utils/metrics/organization.test.ts new file mode 100644 index 00000000..5c3dda33 --- /dev/null +++ b/node/utils/metrics/organization.test.ts @@ -0,0 +1,223 @@ +import { + randAirportName, + randAlpha, + randAlphaNumeric, + randCompanyName, + randFirstName, + randFullName, + randLastName, + randPastDate, + randStatus, + randSuperheroName, + randWord, +} from '@ngneat/falso' +import type { Logger } from '@vtex/api/lib/service/logger/logger' + +import type { Seller } from '../../clients/sellers' +import { sendMetric } from './metrics' +import type { UpdateOrganizationParams } from './organization' +import { sendUpdateOrganizationMetric } from './organization' + +jest.mock('./metrics') +afterEach(() => { + jest.resetAllMocks() +}) + +describe('given an organization to update data', () => { + describe('when change all properties', () => { + const logger = jest.fn() as unknown as Logger + + const account = randWord() + + const currentOrganization: Organization = { + collections: [{ name: randWord() } as Collection], + costCenters: [], + created: randPastDate().toISOString(), + customFields: [{ name: randWord() } as CustomField], + id: randAlphaNumeric().toString(), + name: randCompanyName(), + paymentTerms: [{ name: randAlphaNumeric() } as PaymentTerm], + priceTables: [randAlpha()], + salesChannel: randAlpha(), + sellers: [{ name: randFullName() } as Seller], + status: randAlpha(), + tradeName: randCompanyName(), + } + + const fieldsUpdated: Partial = { + collections: [{ name: randSuperheroName() } as Collection], + costCenters: [], + customFields: [{ name: randAirportName() } as CustomField], + name: randCompanyName(), + paymentTerms: [{ name: randLastName() } as PaymentTerm], + priceTables: [randFirstName()], + salesChannel: randAirportName(), + sellers: [{ name: randFullName() } as Seller], + status: randStatus(), + tradeName: randCompanyName(), + } + + const updateOrganizationParams: UpdateOrganizationParams = { + account, + currentOrganizationData: currentOrganization, + updatedProperties: fieldsUpdated, + } + + beforeEach(async () => { + await sendUpdateOrganizationMetric(logger, updateOrganizationParams) + }) + + it('should metrify that all properties changed', () => { + const metricParam = { + account, + description: 'Update Organization Action - Graphql', + fields: { + update_details: { + properties: [ + 'collections', + 'customFields', + 'name', + 'paymentTerms', + 'priceTables', + 'salesChannel', + 'sellers', + 'status', + 'tradeName', + ], + }, + }, + kind: 'update-organization-graphql-event', + name: 'b2b-suite-buyerorg-data', + } + + expect(sendMetric).toHaveBeenCalledWith(metricParam) + }) + }) + + describe('when no change properties data', () => { + const logger = jest.fn() as unknown as Logger + + const account = randWord() + + const collections = [{ name: randWord() } as Collection] + const customFields = [{ name: randWord() } as CustomField] + const name = randWord() + const paymentTerms = [{ name: randWord() } as PaymentTerm] + const priceTables = [randWord()] + const salesChannel = randWord() + const sellers = [{ name: randFullName() } as Seller] + const status = randWord() + const tradeName = randWord() + + const currentOrganization: Organization = { + collections, + costCenters: [], + created: randWord(), + customFields, + id: randWord(), + name, + paymentTerms, + priceTables, + salesChannel, + sellers, + status, + tradeName, + } + + const fieldsUpdated = { + collections, + customFields, + name, + paymentTerms, + priceTables, + salesChannel, + sellers, + status, + tradeName, + } + + const updateOrganizationParams: UpdateOrganizationParams = { + account, + currentOrganizationData: currentOrganization, + updatedProperties: fieldsUpdated, + } + + beforeEach(async () => { + await sendUpdateOrganizationMetric(logger, updateOrganizationParams) + }) + + it('should metric no properties changed', () => { + const metricParam = { + account, + description: 'Update Organization Action - Graphql', + fields: { + update_details: { + properties: [], + }, + }, + kind: 'update-organization-graphql-event', + name: 'b2b-suite-buyerorg-data', + } + + expect(sendMetric).toHaveBeenCalledWith(metricParam) + }) + }) + describe('when just the name, status and tradeName', () => { + const logger = jest.fn() as unknown as Logger + + const account = randWord() + + const currentOrganization: Organization = { + collections: [{ id: '149', name: 'Teste 2 Jay' }], + costCenters: [], + created: '2023-05-26T17:59:51.665Z', + customFields: [], + id: '166d3921-fbef-11ed-83ab-16759f4a0add', + name: 'Antes', + paymentTerms: [], + priceTables: [], + salesChannel: '1', + sellers: [], + status: 'inactive', + tradeName: 'Antes', + } + + const fieldsUpdated = { + collections: [{ id: '149', name: 'Teste 2 Jay' }], + customFields: [], + name: 'Depois', + paymentTerms: [], + priceTables: [], + salesChannel: '1', + sellers: [], + status: 'active', + tradeName: 'Depois', + } + + const updateOrganizationParams: UpdateOrganizationParams = { + account, + currentOrganizationData: currentOrganization, + updatedProperties: fieldsUpdated, + } + + beforeEach(async () => { + await sendUpdateOrganizationMetric(logger, updateOrganizationParams) + }) + + it('should metric just the properties changed', () => { + const metricParam = { + account, + description: 'Update Organization Action - Graphql', + fields: { + update_details: { + properties: ['name', 'status', 'tradeName'], + }, + }, + kind: 'update-organization-graphql-event', + name: 'b2b-suite-buyerorg-data', + } + + expect(sendMetric).toHaveBeenCalledWith(metricParam) + }) + }) +}) diff --git a/node/utils/metrics/organization.ts b/node/utils/metrics/organization.ts new file mode 100644 index 00000000..71f06fc9 --- /dev/null +++ b/node/utils/metrics/organization.ts @@ -0,0 +1,87 @@ +import type { Logger } from '@vtex/api/lib/service/logger/logger' +import { isEqual } from 'lodash' + +import type { Metric } from './metrics' +import { sendMetric } from './metrics' + +interface UpdateOrganizationFieldsMetric { + update_details: { properties: string[] } +} + +type UpdateOrganization = Metric & { + fields: UpdateOrganizationFieldsMetric +} + +export interface UpdateOrganizationParams { + account: string + currentOrganizationData?: Organization + updatedProperties: Partial +} + +const buildUpdateOrganizationMetric = ( + account: string, + updatedProperties: string[] +): UpdateOrganization => { + const updateOrganizationFields: UpdateOrganizationFieldsMetric = { + update_details: { properties: updatedProperties }, + } + + return { + account, + description: 'Update Organization Action - Graphql', + fields: updateOrganizationFields, + kind: 'update-organization-graphql-event', + name: 'b2b-suite-buyerorg-data', + } as UpdateOrganization +} + +const getPropNamesByUpdateParams = ( + updateOrganizationParams: UpdateOrganizationParams +): string[] => { + const updatedPropName: string[] = [] + + const { currentOrganizationData, updatedProperties } = + updateOrganizationParams + + if (!currentOrganizationData) { + return updatedPropName + } + + Object.entries(updatedProperties).forEach( + ([updatedPropertyKey, updatedPropertyValue]) => { + if ( + !isEqual( + updatedPropertyValue, + currentOrganizationData[updatedPropertyKey as keyof Organization] + ) + ) { + updatedPropName.push(updatedPropertyKey) + } + } + ) + + return updatedPropName +} + +export const sendUpdateOrganizationMetric = async ( + logger: Logger, + updateOrganizationParams: UpdateOrganizationParams +) => { + try { + const fieldsNamesUpdated = getPropNamesByUpdateParams( + updateOrganizationParams + ) + + const metric = buildUpdateOrganizationMetric( + updateOrganizationParams.account, + fieldsNamesUpdated + ) + + await sendMetric(metric) + } catch (error) { + logger.error({ + error, + message: 'Error to send metrics from updateOrganization', + }) + } +} diff --git a/node/yarn.lock b/node/yarn.lock index d414a8f7..b45090b8 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -772,6 +772,11 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/lodash@4.14.74": + version "4.14.74" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.74.tgz#ac3bd8db988e7f7038e5d22bd76a7ba13f876168" + integrity sha512-BZknw3E/z3JmCLqQVANcR17okqVTPZdlxvcIz0fJiJVLUCbSH1hK3zs9r634PVSmrzAxN+n/fxlVRiYoArdOIQ== + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -3470,7 +3475,7 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" -"stats-lite@github:vtex/node-stats-lite#dist": +stats-lite@vtex/node-stats-lite#dist: version "2.2.0" resolved "https://codeload.github.com/vtex/node-stats-lite/tar.gz/1b0d39cc41ef7aaecfd541191f877887a2044797" dependencies: @@ -3909,13 +3914,9 @@ vary@^1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -"vtex.b2b-organizations-graphql@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.b2b-organizations-graphql@0.36.0/public/@types/vtex.b2b-organizations-graphql": - version "0.36.0" - resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.b2b-organizations-graphql@0.36.0/public/@types/vtex.b2b-organizations-graphql#14adc111841e871d38f888cb1c90ae4dacf1914e" - -"vtex.storefront-permissions@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.storefront-permissions@1.36.0/public/@types/vtex.storefront-permissions": - version "1.36.0" - resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.storefront-permissions@1.36.0/public/@types/vtex.storefront-permissions#9b6eca8b176bd2b4a349932a598b6b8691bd6fe9" +"vtex.storefront-permissions@http://vtex.vtexassets.com/_v/public/typings/v1/vtex.storefront-permissions@1.35.3/public/@types/vtex.storefront-permissions": + version "1.35.3" + resolved "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.storefront-permissions@1.35.3/public/@types/vtex.storefront-permissions#58d27d180d75da71e43c6d6059f7191002c5f06b" w3c-hr-time@^1.0.2: version "1.0.2"