From 344c77f558758bcc09cf91aaff6d7fa692ceffae Mon Sep 17 00:00:00 2001 From: Bruna Santos Date: Tue, 26 Nov 2024 14:09:33 -0300 Subject: [PATCH 01/20] feat: add new markeplace splitting quotes on graphql --- CHANGELOG.md | 3 +++ graphql/appSettings.graphql | 6 ++++++ node/resolvers/mutations/index.ts | 29 +++++++++++++++-------------- node/resolvers/queries/index.ts | 8 ++++++-- node/typings.d.ts | 1 + 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fa3c3..6f7316f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Add new input quotesManagedBy on appSettings to handle splitting marketplace + ## [2.6.4] - 2024-10-31 ### Fixed diff --git a/graphql/appSettings.graphql b/graphql/appSettings.graphql index 5757cf7..c6e146c 100644 --- a/graphql/appSettings.graphql +++ b/graphql/appSettings.graphql @@ -3,7 +3,13 @@ type AppSettings { } type AdminSetup { cartLifeSpan: Int + quotesManagedBy: QuotesManagedBy! } input AppSettingsInput { cartLifeSpan: Int + quotesManagedBy: QuotesManagedBy } +enum QuotesManagedBy { + MARKETPLACE + SELLER +} \ No newline at end of file diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index 5da1f2e..3f35de0 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -1,5 +1,18 @@ import { indexBy, map, prop } from 'ramda' +import { + APP_NAME, + QUOTE_DATA_ENTITY, + QUOTE_FIELDS, + routes, + SCHEMA_VERSION, +} from '../../constants' +import { sendCreateQuoteMetric } from '../../metrics/createQuote' +import type { UseQuoteMetricsParams } from '../../metrics/useQuote' +import { sendUseQuoteMetric } from '../../metrics/useQuote' +import { isEmail } from '../../utils' +import GraphQLError from '../../utils/GraphQLError' +import message from '../../utils/message' import { checkAndCreateQuotesConfig, checkConfig, @@ -11,19 +24,6 @@ import { checkQuoteStatus, checkSession, } from '../utils/checkPermissions' -import { isEmail } from '../../utils' -import GraphQLError from '../../utils/GraphQLError' -import message from '../../utils/message' -import { - APP_NAME, - QUOTE_DATA_ENTITY, - QUOTE_FIELDS, - routes, - SCHEMA_VERSION, -} from '../../constants' -import { sendCreateQuoteMetric } from '../../metrics/createQuote' -import type { UseQuoteMetricsParams } from '../../metrics/useQuote' -import { sendUseQuoteMetric } from '../../metrics/useQuote' export const Mutation = { clearCart: async (_: any, params: any, ctx: Context) => { @@ -497,7 +497,7 @@ export const Mutation = { }, saveAppSettings: async ( _: void, - { input: { cartLifeSpan } }: { input: { cartLifeSpan: number } }, + { input: { cartLifeSpan, quotesManagedBy = 'MARKETPLACE' } }: { input: { cartLifeSpan: number , quotesManagedBy: string} }, ctx: Context ) => { const { @@ -533,6 +533,7 @@ export const Mutation = { adminSetup: { ...settings.adminSetup, cartLifeSpan, + quotesManagedBy, }, } diff --git a/node/resolvers/queries/index.ts b/node/resolvers/queries/index.ts index 9e24369..6f90a79 100644 --- a/node/resolvers/queries/index.ts +++ b/node/resolvers/queries/index.ts @@ -1,5 +1,3 @@ -import { checkConfig } from '../utils/checkConfig' -import GraphQLError from '../../utils/GraphQLError' import { APP_NAME, B2B_USER_DATA_ENTITY, @@ -8,6 +6,8 @@ import { QUOTE_FIELDS, SCHEMA_VERSION, } from '../../constants' +import GraphQLError from '../../utils/GraphQLError' +import { checkConfig } from '../utils/checkConfig' // This function checks if given email is an user part of a buyer org. export const isUserPartOfBuyerOrg = async (email: string, ctx: Context) => { @@ -334,6 +334,10 @@ export const Query = { return null } + if(settings && !settings?.adminSetup.quotesManagedBy){ + settings.adminSetup.quotesManagedBy = 'MARKETPLACE' + } + return settings }, } diff --git a/node/typings.d.ts b/node/typings.d.ts index 82b16a2..df99a86 100644 --- a/node/typings.d.ts +++ b/node/typings.d.ts @@ -106,6 +106,7 @@ interface Settings { hasCron?: boolean cronExpression?: string cronWorkspace?: string + quotesManagedBy?: string } schemaVersion: string templateHash: string | null From a9ece27a1e8f623efcc860c7ac68d741b420063c Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Sat, 30 Nov 2024 12:52:10 -0300 Subject: [PATCH 02/20] feat: checking new field quotesManagedBy when value is SELLER --- node/resolvers/mutations/index.ts | 41 ++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index 3f35de0..64d7db7 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -65,15 +65,46 @@ export const Mutation = { }, ctx: Context ) => { + // eslint-disable-next-line no-console + console.dir( + { referenceName, items, subtotal, note, sendToSalesRep }, + { depth: null } + ) + const { clients: { masterdata }, vtex, vtex: { logger }, } = ctx - const { sessionData, storefrontPermissions, segmentData } = vtex as any - const settings = await checkConfig(ctx) + const itemsBySeller: any = {} + + if (settings?.adminSetup.quotesManagedBy === 'SELLER') { + items.forEach((item) => { + if (!itemsBySeller[item.seller]) { + // TODO: criar objeto necessário para criar cotação + itemsBySeller[item.seller] = { + items: [], + referenceName, + note, + sendToSalesRep, + subtotal: 0, + } + } + + itemsBySeller[item.seller].items.push(item) + const sellerSubtotal = + (itemsBySeller[item.seller].subtotal as number) + item.sellingPrice + + itemsBySeller[item.seller].subtotal = sellerSubtotal + }) + + // eslint-disable-next-line no-console + console.dir({ itemsBySeller }, { depth: null }) + } + + const { sessionData, storefrontPermissions, segmentData } = vtex as any checkSession(sessionData) @@ -81,6 +112,7 @@ export const Mutation = { throw new GraphQLError('operation-not-permitted') } + // TODO: o conteúdo entre as linhas 116 e 166 pode ficar em uma função separada const email = sessionData.namespaces.profile.email.value const { role: { slug }, @@ -114,6 +146,7 @@ export const Mutation = { const salesChannel: string = segmentData?.channel + // TODO: criar função que cria este objeto para poder usá-la na separação de cotações por seller const quote = { costCenter: costCenterId, creationDate: nowISO, @@ -497,7 +530,9 @@ export const Mutation = { }, saveAppSettings: async ( _: void, - { input: { cartLifeSpan, quotesManagedBy = 'MARKETPLACE' } }: { input: { cartLifeSpan: number , quotesManagedBy: string} }, + { + input: { cartLifeSpan, quotesManagedBy = 'MARKETPLACE' }, + }: { input: { cartLifeSpan: number; quotesManagedBy: string } }, ctx: Context ) => { const { From d40f7052b69ae6569b0b623b115955fba2c3309b Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Mon, 2 Dec 2024 13:50:27 -0300 Subject: [PATCH 03/20] docs: update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7316f..dc45d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Add new input quotesManagedBy on appSettings to handle splitting marketplace +- Add new input quotesManagedBy on appSettings to handle splitting quotes ## [2.6.4] - 2024-10-31 From 58ff10bfb9fb22ded50952180b130aad820fbe20 Mon Sep 17 00:00:00 2001 From: Bruna Santos Date: Wed, 4 Dec 2024 17:41:58 -0300 Subject: [PATCH 04/20] feat: add configuration for quote creation --- node/metrics/createQuote.ts | 2 +- node/resolvers/mutations/index.ts | 185 ++++++++++++++++++------------ node/typings.d.ts | 4 + 3 files changed, 115 insertions(+), 76 deletions(-) diff --git a/node/metrics/createQuote.ts b/node/metrics/createQuote.ts index 77dc7b8..45b3b98 100644 --- a/node/metrics/createQuote.ts +++ b/node/metrics/createQuote.ts @@ -7,7 +7,7 @@ type UserData = { roleId: string } -type SessionData = { +export type SessionData = { namespaces: { profile: { id: { value: string } diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index 64d7db7..eef4b33 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -25,6 +25,94 @@ import { checkSession, } from '../utils/checkPermissions' +interface SellerQuote { + items: QuoteItem[] + referenceName: string + note: string + sendToSalesRep: boolean + subtotal: number +} + +interface QuoteSessionData { + namespaces: { + [key: string]: any // Dynamic keys allowed + } +} + +const createQuoteObject = ({ + sessionData, + storefrontPermissions, + segmentData, + settings, + items, + referenceName, + subtotal, + note, + sendToSalesRep, +}: { + sessionData: QuoteSessionData + storefrontPermissions: string + segmentData?: { channel?: string } + settings?: Settings + items: QuoteItem[] + referenceName: string + subtotal: number + note: string + sendToSalesRep: boolean +}): QuoteWithOptionalId => { + const email = sessionData.namespaces.profile.email.value + + const { + role: { slug }, + } = storefrontPermissions + + const { + organization: { value: organizationId }, + costcenter: { value: costCenterId }, + } = sessionData.namespaces['storefront-permissions'] + + const now = new Date() + const nowISO = now.toISOString() + const expirationDate = new Date() + + expirationDate.setDate( + expirationDate.getDate() + (settings?.adminSetup?.cartLifeSpan ?? 30) + ) + const expirationDateISO = expirationDate.toISOString() + + const status = sendToSalesRep ? 'pending' : 'ready' + const lastUpdate = nowISO + const updateHistory = [ + { + date: nowISO, + email, + note, + role: slug, + status, + }, + ] + + const salesChannel: string = segmentData?.channel ?? '' + + return { + costCenter: costCenterId, + creationDate: nowISO, + creatorEmail: email, + creatorRole: slug, + expirationDate: expirationDateISO, + items, + lastUpdate, + organization: organizationId, + referenceName, + status, + subtotal, + updateHistory, + viewedByCustomer: !!sendToSalesRep, + viewedBySales: !sendToSalesRep, + salesChannel, + } +} + export const Mutation = { clearCart: async (_: any, params: any, ctx: Context) => { const { @@ -65,12 +153,6 @@ export const Mutation = { }, ctx: Context ) => { - // eslint-disable-next-line no-console - console.dir( - { referenceName, items, subtotal, note, sendToSalesRep }, - { depth: null } - ) - const { clients: { masterdata }, vtex, @@ -78,13 +160,12 @@ export const Mutation = { } = ctx const settings = await checkConfig(ctx) - const itemsBySeller: any = {} + const itemsBySeller: Record = {} if (settings?.adminSetup.quotesManagedBy === 'SELLER') { - items.forEach((item) => { - if (!itemsBySeller[item.seller]) { - // TODO: criar objeto necessário para criar cotação - itemsBySeller[item.seller] = { + items.forEach(({ seller, sellingPrice, ...itemData }) => { + if (!itemsBySeller[seller]) { + itemsBySeller[seller] = { items: [], referenceName, note, @@ -93,15 +174,10 @@ export const Mutation = { } } - itemsBySeller[item.seller].items.push(item) - const sellerSubtotal = - (itemsBySeller[item.seller].subtotal as number) + item.sellingPrice + itemsBySeller[seller].items.push({ seller, sellingPrice, ...itemData }) - itemsBySeller[item.seller].subtotal = sellerSubtotal + itemsBySeller[seller].subtotal += sellingPrice }) - - // eslint-disable-next-line no-console - console.dir({ itemsBySeller }, { depth: null }) } const { sessionData, storefrontPermissions, segmentData } = vtex as any @@ -112,58 +188,17 @@ export const Mutation = { throw new GraphQLError('operation-not-permitted') } - // TODO: o conteúdo entre as linhas 116 e 166 pode ficar em uma função separada - const email = sessionData.namespaces.profile.email.value - const { - role: { slug }, - } = storefrontPermissions - - const { - organization: { value: organizationId }, - costcenter: { value: costCenterId }, - } = sessionData.namespaces['storefront-permissions'] - - const now = new Date() - const nowISO = now.toISOString() - const expirationDate = new Date() - - expirationDate.setDate( - expirationDate.getDate() + (settings?.adminSetup?.cartLifeSpan ?? 30) - ) - const expirationDateISO = expirationDate.toISOString() - - const status = sendToSalesRep ? 'pending' : 'ready' - const lastUpdate = nowISO - const updateHistory = [ - { - date: nowISO, - email, - note, - role: slug, - status, - }, - ] - - const salesChannel: string = segmentData?.channel - - // TODO: criar função que cria este objeto para poder usá-la na separação de cotações por seller - const quote = { - costCenter: costCenterId, - creationDate: nowISO, - creatorEmail: email, - creatorRole: slug, - expirationDate: expirationDateISO, + const quote = createQuoteObject({ + sessionData, + storefrontPermissions, + segmentData, + settings: settings || undefined, items, - lastUpdate, - organization: organizationId, referenceName, - status, subtotal, - updateHistory, - viewedByCustomer: !!sendToSalesRep, - viewedBySales: !sendToSalesRep, - salesChannel, - } + note, + sendToSalesRep, + }) try { const data = await masterdata @@ -177,15 +212,15 @@ export const Mutation = { if (sendToSalesRep) { message(ctx) .quoteCreated({ - costCenter: costCenterId, + costCenter: quote.costCenter, id: data.DocumentId, lastUpdate: { - email, + email: quote.creatorEmail, note, - status: status.toUpperCase(), + status: quote.status.toUpperCase(), }, name: referenceName, - organization: organizationId, + organization: quote.organization, }) .then(() => { logger.info({ @@ -197,16 +232,16 @@ export const Mutation = { const metricsParam = { sessionData, userData: { - orgId: organizationId, - costId: costCenterId, - roleId: slug, + orgId: quote.organization, + costId: quote.costCenter, + roleId: quote.creatorRole, }, costCenterName: 'costCenterData?.getCostCenterById?.name', buyerOrgName: 'organizationData?.getOrganizationById?.name', quoteId: data.DocumentId, quoteReferenceName: referenceName, sendToSalesRep, - creationDate: nowISO, + creationDate: quote.creationDate, } sendCreateQuoteMetric(ctx, metricsParam) diff --git a/node/typings.d.ts b/node/typings.d.ts index df99a86..f5dda59 100644 --- a/node/typings.d.ts +++ b/node/typings.d.ts @@ -17,6 +17,10 @@ interface Quote { salesChannel: string | null } +interface QuoteWithOptionalId extends Quote { + id?: string +} + interface QuoteUpdate { email: string role: string From 3cae02305b3b0c9c28025512a59bca38dffea5d4 Mon Sep 17 00:00:00 2001 From: Bruna Santos Date: Fri, 6 Dec 2024 15:00:06 -0300 Subject: [PATCH 05/20] feat: add adjustment for quote seller --- node/resolvers/mutations/index.ts | 77 +++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index eef4b33..3ff9a55 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -160,12 +160,15 @@ export const Mutation = { } = ctx const settings = await checkConfig(ctx) - const itemsBySeller: Record = {} + const quoteBySeller: Record = {} + const { sessionData, storefrontPermissions, segmentData } = vtex as any + + checkSession(sessionData) if (settings?.adminSetup.quotesManagedBy === 'SELLER') { items.forEach(({ seller, sellingPrice, ...itemData }) => { - if (!itemsBySeller[seller]) { - itemsBySeller[seller] = { + if (!quoteBySeller[seller]) { + quoteBySeller[seller] = { items: [], referenceName, note, @@ -174,15 +177,71 @@ export const Mutation = { } } - itemsBySeller[seller].items.push({ seller, sellingPrice, ...itemData }) - - itemsBySeller[seller].subtotal += sellingPrice + quoteBySeller[seller].items.push({ seller, sellingPrice, ...itemData }) + quoteBySeller[seller].subtotal += sellingPrice }) - } - const { sessionData, storefrontPermissions, segmentData } = vtex as any + const documentIds = await Promise.all( + Object.entries(quoteBySeller).map(async ([seller, sellerQuote]) => { + const sellerQuoteObject = createQuoteObject({ + sessionData, + storefrontPermissions, + segmentData, + settings: settings || undefined, + items: sellerQuote.items, + referenceName: sellerQuote.referenceName, + subtotal: sellerQuote.subtotal, + note: sellerQuote.note, + sendToSalesRep: sellerQuote.sendToSalesRep, + }) - checkSession(sessionData) + const data = await masterdata.createDocument({ + dataEntity: QUOTE_DATA_ENTITY, + fields: sellerQuoteObject, + schema: SCHEMA_VERSION, + }) + + if (sendToSalesRep) { + await message(ctx).quoteCreated({ + costCenter: sellerQuoteObject.costCenter, + id: data.DocumentId, + lastUpdate: { + email: sellerQuoteObject.creatorEmail, + note: sellerQuote.note, + status: sellerQuoteObject.status.toUpperCase(), + }, + name: sellerQuote.referenceName, + organization: sellerQuoteObject.organization, + }) + + logger.info({ + message: `[Quote created for seller: ${seller}] E-mail sent to sales reps`, + }) + } + + const metricsParam = { + sessionData, + userData: { + orgId: sellerQuoteObject.organization, + costId: sellerQuoteObject.costCenter, + roleId: sellerQuoteObject.creatorRole, + }, + costCenterName: 'costCenterData?.getCostCenterById?.name', + buyerOrgName: 'organizationData?.getOrganizationById?.name', + quoteId: data.DocumentId, + quoteReferenceName: referenceName, + sendToSalesRep, + creationDate: sellerQuoteObject.creationDate, + } + + sendCreateQuoteMetric(ctx, metricsParam) + + return data.DocumentId + }) + ) + + return documentIds + } if (!storefrontPermissions?.permissions?.includes('create-quotes')) { throw new GraphQLError('operation-not-permitted') From df480220069531a08306345fec2226c508919b25 Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Fri, 6 Dec 2024 15:33:03 -0300 Subject: [PATCH 06/20] chore: fix prettier errors --- node/resolvers/mutations/index.ts | 4 +++- node/resolvers/queries/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index 3f35de0..b4fc750 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -497,7 +497,9 @@ export const Mutation = { }, saveAppSettings: async ( _: void, - { input: { cartLifeSpan, quotesManagedBy = 'MARKETPLACE' } }: { input: { cartLifeSpan: number , quotesManagedBy: string} }, + { + input: { cartLifeSpan, quotesManagedBy = 'MARKETPLACE' }, + }: { input: { cartLifeSpan: number; quotesManagedBy: string } }, ctx: Context ) => { const { diff --git a/node/resolvers/queries/index.ts b/node/resolvers/queries/index.ts index 6f90a79..946e81b 100644 --- a/node/resolvers/queries/index.ts +++ b/node/resolvers/queries/index.ts @@ -334,10 +334,10 @@ export const Query = { return null } - if(settings && !settings?.adminSetup.quotesManagedBy){ + if (settings && !settings?.adminSetup.quotesManagedBy) { settings.adminSetup.quotesManagedBy = 'MARKETPLACE' } - + return settings }, } From 85622cbe339334f7d5a8937c74892c00e983559c Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Fri, 6 Dec 2024 16:35:57 -0300 Subject: [PATCH 07/20] fix: returning ids separated by commas when multiple quotes --- node/resolvers/mutations/index.ts | 2 +- node/resolvers/queries/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index 3ff9a55..245610b 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -240,7 +240,7 @@ export const Mutation = { }) ) - return documentIds + return documentIds.join(',') } if (!storefrontPermissions?.permissions?.includes('create-quotes')) { diff --git a/node/resolvers/queries/index.ts b/node/resolvers/queries/index.ts index 6f90a79..946e81b 100644 --- a/node/resolvers/queries/index.ts +++ b/node/resolvers/queries/index.ts @@ -334,10 +334,10 @@ export const Query = { return null } - if(settings && !settings?.adminSetup.quotesManagedBy){ + if (settings && !settings?.adminSetup.quotesManagedBy) { settings.adminSetup.quotesManagedBy = 'MARKETPLACE' } - + return settings }, } From b3a2c6ce8d9622a9a3bf8f7102458a3eaea8e106 Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Mon, 9 Dec 2024 15:44:39 -0300 Subject: [PATCH 08/20] fix: check config and default settings with marketplace option --- node/resolvers/utils/checkConfig.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/node/resolvers/utils/checkConfig.ts b/node/resolvers/utils/checkConfig.ts index e68857d..025b8d8 100644 --- a/node/resolvers/utils/checkConfig.ts +++ b/node/resolvers/utils/checkConfig.ts @@ -13,6 +13,7 @@ export const defaultSettings: Settings = { adminSetup: { allowManualPrice: false, cartLifeSpan: 30, + quotesManagedBy: 'MARKETPLACE', hasCron: false, }, schemaVersion: '', @@ -345,7 +346,10 @@ export const checkConfig = async (ctx: Context) => { return null } - if (!settings?.adminSetup?.cartLifeSpan) { + if ( + !settings?.adminSetup?.cartLifeSpan && + !settings?.adminSetup?.quotesManagedBy + ) { settings = defaultSettings changed = true } From ffeadeb7d91cd0a731f2cfa12387f7b1e1ea0073 Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Mon, 9 Dec 2024 19:36:11 -0300 Subject: [PATCH 09/20] feat: new fields for quotes managed by seller --- graphql/quote.graphql | 2 ++ node/constants.ts | 12 ++++++++++++ node/resolvers/mutations/index.ts | 16 ++++++++++++---- node/typings.d.ts | 2 ++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/graphql/quote.graphql b/graphql/quote.graphql index 4ce5057..ba1ab85 100644 --- a/graphql/quote.graphql +++ b/graphql/quote.graphql @@ -59,6 +59,8 @@ type Quote { viewedBySales: Boolean viewedByCustomer: Boolean salesChannel: String + seller: String + approvedBySeller: Boolean } type QuoteUpdate { diff --git a/node/constants.ts b/node/constants.ts index 4513036..c8555ac 100644 --- a/node/constants.ts +++ b/node/constants.ts @@ -22,6 +22,8 @@ export const QUOTE_FIELDS = [ 'viewedBySales', 'viewedByCustomer', 'salesChannel', + 'seller', + 'approvedBySeller', ] export const routes = { @@ -134,6 +136,14 @@ export const schema = { title: 'Viewed by Sales', type: 'boolean', }, + seller: { + title: 'Seller', + type: ['null', 'string'], + }, + approvedBySeller: { + title: 'Quote approved by seller', + type: ['null', 'boolean'], + }, }, 'v-cache': false, 'v-default-fields': [ @@ -157,5 +167,7 @@ export const schema = { 'organization', 'costCenter', 'salesChannel', + 'seller', + 'approvedBySeller', ], } diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index 245610b..66b5ad0 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -49,16 +49,20 @@ const createQuoteObject = ({ subtotal, note, sendToSalesRep, + seller, + approvedBySeller, }: { sessionData: QuoteSessionData - storefrontPermissions: string + storefrontPermissions: { role: { slug: string } } segmentData?: { channel?: string } - settings?: Settings + settings?: Settings | null items: QuoteItem[] referenceName: string subtotal: number note: string sendToSalesRep: boolean + seller?: string + approvedBySeller?: boolean }): QuoteWithOptionalId => { const email = sessionData.namespaces.profile.email.value @@ -110,6 +114,8 @@ const createQuoteObject = ({ viewedByCustomer: !!sendToSalesRep, viewedBySales: !sendToSalesRep, salesChannel, + seller, + approvedBySeller, } } @@ -187,12 +193,14 @@ export const Mutation = { sessionData, storefrontPermissions, segmentData, - settings: settings || undefined, + settings, items: sellerQuote.items, referenceName: sellerQuote.referenceName, subtotal: sellerQuote.subtotal, note: sellerQuote.note, sendToSalesRep: sellerQuote.sendToSalesRep, + seller, + approvedBySeller: false, }) const data = await masterdata.createDocument({ @@ -251,7 +259,7 @@ export const Mutation = { sessionData, storefrontPermissions, segmentData, - settings: settings || undefined, + settings, items, referenceName, subtotal, diff --git a/node/typings.d.ts b/node/typings.d.ts index f5dda59..19b1982 100644 --- a/node/typings.d.ts +++ b/node/typings.d.ts @@ -15,6 +15,8 @@ interface Quote { viewedBySales: boolean viewedByCustomer: boolean salesChannel: string | null + seller?: string | null + approvedBySeller?: boolean | null } interface QuoteWithOptionalId extends Quote { From 1d8b309a027d9f855219ef48eac72d49b9cf2230 Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Tue, 10 Dec 2024 01:59:28 -0300 Subject: [PATCH 10/20] feat: new fields to save parent quote; change getQuotes query to filter only parent quotes --- graphql/quote.graphql | 2 + node/constants.ts | 16 ++ node/metrics/createQuote.ts | 12 -- node/package.json | 2 +- node/resolvers/mutations/index.ts | 269 ++++++++-------------------- node/resolvers/queries/index.ts | 3 +- node/resolvers/utils/checkConfig.ts | 10 +- node/resolvers/utils/quotes.ts | 85 +++++++++ node/typings.d.ts | 31 +++- node/yarn.lock | 10 +- 10 files changed, 222 insertions(+), 218 deletions(-) create mode 100644 node/resolvers/utils/quotes.ts diff --git a/graphql/quote.graphql b/graphql/quote.graphql index ba1ab85..78f2b35 100644 --- a/graphql/quote.graphql +++ b/graphql/quote.graphql @@ -61,6 +61,8 @@ type Quote { salesChannel: String seller: String approvedBySeller: Boolean + parentQuote: String + hasChildren: Boolean } type QuoteUpdate { diff --git a/node/constants.ts b/node/constants.ts index c8555ac..f9d1926 100644 --- a/node/constants.ts +++ b/node/constants.ts @@ -24,6 +24,8 @@ export const QUOTE_FIELDS = [ 'salesChannel', 'seller', 'approvedBySeller', + 'parentQuote', + 'hasChildren', ] export const routes = { @@ -144,6 +146,14 @@ export const schema = { title: 'Quote approved by seller', type: ['null', 'boolean'], }, + parentQuote: { + title: 'Parent quote', + type: ['null', 'string'], + }, + hasChildren: { + title: 'Has children', + type: ['null', 'boolean'], + }, }, 'v-cache': false, 'v-default-fields': [ @@ -155,6 +165,10 @@ export const schema = { 'items', 'subtotal', 'status', + 'seller', + 'approvedBySeller', + 'parentQuote', + 'hasChildren', ], 'v-immediate-indexing': true, 'v-indexed': [ @@ -169,5 +183,7 @@ export const schema = { 'salesChannel', 'seller', 'approvedBySeller', + 'parentQuote', + 'hasChildren', ], } diff --git a/node/metrics/createQuote.ts b/node/metrics/createQuote.ts index 45b3b98..3d048ba 100644 --- a/node/metrics/createQuote.ts +++ b/node/metrics/createQuote.ts @@ -7,18 +7,6 @@ type UserData = { roleId: string } -export type SessionData = { - namespaces: { - profile: { - id: { value: string } - email: { value: string } - } - account: { - accountName: { value: string } - } - } -} - type CreateQuoteMetricParam = { sessionData: SessionData sendToSalesRep: boolean diff --git a/node/package.json b/node/package.json index 914af65..fe0660a 100644 --- a/node/package.json +++ b/node/package.json @@ -9,7 +9,7 @@ "ramda": "^0.25.0", "atob": "^2.1.2", "axios": "0.27.2", - "@vtex/api": "6.47.0" + "@vtex/api": "6.48.0" }, "devDependencies": { "@types/atob": "^2.1.2", diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index 66b5ad0..1fb34b3 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -24,100 +24,7 @@ import { checkQuoteStatus, checkSession, } from '../utils/checkPermissions' - -interface SellerQuote { - items: QuoteItem[] - referenceName: string - note: string - sendToSalesRep: boolean - subtotal: number -} - -interface QuoteSessionData { - namespaces: { - [key: string]: any // Dynamic keys allowed - } -} - -const createQuoteObject = ({ - sessionData, - storefrontPermissions, - segmentData, - settings, - items, - referenceName, - subtotal, - note, - sendToSalesRep, - seller, - approvedBySeller, -}: { - sessionData: QuoteSessionData - storefrontPermissions: { role: { slug: string } } - segmentData?: { channel?: string } - settings?: Settings | null - items: QuoteItem[] - referenceName: string - subtotal: number - note: string - sendToSalesRep: boolean - seller?: string - approvedBySeller?: boolean -}): QuoteWithOptionalId => { - const email = sessionData.namespaces.profile.email.value - - const { - role: { slug }, - } = storefrontPermissions - - const { - organization: { value: organizationId }, - costcenter: { value: costCenterId }, - } = sessionData.namespaces['storefront-permissions'] - - const now = new Date() - const nowISO = now.toISOString() - const expirationDate = new Date() - - expirationDate.setDate( - expirationDate.getDate() + (settings?.adminSetup?.cartLifeSpan ?? 30) - ) - const expirationDateISO = expirationDate.toISOString() - - const status = sendToSalesRep ? 'pending' : 'ready' - const lastUpdate = nowISO - const updateHistory = [ - { - date: nowISO, - email, - note, - role: slug, - status, - }, - ] - - const salesChannel: string = segmentData?.channel ?? '' - - return { - costCenter: costCenterId, - creationDate: nowISO, - creatorEmail: email, - creatorRole: slug, - expirationDate: expirationDateISO, - items, - lastUpdate, - organization: organizationId, - referenceName, - status, - subtotal, - updateHistory, - viewedByCustomer: !!sendToSalesRep, - viewedBySales: !sendToSalesRep, - salesChannel, - seller, - approvedBySeller, - } -} +import { createQuoteObject } from '../utils/quotes' export const Mutation = { clearCart: async (_: any, params: any, ctx: Context) => { @@ -166,96 +73,15 @@ export const Mutation = { } = ctx const settings = await checkConfig(ctx) - const quoteBySeller: Record = {} const { sessionData, storefrontPermissions, segmentData } = vtex as any checkSession(sessionData) - if (settings?.adminSetup.quotesManagedBy === 'SELLER') { - items.forEach(({ seller, sellingPrice, ...itemData }) => { - if (!quoteBySeller[seller]) { - quoteBySeller[seller] = { - items: [], - referenceName, - note, - sendToSalesRep, - subtotal: 0, - } - } - - quoteBySeller[seller].items.push({ seller, sellingPrice, ...itemData }) - quoteBySeller[seller].subtotal += sellingPrice - }) - - const documentIds = await Promise.all( - Object.entries(quoteBySeller).map(async ([seller, sellerQuote]) => { - const sellerQuoteObject = createQuoteObject({ - sessionData, - storefrontPermissions, - segmentData, - settings, - items: sellerQuote.items, - referenceName: sellerQuote.referenceName, - subtotal: sellerQuote.subtotal, - note: sellerQuote.note, - sendToSalesRep: sellerQuote.sendToSalesRep, - seller, - approvedBySeller: false, - }) - - const data = await masterdata.createDocument({ - dataEntity: QUOTE_DATA_ENTITY, - fields: sellerQuoteObject, - schema: SCHEMA_VERSION, - }) - - if (sendToSalesRep) { - await message(ctx).quoteCreated({ - costCenter: sellerQuoteObject.costCenter, - id: data.DocumentId, - lastUpdate: { - email: sellerQuoteObject.creatorEmail, - note: sellerQuote.note, - status: sellerQuoteObject.status.toUpperCase(), - }, - name: sellerQuote.referenceName, - organization: sellerQuoteObject.organization, - }) - - logger.info({ - message: `[Quote created for seller: ${seller}] E-mail sent to sales reps`, - }) - } - - const metricsParam = { - sessionData, - userData: { - orgId: sellerQuoteObject.organization, - costId: sellerQuoteObject.costCenter, - roleId: sellerQuoteObject.creatorRole, - }, - costCenterName: 'costCenterData?.getCostCenterById?.name', - buyerOrgName: 'organizationData?.getOrganizationById?.name', - quoteId: data.DocumentId, - quoteReferenceName: referenceName, - sendToSalesRep, - creationDate: sellerQuoteObject.creationDate, - } - - sendCreateQuoteMetric(ctx, metricsParam) - - return data.DocumentId - }) - ) - - return documentIds.join(',') - } - if (!storefrontPermissions?.permissions?.includes('create-quotes')) { throw new GraphQLError('operation-not-permitted') } - const quote = createQuoteObject({ + const parentQuote = createQuoteObject({ sessionData, storefrontPermissions, segmentData, @@ -268,26 +94,83 @@ export const Mutation = { }) try { - const data = await masterdata - .createDocument({ - dataEntity: QUOTE_DATA_ENTITY, - fields: quote, - schema: SCHEMA_VERSION, + const { DocumentId: parentQuoteId } = await masterdata.createDocument({ + dataEntity: QUOTE_DATA_ENTITY, + fields: parentQuote, + schema: SCHEMA_VERSION, + }) + + if (settings?.adminSetup.quotesManagedBy === 'SELLER') { + const quoteBySeller: Record = {} + + items.forEach(({ seller, sellingPrice, ...itemData }) => { + if (!quoteBySeller[seller]) { + quoteBySeller[seller] = { + items: [], + referenceName, + note, + sendToSalesRep, + subtotal: 0, + } + } + + quoteBySeller[seller].items.push({ + seller, + sellingPrice, + ...itemData, + }) + quoteBySeller[seller].subtotal += sellingPrice * itemData.quantity }) - .then((res: any) => res) + + const documentIds = await Promise.all( + Object.entries(quoteBySeller).map(async ([seller, sellerQuote]) => { + const sellerQuoteObject = createQuoteObject({ + sessionData, + storefrontPermissions, + segmentData, + settings, + items: sellerQuote.items, + referenceName: sellerQuote.referenceName, + subtotal: sellerQuote.subtotal, + note: sellerQuote.note, + sendToSalesRep: sellerQuote.sendToSalesRep, + seller, + approvedBySeller: false, + parentQuote: parentQuoteId, + }) + + const data = await masterdata.createDocument({ + dataEntity: QUOTE_DATA_ENTITY, + fields: sellerQuoteObject, + schema: SCHEMA_VERSION, + }) + + return data.DocumentId + }) + ) + + if (documentIds.length) { + await masterdata.updatePartialDocument({ + dataEntity: QUOTE_DATA_ENTITY, + fields: { hasChildren: true }, + id: parentQuoteId, + schema: SCHEMA_VERSION, + }) + } + } if (sendToSalesRep) { message(ctx) .quoteCreated({ - costCenter: quote.costCenter, - id: data.DocumentId, + costCenter: parentQuote.costCenter, + id: parentQuoteId, lastUpdate: { - email: quote.creatorEmail, + email: parentQuote.creatorEmail, note, - status: quote.status.toUpperCase(), + status: parentQuote.status.toUpperCase(), }, name: referenceName, - organization: quote.organization, + organization: parentQuote.organization, }) .then(() => { logger.info({ @@ -299,21 +182,21 @@ export const Mutation = { const metricsParam = { sessionData, userData: { - orgId: quote.organization, - costId: quote.costCenter, - roleId: quote.creatorRole, + orgId: parentQuote.organization, + costId: parentQuote.costCenter, + roleId: parentQuote.creatorRole, }, costCenterName: 'costCenterData?.getCostCenterById?.name', buyerOrgName: 'organizationData?.getOrganizationById?.name', - quoteId: data.DocumentId, + quoteId: parentQuoteId, quoteReferenceName: referenceName, sendToSalesRep, - creationDate: quote.creationDate, + creationDate: parentQuote.creationDate, } sendCreateQuoteMetric(ctx, metricsParam) - return data.DocumentId + return parentQuoteId } catch (error) { logger.error({ error, diff --git a/node/resolvers/queries/index.ts b/node/resolvers/queries/index.ts index 946e81b..844f5d5 100644 --- a/node/resolvers/queries/index.ts +++ b/node/resolvers/queries/index.ts @@ -57,7 +57,8 @@ const buildWhereStatement = async ({ userCostCenterId: string userSalesChannel?: string }) => { - const whereArray = [] + // only the main quotes must be fetched + const whereArray = ['(parentQuote is null)'] // if user only has permission to access their organization's quotes, // hard-code that organization into the masterdata search diff --git a/node/resolvers/utils/checkConfig.ts b/node/resolvers/utils/checkConfig.ts index 025b8d8..1d23c19 100644 --- a/node/resolvers/utils/checkConfig.ts +++ b/node/resolvers/utils/checkConfig.ts @@ -271,6 +271,8 @@ const checkInitializations = async ({ vtex: { workspace }, } = ctx + const hasSplittingQuoteFields = settings.hasSplittingQuoteFieldsInSchema + if ( !settings?.adminSetup?.hasCron || settings?.adminSetup?.cronExpression !== CRON_EXPRESSION || @@ -291,12 +293,16 @@ const checkInitializations = async ({ } } - if (settings?.schemaVersion !== SCHEMA_VERSION) { + if (settings?.schemaVersion !== SCHEMA_VERSION || !hasSplittingQuoteFields) { const oldSchemaVersion = settings?.schemaVersion settings = await initializeSchema(settings, ctx) - if (settings.schemaVersion !== oldSchemaVersion) { + const mustUpdateSettings = + settings.schemaVersion !== oldSchemaVersion || !hasSplittingQuoteFields + + if (mustUpdateSettings) { + settings.hasSplittingQuoteFieldsInSchema = true changed = true } } diff --git a/node/resolvers/utils/quotes.ts b/node/resolvers/utils/quotes.ts new file mode 100644 index 0000000..8a8cc44 --- /dev/null +++ b/node/resolvers/utils/quotes.ts @@ -0,0 +1,85 @@ +export const createQuoteObject = ({ + sessionData, + storefrontPermissions, + segmentData, + settings, + items, + referenceName, + subtotal, + note, + sendToSalesRep, + seller, + approvedBySeller, + parentQuote, + hasChildren, +}: { + sessionData: SessionData + storefrontPermissions: { role: { slug: string } } + segmentData?: { channel?: string } + settings?: Settings | null + items: QuoteItem[] + referenceName: string + subtotal: number + note: string + sendToSalesRep: boolean + seller?: string + approvedBySeller?: boolean | null + parentQuote?: string | null + hasChildren?: boolean | null +}): Omit => { + const email = sessionData.namespaces.profile.email.value + + const { + role: { slug }, + } = storefrontPermissions + + const { + organization: { value: organizationId }, + costcenter: { value: costCenterId }, + } = sessionData.namespaces['storefront-permissions'] + + const now = new Date() + const nowISO = now.toISOString() + const expirationDate = new Date() + + expirationDate.setDate( + expirationDate.getDate() + (settings?.adminSetup?.cartLifeSpan ?? 30) + ) + const expirationDateISO = expirationDate.toISOString() + + const status = sendToSalesRep ? 'pending' : 'ready' + const lastUpdate = nowISO + const updateHistory = [ + { + date: nowISO, + email, + note, + role: slug, + status, + }, + ] + + const salesChannel: string = segmentData?.channel ?? '' + + return { + costCenter: costCenterId, + creationDate: nowISO, + creatorEmail: email, + creatorRole: slug, + expirationDate: expirationDateISO, + items, + lastUpdate, + organization: organizationId, + referenceName, + status, + subtotal, + updateHistory, + viewedByCustomer: !!sendToSalesRep, + viewedBySales: !sendToSalesRep, + salesChannel, + seller, + approvedBySeller, + parentQuote, + hasChildren, + } +} diff --git a/node/typings.d.ts b/node/typings.d.ts index 19b1982..9b471ec 100644 --- a/node/typings.d.ts +++ b/node/typings.d.ts @@ -17,10 +17,8 @@ interface Quote { salesChannel: string | null seller?: string | null approvedBySeller?: boolean | null -} - -interface QuoteWithOptionalId extends Quote { - id?: string + parentQuote?: string | null + hasChildren?: boolean | null } interface QuoteUpdate { @@ -114,6 +112,31 @@ interface Settings { cronWorkspace?: string quotesManagedBy?: string } + hasSplittingQuoteFieldsInSchema?: boolean schemaVersion: string templateHash: string | null } + +interface SessionData { + namespaces: { + profile: { + id: { value: string } + email: { value: string } + } + account: { + accountName: { value: string } + } + 'storefront-permissions': { + organization: { value: string } + costcenter: { value: string } + } + } +} + +interface SellerQuoteInput { + items: QuoteItem[] + referenceName: string + note: string + sendToSalesRep: boolean + subtotal: number +} diff --git a/node/yarn.lock b/node/yarn.lock index 704ce0e..d572997 100644 --- a/node/yarn.lock +++ b/node/yarn.lock @@ -178,10 +178,10 @@ "@types/mime" "^1" "@types/node" "*" -"@vtex/api@6.47.0": - version "6.47.0" - resolved "https://registry.yarnpkg.com/@vtex/api/-/api-6.47.0.tgz#6910455d593d8bb76f1f4f2b7660023853fda35e" - integrity sha512-t9gt7Q89EMbSj3rLhho+49Fv+/lQgiy8EPVRgtmmXFp1J4v8hIAZF7GPjCPie111KVs4eG0gfZFpmhA5dafKNA== +"@vtex/api@6.48.0": + version "6.48.0" + resolved "https://registry.yarnpkg.com/@vtex/api/-/api-6.48.0.tgz#67f9f11d197d543d4f854b057d31a8d6999241e9" + integrity sha512-mAdT7gbV0/BwiuqUkNH1E7KZqTUczT5NbBBZcPJq5kmTr73PUjbR9wh//70ryJo2EAdHlqIgqgwsCVpozenlhg== dependencies: "@types/koa" "^2.11.0" "@types/koa-compose" "^3.2.3" @@ -1522,7 +1522,7 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -stats-lite@vtex/node-stats-lite#dist: +"stats-lite@github:vtex/node-stats-lite#dist": version "2.2.0" resolved "https://codeload.github.com/vtex/node-stats-lite/tar.gz/1b0d39cc41ef7aaecfd541191f877887a2044797" dependencies: From 36132622865ed0cf20093d1f0d5dd4fcd774bbb2 Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Tue, 10 Dec 2024 02:09:55 -0300 Subject: [PATCH 11/20] feat: create client to notify seller quote --- node/clients/SellerQuotesClient.ts | 47 ++++++++++++++++++++++++++++++ node/clients/index.ts | 5 ++++ 2 files changed, 52 insertions(+) create mode 100644 node/clients/SellerQuotesClient.ts diff --git a/node/clients/SellerQuotesClient.ts b/node/clients/SellerQuotesClient.ts new file mode 100644 index 0000000..6426d8c --- /dev/null +++ b/node/clients/SellerQuotesClient.ts @@ -0,0 +1,47 @@ +import type { InstanceOptions, IOContext } from '@vtex/api' +import { ExternalClient } from '@vtex/api' + +const SELLER_CLIENT_OPTIONS: InstanceOptions = { + retries: 5, + timeout: 5000, + exponentialTimeoutCoefficient: 2, + exponentialBackoffCoefficient: 2, + initialBackoffDelay: 100, +} + +interface NotifySellerQuoteResponse { + status: string +} + +export default class SellerQuotesClient extends ExternalClient { + constructor(ctx: IOContext, options?: InstanceOptions) { + super('', ctx, { + ...options, + ...SELLER_CLIENT_OPTIONS, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + VtexIdclientAutCookie: ctx.authToken, + }, + }) + } + + private getUrl(account: string) { + const subdomain = this.context.production + ? account + : `${this.context.workspace}--${account}` + + return `http://${subdomain}.myvtex.com/_v/b2b-seller-quotes/notify-quote` + } + + public async notify(account: string, quote: Quote) { + return this.http + .postRaw(this.getUrl(account), quote) + .then((res) => { + // eslint-disable-next-line no-console + console.log('RESPONSE', res) + + return res + }) + } +} diff --git a/node/clients/index.ts b/node/clients/index.ts index c872deb..b8af4a6 100644 --- a/node/clients/index.ts +++ b/node/clients/index.ts @@ -13,6 +13,7 @@ import OrdersClient from './OrdersClient' import Organizations from './organizations' import StorefrontPermissions from './storefrontPermissions' import VtexId from './vtexId' +import SellerQuotesClient from './SellerQuotesClient' export const getTokenToHeader = (ctx: IOContext) => { // provide authToken (app token) as an admin token as this is a call @@ -86,4 +87,8 @@ export class Clients extends IOClients { public get identity() { return this.getOrSet('identity', Identity) } + + public get sellerQuotes() { + return this.getOrSet('sellerQuotes', SellerQuotesClient) + } } From f81db0154d4b775d8a7cd8ea9db07fc5f69c8eb4 Mon Sep 17 00:00:00 2001 From: Bruna Santos Date: Tue, 10 Dec 2024 11:04:32 -0300 Subject: [PATCH 12/20] feat: add changelog --- CHANGELOG.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc45d1f..38b01e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,36 +8,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - Add new input quotesManagedBy on appSettings to handle splitting quotes +- Add new quote configuration to handle split quote by seller ## [2.6.4] - 2024-10-31 ### Fixed + - Only update Status, LastUpdate and UpdateHistory, in expired quotes ## [2.6.3] - 2024-10-30 ### Fixed + - Set viewedByCustomer value False when value is null ## [2.6.2] - 2024-10-02 ### Added + - Add audit access metrics to all graphql APIs ## [2.6.1] - 2024-09-09 ### Fixed + - Set viewedByCustomer value corectly on quote creation ## [2.6.0] - 2024-09-04 ### Added + - Add getQuoteEnabledForUser query to be used by the b2b-quotes app ## [2.5.4] - 2024-08-20 ### Fixed + - Use listUsersPaginated internally instead of deprecated listUsers ## [2.5.3] - 2024-06-10 @@ -79,15 +87,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.3.1] - 2023-09-13 ### Fixed + - Use the account to get the token in the header and send it to clear the cart and order ## [2.3.0] - 2023-08-14 ### Added + - Send metrics to Analytics (Create Quote and Send Message events) -- Send use quote metrics to Analytics - +- Send use quote metrics to Analytics + ### Removed + - [ENGINEERS-1247] - Disable cypress tests in PR level ### Changed From 9af7ee0e3407f1bdc1e4cde0a3ab661b9d697b5e Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Wed, 11 Dec 2024 02:06:27 -0300 Subject: [PATCH 13/20] feat: verify and notify seller quote --- CHANGELOG.md | 2 ++ node/clients/SellerQuotesClient.ts | 48 ++++++++++++++++++++++++++---- node/resolvers/mutations/index.ts | 13 +++++++- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc45d1f..b33182f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add new input quotesManagedBy on appSettings to handle splitting quotes +- Verify if seller accept manage quotes +- Notify seller with quote payload if it accepts to manage quotes ## [2.6.4] - 2024-10-31 diff --git a/node/clients/SellerQuotesClient.ts b/node/clients/SellerQuotesClient.ts index 6426d8c..9d8e758 100644 --- a/node/clients/SellerQuotesClient.ts +++ b/node/clients/SellerQuotesClient.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import type { InstanceOptions, IOContext } from '@vtex/api' import { ExternalClient } from '@vtex/api' @@ -9,10 +10,19 @@ const SELLER_CLIENT_OPTIONS: InstanceOptions = { initialBackoffDelay: 100, } +interface VerifyQuoteSettingsResponse { + receiveQuotes: boolean +} + interface NotifySellerQuoteResponse { status: string } +const routes = { + verifyQuoteSettings: '/verify-quote-settings', + notifyNewQuote: '/notify-new-quote', +} + export default class SellerQuotesClient extends ExternalClient { constructor(ctx: IOContext, options?: InstanceOptions) { super('', ctx, { @@ -26,22 +36,48 @@ export default class SellerQuotesClient extends ExternalClient { }) } - private getUrl(account: string) { + private getRoute(account: string, path: string) { const subdomain = this.context.production ? account : `${this.context.workspace}--${account}` - return `http://${subdomain}.myvtex.com/_v/b2b-seller-quotes/notify-quote` + return `http://${subdomain}.myvtex.com/_v/b2b-seller-quotes${path}` } - public async notify(account: string, quote: Quote) { + public async verifyQuoteSettings(account: string) { return this.http - .postRaw(this.getUrl(account), quote) + .get( + this.getRoute(account, routes.verifyQuoteSettings) + ) .then((res) => { - // eslint-disable-next-line no-console - console.log('RESPONSE', res) + console.log('==================================================') + console.log('SUCCESS RESPONSE WHEN VERIFY SELLER:', res) return res }) + .catch((err) => { + console.log('==================================================') + console.log('ERROR WHEN VEFIFY SELLER:', err) + throw err + }) + } + + public async notifyNewQuote(account: string, quote: Quote) { + return this.http + .postRaw( + this.getRoute(account, routes.notifyNewQuote), + quote + ) + .then((res) => { + console.log('==================================================') + console.log('SUCCESS RESPONSE WHEN NOTIFY SELLER:', res) + + return res + }) + .catch((err) => { + console.log('==================================================') + console.log('ERROR WHEN NOTIFY SELLER:', err) + throw err + }) } } diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index 1fb34b3..ae61349 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -124,6 +124,12 @@ export const Mutation = { const documentIds = await Promise.all( Object.entries(quoteBySeller).map(async ([seller, sellerQuote]) => { + const verifyResponse = await ctx.clients.sellerQuotes.verifyQuoteSettings( + seller + ) + + if (!verifyResponse.receiveQuotes) return null + const sellerQuoteObject = createQuoteObject({ sessionData, storefrontPermissions, @@ -145,11 +151,16 @@ export const Mutation = { schema: SCHEMA_VERSION, }) + await ctx.clients.sellerQuotes.notifyNewQuote(seller, { + id: data.DocumentId, + ...sellerQuoteObject, + }) + return data.DocumentId }) ) - if (documentIds.length) { + if (documentIds.filter(Boolean).length) { await masterdata.updatePartialDocument({ dataEntity: QUOTE_DATA_ENTITY, fields: { hasChildren: true }, From caccc46a8a31f3138c7e3f9526a515d753c9280c Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Wed, 11 Dec 2024 10:11:04 -0300 Subject: [PATCH 14/20] chore: removes console.log from SellerQuotesClient --- node/clients/SellerQuotesClient.ts | 39 ++++++------------------------ 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/node/clients/SellerQuotesClient.ts b/node/clients/SellerQuotesClient.ts index 9d8e758..fcd1065 100644 --- a/node/clients/SellerQuotesClient.ts +++ b/node/clients/SellerQuotesClient.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import type { InstanceOptions, IOContext } from '@vtex/api' import { ExternalClient } from '@vtex/api' @@ -45,39 +44,15 @@ export default class SellerQuotesClient extends ExternalClient { } public async verifyQuoteSettings(account: string) { - return this.http - .get( - this.getRoute(account, routes.verifyQuoteSettings) - ) - .then((res) => { - console.log('==================================================') - console.log('SUCCESS RESPONSE WHEN VERIFY SELLER:', res) - - return res - }) - .catch((err) => { - console.log('==================================================') - console.log('ERROR WHEN VEFIFY SELLER:', err) - throw err - }) + return this.http.get( + this.getRoute(account, routes.verifyQuoteSettings) + ) } public async notifyNewQuote(account: string, quote: Quote) { - return this.http - .postRaw( - this.getRoute(account, routes.notifyNewQuote), - quote - ) - .then((res) => { - console.log('==================================================') - console.log('SUCCESS RESPONSE WHEN NOTIFY SELLER:', res) - - return res - }) - .catch((err) => { - console.log('==================================================') - console.log('ERROR WHEN NOTIFY SELLER:', err) - throw err - }) + return this.http.postRaw( + this.getRoute(account, routes.notifyNewQuote), + quote + ) } } From 34901e0f83eb8472975b3eebdd529168e35ed15e Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Sat, 14 Dec 2024 20:07:01 -0300 Subject: [PATCH 15/20] docs: update CHANGELOG and prettier fix on markdown --- CHANGELOG.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc45d1f..dc3f649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,36 +8,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Add new input quotesManagedBy on appSettings to handle splitting quotes + +- Add field quotesManagedBy on appSettings to handle splitting quotes ## [2.6.4] - 2024-10-31 ### Fixed + - Only update Status, LastUpdate and UpdateHistory, in expired quotes ## [2.6.3] - 2024-10-30 ### Fixed + - Set viewedByCustomer value False when value is null ## [2.6.2] - 2024-10-02 ### Added + - Add audit access metrics to all graphql APIs ## [2.6.1] - 2024-09-09 ### Fixed + - Set viewedByCustomer value corectly on quote creation ## [2.6.0] - 2024-09-04 ### Added + - Add getQuoteEnabledForUser query to be used by the b2b-quotes app ## [2.5.4] - 2024-08-20 ### Fixed + - Use listUsersPaginated internally instead of deprecated listUsers ## [2.5.3] - 2024-06-10 @@ -79,15 +86,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.3.1] - 2023-09-13 ### Fixed + - Use the account to get the token in the header and send it to clear the cart and order ## [2.3.0] - 2023-08-14 ### Added + - Send metrics to Analytics (Create Quote and Send Message events) -- Send use quote metrics to Analytics - +- Send use quote metrics to Analytics + ### Removed + - [ENGINEERS-1247] - Disable cypress tests in PR level ### Changed From 70d1c4d2a529d5aef1cabd283ac2d147f0c4bced Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Sat, 14 Dec 2024 20:20:51 -0300 Subject: [PATCH 16/20] fix: right splitting of seller quotes --- CHANGELOG.md | 7 +- node/clients/SellerQuotesClient.ts | 34 +++++---- node/resolvers/mutations/index.ts | 107 ++++++++++++++++------------- node/resolvers/utils/quotes.ts | 59 ++++++++++++++++ 4 files changed, 142 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78286ab..58df8ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add new input quotesManagedBy on appSettings to handle splitting quotes -- Add new quote configuration to handle split quote by seller -- Verify if seller accept manage quotes -- Notify seller with quote payload if it accepts to manage quotes +- Add field quotesManagedBy on appSettings to handle splitting quotes +- Process splitting quote by seller if it accepts to manage quotes +- Notify seller with quote reference data as payload ## [2.6.4] - 2024-10-31 diff --git a/node/clients/SellerQuotesClient.ts b/node/clients/SellerQuotesClient.ts index fcd1065..0b0d9d1 100644 --- a/node/clients/SellerQuotesClient.ts +++ b/node/clients/SellerQuotesClient.ts @@ -13,13 +13,14 @@ interface VerifyQuoteSettingsResponse { receiveQuotes: boolean } -interface NotifySellerQuoteResponse { - status: string +interface SellerQuoteNotifyInput { + quoteId: string + marketplaceAccount: string } const routes = { - verifyQuoteSettings: '/verify-quote-settings', - notifyNewQuote: '/notify-new-quote', + verifyQuoteSettings: 'verify-quote-settings', + notifyNewQuote: 'notify-new-quote', } export default class SellerQuotesClient extends ExternalClient { @@ -35,24 +36,29 @@ export default class SellerQuotesClient extends ExternalClient { }) } - private getRoute(account: string, path: string) { + private getRoute(sellerAccount: string, path: string) { const subdomain = this.context.production - ? account - : `${this.context.workspace}--${account}` + ? sellerAccount + : `${this.context.workspace}--${sellerAccount}` - return `http://${subdomain}.myvtex.com/_v/b2b-seller-quotes${path}` + return `http://${subdomain}.myvtex.com/b2b-seller-quotes/_v/0/${path}` } - public async verifyQuoteSettings(account: string) { + public async verifyQuoteSettings(sellerAccount: string) { return this.http.get( - this.getRoute(account, routes.verifyQuoteSettings) + this.getRoute(sellerAccount, routes.verifyQuoteSettings) ) } - public async notifyNewQuote(account: string, quote: Quote) { - return this.http.postRaw( - this.getRoute(account, routes.notifyNewQuote), - quote + public async notifyNewQuote(sellerAccount: string, quoteId: string) { + const payload: SellerQuoteNotifyInput = { + quoteId, + marketplaceAccount: this.context.account, + } + + return this.http.postRaw( + this.getRoute(sellerAccount, routes.notifyNewQuote), + payload ) } } diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index ae61349..f7e30c2 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -24,7 +24,7 @@ import { checkQuoteStatus, checkSession, } from '../utils/checkPermissions' -import { createQuoteObject } from '../utils/quotes' +import { createQuoteObject, processSellerItems } from '../utils/quotes' export const Mutation = { clearCart: async (_: any, params: any, ctx: Context) => { @@ -81,55 +81,68 @@ export const Mutation = { throw new GraphQLError('operation-not-permitted') } - const parentQuote = createQuoteObject({ - sessionData, - storefrontPermissions, - segmentData, - settings, - items, - referenceName, - subtotal, - note, - sendToSalesRep, - }) - try { - const { DocumentId: parentQuoteId } = await masterdata.createDocument({ - dataEntity: QUOTE_DATA_ENTITY, - fields: parentQuote, - schema: SCHEMA_VERSION, - }) + const quoteBySeller: Record = {} if (settings?.adminSetup.quotesManagedBy === 'SELLER') { - const quoteBySeller: Record = {} - - items.forEach(({ seller, sellingPrice, ...itemData }) => { - if (!quoteBySeller[seller]) { - quoteBySeller[seller] = { - items: [], - referenceName, - note, - sendToSalesRep, - subtotal: 0, - } - } + const sellerItems = items.filter( + ({ seller }) => seller && seller !== '1' + ) - quoteBySeller[seller].items.push({ - seller, - sellingPrice, - ...itemData, - }) - quoteBySeller[seller].subtotal += sellingPrice * itemData.quantity + await processSellerItems({ + ctx, + quoteBySeller, + referenceName, + note, + sendToSalesRep, + items: sellerItems, }) + } - const documentIds = await Promise.all( - Object.entries(quoteBySeller).map(async ([seller, sellerQuote]) => { - const verifyResponse = await ctx.clients.sellerQuotes.verifyQuoteSettings( - seller - ) + const hasSellerQuotes = Object.keys(quoteBySeller).length + + const parentQuoteItems = hasSellerQuotes + ? items.filter( + (item) => + !Object.values(quoteBySeller).some((quote) => + quote.items.some((quoteItem) => quoteItem.id === item.id) + ) + ) + : items + + // We believe that parent quote should contain the overall subtotal. + // If for some reason it is necessary to subtract the subtotal from + // sellers quotes, we can use the adjustedSubtotal below, assigning + // it to subtotal in createQuoteObject (subtotal: adjustedSubtotal). + // + // const adjustedSubtotal = hasSellerQuotes + // ? Object.values(quoteBySeller).reduce( + // (acc, quote) => acc - quote.subtotal, + // subtotal + // ) + // : subtotal + + const parentQuote = createQuoteObject({ + sessionData, + storefrontPermissions, + segmentData, + settings, + items: parentQuoteItems, + referenceName, + subtotal, + note, + sendToSalesRep, + }) - if (!verifyResponse.receiveQuotes) return null + const { DocumentId: parentQuoteId } = await masterdata.createDocument({ + dataEntity: QUOTE_DATA_ENTITY, + fields: parentQuote, + schema: SCHEMA_VERSION, + }) + if (hasSellerQuotes) { + const sellerQuoteIds = await Promise.all( + Object.entries(quoteBySeller).map(async ([seller, sellerQuote]) => { const sellerQuoteObject = createQuoteObject({ sessionData, storefrontPermissions, @@ -151,16 +164,16 @@ export const Mutation = { schema: SCHEMA_VERSION, }) - await ctx.clients.sellerQuotes.notifyNewQuote(seller, { - id: data.DocumentId, - ...sellerQuoteObject, - }) + await ctx.clients.sellerQuotes.notifyNewQuote( + seller, + data.DocumentId + ) return data.DocumentId }) ) - if (documentIds.filter(Boolean).length) { + if (sellerQuoteIds.length) { await masterdata.updatePartialDocument({ dataEntity: QUOTE_DATA_ENTITY, fields: { hasChildren: true }, diff --git a/node/resolvers/utils/quotes.ts b/node/resolvers/utils/quotes.ts index 8a8cc44..9e614c7 100644 --- a/node/resolvers/utils/quotes.ts +++ b/node/resolvers/utils/quotes.ts @@ -1,3 +1,62 @@ +export async function processSellerItems({ + ctx, + quoteBySeller, + referenceName, + note, + sendToSalesRep, + items, + index = 0, +}: { + ctx: Context + quoteBySeller: Record + referenceName: string + note: string + sendToSalesRep: boolean + items: QuoteItem[] + index?: number +}): Promise { + if (index >= items.length) return + + const item = items[index] + const { seller } = item + + const next = async () => + processSellerItems({ + ctx, + quoteBySeller, + referenceName, + note, + sendToSalesRep, + items, + index: index + 1, + }) + + const verifyResponse = await ctx.clients.sellerQuotes + .verifyQuoteSettings(seller) + .catch(() => null) + + if (!verifyResponse?.receiveQuotes) { + await next() + + return + } + + if (!quoteBySeller[seller]) { + quoteBySeller[seller] = { + items: [], + referenceName, + note, + sendToSalesRep, + subtotal: 0, + } + } + + quoteBySeller[seller].items.push(item) + quoteBySeller[seller].subtotal += item.sellingPrice * item.quantity + + await next() +} + export const createQuoteObject = ({ sessionData, storefrontPermissions, From 69d2076079286bbd4005d1f6390ff9f08813e350 Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Sun, 15 Dec 2024 00:45:56 -0300 Subject: [PATCH 17/20] feat: refactor on creating seller quote map; avoid double check of seller --- node/clients/SellerQuotesClient.ts | 9 ------ node/resolvers/mutations/index.ts | 48 ++++++++++++++---------------- node/resolvers/utils/quotes.ts | 48 +++++++++++++----------------- node/typings.d.ts | 14 +++++++-- 4 files changed, 54 insertions(+), 65 deletions(-) diff --git a/node/clients/SellerQuotesClient.ts b/node/clients/SellerQuotesClient.ts index 0b0d9d1..827c388 100644 --- a/node/clients/SellerQuotesClient.ts +++ b/node/clients/SellerQuotesClient.ts @@ -9,15 +9,6 @@ const SELLER_CLIENT_OPTIONS: InstanceOptions = { initialBackoffDelay: 100, } -interface VerifyQuoteSettingsResponse { - receiveQuotes: boolean -} - -interface SellerQuoteNotifyInput { - quoteId: string - marketplaceAccount: string -} - const routes = { verifyQuoteSettings: 'verify-quote-settings', notifyNewQuote: 'notify-new-quote', diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index f7e30c2..c55abd9 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -24,7 +24,11 @@ import { checkQuoteStatus, checkSession, } from '../utils/checkPermissions' -import { createQuoteObject, processSellerItems } from '../utils/quotes' +import { + createItemComparator, + createQuoteObject, + splitItemsBySeller, +} from '../utils/quotes' export const Mutation = { clearCart: async (_: any, params: any, ctx: Context) => { @@ -82,19 +86,15 @@ export const Mutation = { } try { - const quoteBySeller: Record = {} + let quoteBySeller: SellerQuoteMap = {} if (settings?.adminSetup.quotesManagedBy === 'SELLER') { const sellerItems = items.filter( ({ seller }) => seller && seller !== '1' ) - await processSellerItems({ + quoteBySeller = await splitItemsBySeller({ ctx, - quoteBySeller, - referenceName, - note, - sendToSalesRep, items: sellerItems, }) } @@ -105,15 +105,25 @@ export const Mutation = { ? items.filter( (item) => !Object.values(quoteBySeller).some((quote) => - quote.items.some((quoteItem) => quoteItem.id === item.id) + quote.items.some(createItemComparator(item)) ) ) : items + const quoteCommonFields = { + sessionData, + storefrontPermissions, + segmentData, + settings, + referenceName, + note, + sendToSalesRep, + } + // We believe that parent quote should contain the overall subtotal. // If for some reason it is necessary to subtract the subtotal from // sellers quotes, we can use the adjustedSubtotal below, assigning - // it to subtotal in createQuoteObject (subtotal: adjustedSubtotal). + // it to subtotal in createQuoteObject -> `subtotal: adjustedSubtotal` // // const adjustedSubtotal = hasSellerQuotes // ? Object.values(quoteBySeller).reduce( @@ -121,17 +131,10 @@ export const Mutation = { // subtotal // ) // : subtotal - const parentQuote = createQuoteObject({ - sessionData, - storefrontPermissions, - segmentData, - settings, + ...quoteCommonFields, items: parentQuoteItems, - referenceName, subtotal, - note, - sendToSalesRep, }) const { DocumentId: parentQuoteId } = await masterdata.createDocument({ @@ -144,15 +147,8 @@ export const Mutation = { const sellerQuoteIds = await Promise.all( Object.entries(quoteBySeller).map(async ([seller, sellerQuote]) => { const sellerQuoteObject = createQuoteObject({ - sessionData, - storefrontPermissions, - segmentData, - settings, - items: sellerQuote.items, - referenceName: sellerQuote.referenceName, - subtotal: sellerQuote.subtotal, - note: sellerQuote.note, - sendToSalesRep: sellerQuote.sendToSalesRep, + ...quoteCommonFields, + ...sellerQuote, seller, approvedBySeller: false, parentQuote: parentQuoteId, diff --git a/node/resolvers/utils/quotes.ts b/node/resolvers/utils/quotes.ts index 9e614c7..7ec7d6d 100644 --- a/node/resolvers/utils/quotes.ts +++ b/node/resolvers/utils/quotes.ts @@ -1,60 +1,54 @@ -export async function processSellerItems({ +export async function splitItemsBySeller({ ctx, - quoteBySeller, - referenceName, - note, - sendToSalesRep, items, + quoteBySeller = {}, index = 0, }: { ctx: Context - quoteBySeller: Record - referenceName: string - note: string - sendToSalesRep: boolean items: QuoteItem[] + quoteBySeller?: SellerQuoteMap index?: number -}): Promise { - if (index >= items.length) return +}): Promise { + if (index >= items.length) return quoteBySeller const item = items[index] const { seller } = item const next = async () => - processSellerItems({ + splitItemsBySeller({ ctx, - quoteBySeller, - referenceName, - note, - sendToSalesRep, items, + quoteBySeller, index: index + 1, }) - const verifyResponse = await ctx.clients.sellerQuotes - .verifyQuoteSettings(seller) - .catch(() => null) + // The ternary check is to not request again from the same seller + const verifyResponse = quoteBySeller[seller] + ? { receiveQuotes: true } + : await ctx.clients.sellerQuotes + .verifyQuoteSettings(seller) + .catch(() => null) if (!verifyResponse?.receiveQuotes) { await next() - return + return quoteBySeller } if (!quoteBySeller[seller]) { - quoteBySeller[seller] = { - items: [], - referenceName, - note, - sendToSalesRep, - subtotal: 0, - } + quoteBySeller[seller] = { items: [], subtotal: 0 } } quoteBySeller[seller].items.push(item) quoteBySeller[seller].subtotal += item.sellingPrice * item.quantity await next() + + return quoteBySeller +} + +export function createItemComparator(item: T) { + return ({ id, seller }: T) => item.id === id && item.seller === seller } export const createQuoteObject = ({ diff --git a/node/typings.d.ts b/node/typings.d.ts index 9b471ec..b4585b8 100644 --- a/node/typings.d.ts +++ b/node/typings.d.ts @@ -135,8 +135,16 @@ interface SessionData { interface SellerQuoteInput { items: QuoteItem[] - referenceName: string - note: string - sendToSalesRep: boolean subtotal: number } + +type SellerQuoteMap = Record + +interface VerifyQuoteSettingsResponse { + receiveQuotes: boolean +} + +interface SellerQuoteNotifyInput { + quoteId: string + marketplaceAccount: string +} From fbb652920b29896d6d2c0b1b9575d72b75e4aa17 Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Sun, 15 Dec 2024 00:52:54 -0300 Subject: [PATCH 18/20] refactor: seller quotes client constants --- node/clients/SellerQuotesClient.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/node/clients/SellerQuotesClient.ts b/node/clients/SellerQuotesClient.ts index 827c388..b784916 100644 --- a/node/clients/SellerQuotesClient.ts +++ b/node/clients/SellerQuotesClient.ts @@ -9,7 +9,9 @@ const SELLER_CLIENT_OPTIONS: InstanceOptions = { initialBackoffDelay: 100, } -const routes = { +const BASE_PATH = 'b2b-seller-quotes/_v/0' + +const ROUTES = { verifyQuoteSettings: 'verify-quote-settings', notifyNewQuote: 'notify-new-quote', } @@ -32,12 +34,12 @@ export default class SellerQuotesClient extends ExternalClient { ? sellerAccount : `${this.context.workspace}--${sellerAccount}` - return `http://${subdomain}.myvtex.com/b2b-seller-quotes/_v/0/${path}` + return `http://${subdomain}.myvtex.com/${BASE_PATH}/${path}` } public async verifyQuoteSettings(sellerAccount: string) { return this.http.get( - this.getRoute(sellerAccount, routes.verifyQuoteSettings) + this.getRoute(sellerAccount, ROUTES.verifyQuoteSettings) ) } @@ -48,7 +50,7 @@ export default class SellerQuotesClient extends ExternalClient { } return this.http.postRaw( - this.getRoute(sellerAccount, routes.notifyNewQuote), + this.getRoute(sellerAccount, ROUTES.notifyNewQuote), payload ) } From 021b3591cf247bd195d2c54a825b1f0e81feccd5 Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Sun, 15 Dec 2024 17:08:47 -0300 Subject: [PATCH 19/20] feat: send creationDate on notify seller quote --- node/clients/SellerQuotesClient.ts | 7 ++++++- node/resolvers/mutations/index.ts | 3 ++- node/typings.d.ts | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/node/clients/SellerQuotesClient.ts b/node/clients/SellerQuotesClient.ts index b784916..74bc681 100644 --- a/node/clients/SellerQuotesClient.ts +++ b/node/clients/SellerQuotesClient.ts @@ -43,9 +43,14 @@ export default class SellerQuotesClient extends ExternalClient { ) } - public async notifyNewQuote(sellerAccount: string, quoteId: string) { + public async notifyNewQuote( + sellerAccount: string, + quoteId: string, + creationDate: string + ) { const payload: SellerQuoteNotifyInput = { quoteId, + creationDate, marketplaceAccount: this.context.account, } diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index c55abd9..a4197a3 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -162,7 +162,8 @@ export const Mutation = { await ctx.clients.sellerQuotes.notifyNewQuote( seller, - data.DocumentId + data.DocumentId, + sellerQuoteObject.creationDate ) return data.DocumentId diff --git a/node/typings.d.ts b/node/typings.d.ts index b4585b8..fc34590 100644 --- a/node/typings.d.ts +++ b/node/typings.d.ts @@ -147,4 +147,5 @@ interface VerifyQuoteSettingsResponse { interface SellerQuoteNotifyInput { quoteId: string marketplaceAccount: string + creationDate: string } From f6a11178e50e1875592b04970d14d11546457d18 Mon Sep 17 00:00:00 2001 From: Tiago de Andrade Freire Date: Tue, 17 Dec 2024 20:28:02 -0300 Subject: [PATCH 20/20] fix: removing approvedBySeller field from quote entity --- graphql/quote.graphql | 1 - node/constants.ts | 7 ------- node/resolvers/mutations/index.ts | 1 - node/resolvers/utils/quotes.ts | 3 --- node/typings.d.ts | 1 - 5 files changed, 13 deletions(-) diff --git a/graphql/quote.graphql b/graphql/quote.graphql index 78f2b35..1fa53b9 100644 --- a/graphql/quote.graphql +++ b/graphql/quote.graphql @@ -60,7 +60,6 @@ type Quote { viewedByCustomer: Boolean salesChannel: String seller: String - approvedBySeller: Boolean parentQuote: String hasChildren: Boolean } diff --git a/node/constants.ts b/node/constants.ts index f9d1926..01dc677 100644 --- a/node/constants.ts +++ b/node/constants.ts @@ -23,7 +23,6 @@ export const QUOTE_FIELDS = [ 'viewedByCustomer', 'salesChannel', 'seller', - 'approvedBySeller', 'parentQuote', 'hasChildren', ] @@ -142,10 +141,6 @@ export const schema = { title: 'Seller', type: ['null', 'string'], }, - approvedBySeller: { - title: 'Quote approved by seller', - type: ['null', 'boolean'], - }, parentQuote: { title: 'Parent quote', type: ['null', 'string'], @@ -166,7 +161,6 @@ export const schema = { 'subtotal', 'status', 'seller', - 'approvedBySeller', 'parentQuote', 'hasChildren', ], @@ -182,7 +176,6 @@ export const schema = { 'costCenter', 'salesChannel', 'seller', - 'approvedBySeller', 'parentQuote', 'hasChildren', ], diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index a4197a3..affc0dc 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -150,7 +150,6 @@ export const Mutation = { ...quoteCommonFields, ...sellerQuote, seller, - approvedBySeller: false, parentQuote: parentQuoteId, }) diff --git a/node/resolvers/utils/quotes.ts b/node/resolvers/utils/quotes.ts index 7ec7d6d..4fddbea 100644 --- a/node/resolvers/utils/quotes.ts +++ b/node/resolvers/utils/quotes.ts @@ -62,7 +62,6 @@ export const createQuoteObject = ({ note, sendToSalesRep, seller, - approvedBySeller, parentQuote, hasChildren, }: { @@ -76,7 +75,6 @@ export const createQuoteObject = ({ note: string sendToSalesRep: boolean seller?: string - approvedBySeller?: boolean | null parentQuote?: string | null hasChildren?: boolean | null }): Omit => { @@ -131,7 +129,6 @@ export const createQuoteObject = ({ viewedBySales: !sendToSalesRep, salesChannel, seller, - approvedBySeller, parentQuote, hasChildren, } diff --git a/node/typings.d.ts b/node/typings.d.ts index fc34590..00bba8b 100644 --- a/node/typings.d.ts +++ b/node/typings.d.ts @@ -16,7 +16,6 @@ interface Quote { viewedByCustomer: boolean salesChannel: string | null seller?: string | null - approvedBySeller?: boolean | null parentQuote?: string | null hasChildren?: boolean | null }