diff --git a/src/app/api/quickbooks/customer/customer.service.ts b/src/app/api/quickbooks/customer/customer.service.ts index 948603c..7446f58 100644 --- a/src/app/api/quickbooks/customer/customer.service.ts +++ b/src/app/api/quickbooks/customer/customer.service.ts @@ -11,8 +11,11 @@ import { QBCustomerUpdateSchemaType, } from '@/db/schema/qbCustomers' import { CompanyResponse, WhereClause } from '@/type/common' +import { QBCustomerCreatePayloadType } from '@/type/dto/intuitAPI.dto' +import { InvoiceCreatedResponseType } from '@/type/dto/webhook.dto' import { CopilotAPI } from '@/utils/copilotAPI' import IntuitAPI from '@/utils/intuitAPI' +import { replaceSpecialCharsForQB } from '@/utils/string' import { and, eq, isNull } from 'drizzle-orm' import httpStatus from 'http-status' @@ -262,7 +265,7 @@ export class CustomerService extends BaseService { Active: true, sparse: true, }) - customer = updateRes.Customer + customer = updateRes } // 2. update sync token in customer sync table @@ -319,4 +322,70 @@ export class CustomerService extends BaseService { 'displayName', ]) } + + async findOrCreateCustomer({ + intuitApiService, + recipientInfo, + companyInfo, + invoiceResource, + }: { + intuitApiService: IntuitAPI + recipientInfo: ClientCompanyType + companyInfo: CompanyResponse | undefined + invoiceResource: InvoiceCreatedResponseType['data'] + }) { + const displayName = recipientInfo.displayName + // 2.1. search client in qb using recipient's email + let customer = await intuitApiService.getCustomerByEmail( + recipientInfo.email, + ) + + // 3. if not found, create a new client in the QB + if (!customer) { + console.info( + `InvoiceService#WebhookInvoiceCreated | Customer named ${recipientInfo.displayName} not found in QB. Creating new customer...`, + ) + // Create a new customer in QB + let customerPayload: QBCustomerCreatePayloadType = { + DisplayName: displayName, + CompanyName: companyInfo && replaceSpecialCharsForQB(companyInfo.name), + PrimaryEmailAddr: { + Address: recipientInfo?.email || '', + }, + } + + if (recipientInfo.givenName && recipientInfo.familyName) { + customerPayload = { + ...customerPayload, + GivenName: replaceSpecialCharsForQB(recipientInfo.givenName), + FamilyName: replaceSpecialCharsForQB(recipientInfo.familyName), + } + } + + const customerRes = await intuitApiService.createCustomer(customerPayload) + customer = customerRes + + console.info( + `InvoiceService#WebhookInvoiceCreated | Customer created in QB with ID: ${customer.Id}.`, + ) + } + + // create map for customer into mapping table + const customerSync = await this.createQBCustomer({ + portalId: this.user.workspaceId, + customerId: recipientInfo.recipientId, // TODO: remove everything related to this field. in case anything goes off the track + clientCompanyId: recipientInfo.clientCompanyId, + clientId: invoiceResource.clientId || null, + companyId: invoiceResource.companyId || null, + givenName: recipientInfo.givenName, + familyName: recipientInfo.familyName, + displayName: recipientInfo.displayName, + email: recipientInfo.email, + companyName: companyInfo?.name, + qbSyncToken: customer.SyncToken, + qbCustomerId: customer.Id, + }) + + return { customer, customerSyncId: customerSync.id } + } } diff --git a/src/app/api/quickbooks/invoice/invoice.service.ts b/src/app/api/quickbooks/invoice/invoice.service.ts index 9378484..01e9163 100644 --- a/src/app/api/quickbooks/invoice/invoice.service.ts +++ b/src/app/api/quickbooks/invoice/invoice.service.ts @@ -28,7 +28,6 @@ import { QBSyncLog } from '@/db/schema/qbSyncLogs' import { TransactionType, WhereClause } from '@/type/common' import { QBCustomerSparseUpdatePayloadType, - QBCustomerCreatePayloadType, QBDestructiveInvoicePayloadSchema, QBNameValueSchemaType, QBInvoiceLineItemSchemaType, @@ -573,64 +572,14 @@ export class InvoiceService extends BaseService { let customer, existingCustomerMapId = existingCustomer?.id if (!existingCustomer) { - // 2.1. search client in qb using client's given name and family name - customer = await intuitApiService.getACustomer( - replaceSpecialCharsForQB(recipientInfo.displayName), - undefined, - true, - ) - - // 3. if not found, create a new client in the QB - if (!customer) { - console.info( - `InvoiceService#WebhookInvoiceCreated | Customer named ${recipientInfo.displayName} not found in QB. Creating new customer...`, - ) - // Create a new customer in QB - let customerPayload: QBCustomerCreatePayloadType = { - DisplayName: replaceSpecialCharsForQB(recipientInfo.displayName), - CompanyName: - companyInfo && replaceSpecialCharsForQB(companyInfo.name), - PrimaryEmailAddr: { - Address: recipientInfo?.email || '', - }, - } - - if (recipientInfo.givenName && recipientInfo.familyName) { - customerPayload = { - ...customerPayload, - GivenName: replaceSpecialCharsForQB(recipientInfo.givenName), - FamilyName: replaceSpecialCharsForQB(recipientInfo.familyName), - } - } - - const customerRes = - await intuitApiService.createCustomer(customerPayload) - customer = customerRes.Customer - - console.info( - `InvoiceService#WebhookInvoiceCreated | Customer created in QB with ID: ${customer.Id}.`, - ) - } - - // create map for customer into mapping table - const customerSyncPayload = { - portalId: this.user.workspaceId, - customerId: recipientInfo.recipientId, // TODO: remove everything related to this field. in case anything goes off the track - clientCompanyId: recipientInfo.clientCompanyId, - clientId: invoiceResource.clientId || null, - companyId: invoiceResource.companyId || null, - givenName: recipientInfo.givenName, - familyName: recipientInfo.familyName, - displayName: recipientInfo.displayName, - email: recipientInfo.email, - companyName: companyInfo?.name, - qbSyncToken: customer.SyncToken, - qbCustomerId: customer.Id, - } - - const customerSync = - await customerService.createQBCustomer(customerSyncPayload) - existingCustomerMapId = customerSync.id + const customerWName = await customerService.findOrCreateCustomer({ + intuitApiService, + recipientInfo, + companyInfo, + invoiceResource, + }) + customer = customerWName.customer + existingCustomerMapId = customerWName.customerSyncId } else { console.info('InvoiceService#webhookInvoiceCreated. Customer exists.') @@ -645,20 +594,26 @@ export class InvoiceService extends BaseService { Address: recipientInfo.email, } } - if (existingCustomer.displayName !== recipientInfo.displayName) { - // DisplayName = GivenName + FamilyName + CompanyName (if exists) - sparseUpdatePayload.DisplayName = replaceSpecialCharsForQB( - recipientInfo.displayName, - ) - sparseUpdatePayload.GivenName = replaceSpecialCharsForQB( - recipientInfo.givenName, - ) - sparseUpdatePayload.FamilyName = replaceSpecialCharsForQB( - recipientInfo.familyName, - ) + // if (existingCustomer.displayName !== recipientInfo.displayName) { + // // DisplayName = GivenName + FamilyName + CompanyName (if exists) + // sparseUpdatePayload.DisplayName = replaceSpecialCharsForQB( + // recipientInfo.displayName, + // ) + // sparseUpdatePayload.GivenName = replaceSpecialCharsForQB( + // recipientInfo.givenName, + // ) + // sparseUpdatePayload.FamilyName = replaceSpecialCharsForQB( + // recipientInfo.familyName, + // ) + // sparseUpdatePayload.CompanyName = + // companyInfo && replaceSpecialCharsForQB(companyInfo.name) + // } + + if (existingCustomer.companyName !== companyInfo?.name) { sparseUpdatePayload.CompanyName = companyInfo && replaceSpecialCharsForQB(companyInfo.name) } + if (Object.keys(sparseUpdatePayload).length > 0) { const customerSparsePayload = { ...sparseUpdatePayload, @@ -673,10 +628,9 @@ export class InvoiceService extends BaseService { sparse: true as const, } - const customerRes = await intuitApiService.customerSparseUpdate( + customer = await intuitApiService.customerSparseUpdate( customerSparsePayload, ) - customer = customerRes.Customer // update the customer map in our table const customerSyncUpPayload = { diff --git a/src/type/dto/intuitAPI.dto.ts b/src/type/dto/intuitAPI.dto.ts index 91d5474..06b2f75 100644 --- a/src/type/dto/intuitAPI.dto.ts +++ b/src/type/dto/intuitAPI.dto.ts @@ -209,3 +209,18 @@ export const CompanyInfoSchema = z.object({ ), }) export type CompanyInfoType = z.infer + +export const CustomerQueryResponseSchema = z.object({ + Id: z.string(), + SyncToken: z.string(), + Active: z.boolean(), + PrimaryEmailAddr: z + .object({ + Address: z.string(), + }) + .optional(), +}) + +export type CustomerQueryResponseType = z.infer< + typeof CustomerQueryResponseSchema +> diff --git a/src/utils/intuitAPI.ts b/src/utils/intuitAPI.ts index 6dd117f..ac61b93 100644 --- a/src/utils/intuitAPI.ts +++ b/src/utils/intuitAPI.ts @@ -23,6 +23,8 @@ import { QBAccountResponseSchema, CompanyInfoType, CompanyInfoSchema, + CustomerQueryResponseType, + CustomerQueryResponseSchema, } from '@/type/dto/intuitAPI.dto' import { RetryableError } from '@/utils/error' import CustomLogger from '@/utils/logger' @@ -158,7 +160,9 @@ export default class IntuitAPI { return invoice } - async _createCustomer(payload: QBCustomerCreatePayloadType) { + async _createCustomer( + payload: QBCustomerCreatePayloadType, + ): Promise { CustomLogger.info({ obj: { payload }, message: `IntuitAPI#createCustomer | customer creation start for realmId: ${this.tokens.intuitRealmId}. Payload: `, @@ -185,7 +189,7 @@ export default class IntuitAPI { obj: { response: customer.Customer }, message: `IntuitAPI#createCustomer | customer created with name = ${customer.Customer?.FullyQualifiedName}.`, }) - return customer + return customer.Customer } async _createItem(payload: QBItemCreatePayloadType) { @@ -253,17 +257,17 @@ export default class IntuitAPI { displayName: string, id?: undefined, includeInactive?: boolean, - ): Promise + ): Promise async _getACustomer( displayName: undefined, id: string, includeInactive?: boolean, - ): Promise + ): Promise async _getACustomer( displayName: string, id: string, includeInactive?: boolean, - ): Promise + ): Promise async _getACustomer( displayName?: string, id?: string, @@ -286,7 +290,7 @@ export default class IntuitAPI { CustomLogger.info({ message: `IntuitAPI#getACustomer | Customer query start for realmId: ${this.tokens.intuitRealmId}. Name: ${displayName}, Id: ${id}`, }) - const customerQuery = `SELECT Id, SyncToken, Active FROM Customer WHERE ${queryCondition}` + const customerQuery = `SELECT Id, SyncToken, Active, PrimaryEmailAddr FROM Customer WHERE ${queryCondition}` const qbCustomers = await this.customQuery(customerQuery) if (!qbCustomers) return null @@ -300,7 +304,33 @@ export default class IntuitAPI { ) } - return qbCustomers.Customer?.[0] + if (!qbCustomers.Customer) return + return CustomerQueryResponseSchema.parse(qbCustomers.Customer[0]) + } + + async _getCustomerByEmail( + email: string, + ): Promise { + CustomLogger.info({ + obj: { email }, + message: `IntuitAPI#getCustomerByEmail | Customer query start for realmId: ${this.tokens.intuitRealmId}. Email: ${email}`, + }) + const customerQuery = `SELECT Id, SyncToken, Active, PrimaryEmailAddr FROM Customer WHERE PrimaryEmailAddr = '${email}' AND Active in (true, false)` + const qbCustomers = await this.customQuery(customerQuery) + + if (!qbCustomers) return + + if (qbCustomers?.Fault) { + CustomLogger.error({ obj: qbCustomers.Fault?.Error, message: 'Error: ' }) + throw new APIError( + qbCustomers.Fault?.Error?.code || httpStatus.BAD_REQUEST, + `${IntuitAPIErrorMessage}getCustomerByEmail`, + qbCustomers.Fault?.Error, + ) + } + + if (!qbCustomers.Customer) return + return CustomerQueryResponseSchema.parse(qbCustomers.Customer[0]) } /** @@ -411,7 +441,9 @@ export default class IntuitAPI { return invoice } - async _customerSparseUpdate(payload: QBCustomerSparseUpdatePayloadType) { + async _customerSparseUpdate( + payload: QBCustomerSparseUpdatePayloadType, + ): Promise { CustomLogger.info({ obj: { payload }, message: `IntuitAPI#customerSparseUpdate | customer sparse update start for realmId: ${this.tokens.intuitRealmId}. `, @@ -438,7 +470,7 @@ export default class IntuitAPI { obj: { response: customer.Customer }, message: `IntuitAPI#customerSparseUpdate | customer sparse updated with name = ${customer.Customer?.FullyQualifiedName}. `, }) - return customer + return customer.Customer } async _itemFullUpdate( @@ -814,18 +846,19 @@ export default class IntuitAPI { displayName: string, id?: undefined, includeInactive?: boolean, - ): Promise + ): Promise ( displayName: undefined, id: string, includeInactive?: boolean, - ): Promise + ): Promise ( displayName: string, id: string, includeInactive?: boolean, - ): Promise + ): Promise } = this.wrapWithRetry(this._getACustomer) as any + getCustomerByEmail = this.wrapWithRetry(this._getCustomerByEmail) getAnItem: { ( name: string,