Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion src/app/api/quickbooks/customer/customer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }
}
}
98 changes: 26 additions & 72 deletions src/app/api/quickbooks/invoice/invoice.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import { QBSyncLog } from '@/db/schema/qbSyncLogs'
import { TransactionType, WhereClause } from '@/type/common'
import {
QBCustomerSparseUpdatePayloadType,
QBCustomerCreatePayloadType,
QBDestructiveInvoicePayloadSchema,
QBNameValueSchemaType,
QBInvoiceLineItemSchemaType,
Expand Down Expand Up @@ -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.')

Expand All @@ -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,
Expand All @@ -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 = {
Expand Down
15 changes: 15 additions & 0 deletions src/type/dto/intuitAPI.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,18 @@ export const CompanyInfoSchema = z.object({
),
})
export type CompanyInfoType = z.infer<typeof CompanyInfoSchema>

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
>
57 changes: 45 additions & 12 deletions src/utils/intuitAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -158,7 +160,9 @@ export default class IntuitAPI {
return invoice
}

async _createCustomer(payload: QBCustomerCreatePayloadType) {
async _createCustomer(
payload: QBCustomerCreatePayloadType,
): Promise<CustomerQueryResponseType> {
CustomLogger.info({
obj: { payload },
message: `IntuitAPI#createCustomer | customer creation start for realmId: ${this.tokens.intuitRealmId}. Payload: `,
Expand All @@ -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) {
Expand Down Expand Up @@ -253,17 +257,17 @@ export default class IntuitAPI {
displayName: string,
id?: undefined,
includeInactive?: boolean,
): Promise<BaseResponseType>
): Promise<CustomerQueryResponseType>
async _getACustomer(
displayName: undefined,
id: string,
includeInactive?: boolean,
): Promise<BaseResponseType>
): Promise<CustomerQueryResponseType>
async _getACustomer(
displayName: string,
id: string,
includeInactive?: boolean,
): Promise<BaseResponseType>
): Promise<CustomerQueryResponseType>
async _getACustomer(
displayName?: string,
id?: string,
Expand All @@ -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
Expand All @@ -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<CustomerQueryResponseType | undefined> {
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])
}

/**
Expand Down Expand Up @@ -411,7 +441,9 @@ export default class IntuitAPI {
return invoice
}

async _customerSparseUpdate(payload: QBCustomerSparseUpdatePayloadType) {
async _customerSparseUpdate(
payload: QBCustomerSparseUpdatePayloadType,
): Promise<CustomerQueryResponseType> {
CustomLogger.info({
obj: { payload },
message: `IntuitAPI#customerSparseUpdate | customer sparse update start for realmId: ${this.tokens.intuitRealmId}. `,
Expand All @@ -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(
Expand Down Expand Up @@ -814,18 +846,19 @@ export default class IntuitAPI {
displayName: string,
id?: undefined,
includeInactive?: boolean,
): Promise<BaseResponseType>
): Promise<CustomerQueryResponseType>
(
displayName: undefined,
id: string,
includeInactive?: boolean,
): Promise<BaseResponseType>
): Promise<CustomerQueryResponseType>
(
displayName: string,
id: string,
includeInactive?: boolean,
): Promise<BaseResponseType>
): Promise<CustomerQueryResponseType>
} = this.wrapWithRetry(this._getACustomer) as any
getCustomerByEmail = this.wrapWithRetry(this._getCustomerByEmail)
getAnItem: {
(
name: string,
Expand Down
Loading