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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"supabase:dev": "supabase start --ignore-health-check",
"cmd:rename-qb-accounts": "tsx src/cmd/renameQbAccount/index.ts",
"patch-assembly-node-sdk": "cp ./lib-patches/assembly-js-node-sdk.js ./node_modules/@assembly-js/node-sdk/dist/api/init.js",
"cmd:backfill-product-info": "tsx src/cmd/backfillProductInfo/index.ts"
"cmd:backfill-product-info": "tsx src/cmd/backfillProductInfo/index.ts",
"cmd:sync-missed-invoices": "tsx src/cmd/syncMissedInvoices/index.ts"
},
"dependencies": {
"@sentry/nextjs": "^9.13.0",
Expand Down
3 changes: 2 additions & 1 deletion src/app/api/core/constants/limit.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const MAX_PRODUCT_LIST_LIMIT = 1_000
export const MAX_INVOICE_LIST_LIMIT = 10_000
export const MAX_INVOICE_LIST_LIMIT = 5_000
export const MAX_ASSEMBLY_RESOURCE_LIST_LIMIT = 10_000
55 changes: 52 additions & 3 deletions src/app/api/quickbooks/invoice/invoice.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,9 +685,12 @@ export class InvoiceService extends BaseService {
return acc + item.Amount
}, 0)
let actualTotalAmount = subtotal
const totalTax = parseFloat(
((subtotal * invoiceResource.taxPercentage) / 100).toFixed(2),
)
const totalTax =
(invoiceResource.taxPercentage
? parseFloat(
((subtotal * invoiceResource.taxPercentage) / 100).toFixed(2),
)
: invoiceResource.taxAmount) || 0

// check if invoice is paid. This needs to be done after actualTotalAmount and totalTax calculation to avoid miscalculation
if (invoiceResource.status === InvoiceStatus.PAID) {
Expand Down Expand Up @@ -1128,4 +1131,50 @@ export class InvoiceService extends BaseService {
)
return z.string().parse(incomeAccountRef)
}

async checkIfInvoiceExistsInQBO(
invoiceResource: InvoiceCreatedResponseType,
qbTokenInfo: IntuitAPITokensType,
): Promise<{ exists: boolean }> {
console.info(
'InvoiceService#checkIfInvoiceExistsInQBO | Checking if invoice exists in QBO',
)
const invoice = invoiceResource.data
const intuitApi = new IntuitAPI(qbTokenInfo)
const qbInvoice = await intuitApi.getInvoice(invoice.number)

if (!qbInvoice) {
console.info(
'InvoiceService#checkIfInvoiceExistsInQBO | No invoice found in QBO',
)
return { exists: false }
}

const customerService = new CustomerService(this.user)

const { recipientInfo } = await customerService.getRecipientInfo({
clientId: invoice.clientId,
companyId: invoice.companyId,
})

await this.logSync(
invoice.id,
{
qbInvoiceId: qbInvoice.Id,
invoiceNumber: invoice.number,
},
EventType.CREATED,
{
amount: (invoice.lineItems[0].amount * 100).toFixed(2),
taxAmount: invoice.taxAmount ? invoice.taxAmount.toFixed(2) : '0',
customerName: recipientInfo.displayName,
customerEmail: recipientInfo.email,
errorMessage: '',
},
)
console.info(
'InvoiceService#checkIfInvoiceExistsInQBO | Invoice exists in QBO',
)
return { exists: true }
}
}
6 changes: 6 additions & 0 deletions src/app/api/quickbooks/payment/payment.service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import APIError from '@/app/api/core/exceptions/api'
import User from '@/app/api/core/models/User.model'
import { BaseService } from '@/app/api/core/services/base.service'
import { SyncableEntity } from '@/app/api/core/types/invoice'
Expand Down Expand Up @@ -34,6 +35,7 @@ import {
} from '@/utils/synclog'
import dayjs from 'dayjs'
import { z } from 'zod'
import httpStatus from 'http-status'

export class PaymentService extends BaseService {
private syncLogService: SyncLogService
Expand Down Expand Up @@ -190,6 +192,10 @@ export class PaymentService extends BaseService {
invoice: InvoiceResponse | undefined,
): Promise<void> {
const paymentResource = parsedPaymentSucceedResource.data

if (!paymentResource.feeAmount)
throw new APIError(httpStatus.BAD_REQUEST, 'Fee amount is not found')

const intuitApi = new IntuitAPI(qbTokenInfo)
const tokenService = new TokenService(this.user)
const assetAccountRef = await tokenService.checkAndUpdateAccountStatus(
Expand Down
11 changes: 5 additions & 6 deletions src/app/api/quickbooks/webhook/webhook.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export class WebhookService extends BaseService {
payload: unknown,
qbTokenInfo: IntuitAPITokensType,
) {
await sleep(1000) // Payment succeed event can sometimes trigger before invoice created.
await sleep(7000) // Payment succeed event can sometimes trigger before invoice created.

console.info('###### PAYMENT SUCCEEDED ######')
const parsedPaymentSucceed =
Expand All @@ -397,8 +397,9 @@ export class WebhookService extends BaseService {
return
}
const parsedPaymentSucceedResource = parsedPaymentSucceed.data
const feeAmount = parsedPaymentSucceedResource.data.feeAmount

if (parsedPaymentSucceedResource.data.feeAmount.paidByPlatform > 0) {
if (feeAmount?.paidByPlatform && feeAmount.paidByPlatform > 0) {
// check if absorbed fee flag is true
const settingService = new SettingService(this.user)
const setting = await settingService.getOneByPortalId(['absorbedFeeFlag'])
Expand Down Expand Up @@ -444,17 +445,15 @@ export class WebhookService extends BaseService {
} catch (error: unknown) {
const errorWithCode = getMessageAndCodeFromError(error)
const errorMessage = errorWithCode.message
const feeAmount = parsedPaymentSucceedResource.data.feeAmount

await syncLogService.updateOrCreateQBSyncLog({
portalId: this.user.workspaceId,
entityType: EntityType.PAYMENT,
eventType: EventType.SUCCEEDED,
status: LogStatus.FAILED,
copilotId: parsedPaymentSucceedResource.data.id,
feeAmount:
parsedPaymentSucceedResource.data.feeAmount.paidByPlatform.toFixed(
2,
),
feeAmount: feeAmount ? feeAmount.paidByPlatform.toFixed(2) : '0',
remark: 'Absorbed fees',
qbItemName: 'Assembly Fees',
errorMessage,
Expand Down
68 changes: 68 additions & 0 deletions src/cmd/syncMissedInvoices/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import APIError from '@/app/api/core/exceptions/api'
import User from '@/app/api/core/models/User.model'
import { SyncMissedInvoicesService } from '@/cmd/syncMissedInvoices/syncMissedInvoices.service'
import { copilotAPIKey } from '@/config'
import { PortalConnectionWithSettingType } from '@/db/schema/qbPortalConnections'
import { getAllActivePortalConnections } from '@/db/service/token.service'
import { CopilotAPI } from '@/utils/copilotAPI'
import { encodePayload } from '@/utils/crypto'
import CustomLogger from '@/utils/logger'

/**
* This script is used to sync missed invoices that have payment records but no invoice records in QBO.
*/

// command to run the script: `yarn run cmd:sync-missed-invoices`
;(async function run() {
try {
console.info('SyncMissedInvoices#run | Starting sync missed invoices')
const activeConnections = await getAllActivePortalConnections()

if (!activeConnections.length) {
console.info('No active connection found')
process.exit(0)
}

for (const connection of activeConnections) {
if (!connection.setting?.syncFlag || !connection.setting?.isEnabled) {
console.info(
'Skipping connection: ' + JSON.stringify(connection.portalId),
)
continue
}

console.info(
`\n\n\n ########### Processing for PORTAL: ${connection.portalId} #############`,
)

await initiateProcess(connection)
}

console.info('\n Sync missed invoices completed successfully')
process.exit(0)
} catch (error) {
console.error(error)
process.exit(1)
}
})()

async function initiateProcess(connection: PortalConnectionWithSettingType) {
console.info('Generating token for the portal')
const payload = {
workspaceId: connection.portalId,
}
const token = encodePayload(copilotAPIKey, payload)

const copilot = new CopilotAPI(token)
const tokenPayload = await copilot.getTokenPayload()
CustomLogger.info({
obj: { copilotApiCronToken: token, tokenPayload },
message:
'syncMissedInvoices#initiateProcess | Copilot API token and payload',
})
if (!tokenPayload) throw new APIError(500, 'Encoded token is not valid')

const user = new User(token, tokenPayload)
const syncMissedService = new SyncMissedInvoicesService(user)
await syncMissedService.syncMissedInvoicesForPortal()
}
Loading
Loading