From 64a2789fb3a348e412d79e9b410a8bd7a2103ed2 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 5 Feb 2026 11:16:07 +0545 Subject: [PATCH 1/4] fix(OUT-3071): implement retry mechanisms when companyInfo is not available --- src/action/quickbooks.action.ts | 2 +- src/app/api/core/utils/withErrorHandler.ts | 5 ++++- src/app/api/core/utils/withRetry.ts | 5 +++++ src/type/dto/intuitAPI.dto.ts | 9 +++++++++ src/utils/error.ts | 11 +++++++++++ src/utils/intuitAPI.ts | 17 +++++++++++++---- 6 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/action/quickbooks.action.ts b/src/action/quickbooks.action.ts index f074cfee..ef225277 100644 --- a/src/action/quickbooks.action.ts +++ b/src/action/quickbooks.action.ts @@ -45,7 +45,7 @@ export async function checkForNonUsCompany(tokenInfo: IntuitAPITokensType) { message: 'checkForNonUsCompany | Company Info', }) - return companyInfo?.Country !== 'US' + return companyInfo.Country !== 'US' } export async function reconnectIfCta(type?: string) { diff --git a/src/app/api/core/utils/withErrorHandler.ts b/src/app/api/core/utils/withErrorHandler.ts index 64bbc65b..838b8bf5 100644 --- a/src/app/api/core/utils/withErrorHandler.ts +++ b/src/app/api/core/utils/withErrorHandler.ts @@ -9,7 +9,7 @@ import { NextRequest, NextResponse } from 'next/server' import { ZodError, ZodFormattedError } from 'zod' import { isAxiosError } from '@/app/api/core/exceptions/custom' import * as Sentry from '@sentry/nextjs' -import { IntuitAPIErrorMessage } from '@/utils/intuitAPI' +import { RetryableError } from '@/utils/error' type RequestHandler = (req: NextRequest, params: any) => Promise @@ -64,6 +64,9 @@ export const withErrorHandler = (handler: RequestHandler): RequestHandler => { status = error.status message = error.message || message errors = error.errors + } else if (error instanceof RetryableError) { + status = error.status + message = error.message || message } else if (error instanceof Error && error.message) { message = error.message } else if (isAxiosError(error)) { diff --git a/src/app/api/core/utils/withRetry.ts b/src/app/api/core/utils/withRetry.ts index 56210b5d..3a71b07a 100644 --- a/src/app/api/core/utils/withRetry.ts +++ b/src/app/api/core/utils/withRetry.ts @@ -1,6 +1,7 @@ import { StatusableError } from '@/type/CopilotApiError' import pRetry, { FailedAttemptError } from 'p-retry' import * as Sentry from '@sentry/nextjs' +import { RetryableError } from '@/utils/error' export const withRetry = async ( fn: (...args: any[]) => Promise, @@ -46,6 +47,10 @@ export const withRetry = async ( ) }, shouldRetry: (error: any) => { + if (error instanceof RetryableError) { + return error.retry + } + // Typecasting because Copilot doesn't export an error class const err = error as StatusableError // Retry only if statusCode === 429 diff --git a/src/type/dto/intuitAPI.dto.ts b/src/type/dto/intuitAPI.dto.ts index 3fd9924b..91d54741 100644 --- a/src/type/dto/intuitAPI.dto.ts +++ b/src/type/dto/intuitAPI.dto.ts @@ -200,3 +200,12 @@ export const QBDeletePayloadSchema = z.object({ }) export type QBDeletePayloadType = z.infer + +export const CompanyInfoSchema = z.object({ + CompanyInfo: z.array( + z.object({ + Country: z.string(), + }), + ), +}) +export type CompanyInfoType = z.infer diff --git a/src/utils/error.ts b/src/utils/error.ts index 5eaa6bb0..f4043689 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -40,3 +40,14 @@ export const getMessageAndCodeFromError = ( } return { message, code } } + +export class RetryableError extends Error { + readonly retry: boolean + readonly status: number + + constructor(status: number, message: string, retry: boolean) { + super(message) + this.retry = retry + this.status = status + } +} diff --git a/src/utils/intuitAPI.ts b/src/utils/intuitAPI.ts index 65e86b7a..0ac1f07b 100644 --- a/src/utils/intuitAPI.ts +++ b/src/utils/intuitAPI.ts @@ -21,7 +21,10 @@ import { QBAccountUpdatePayloadType, QBAccountResponseType, QBAccountResponseSchema, + CompanyInfoType, + CompanyInfoSchema, } from '@/type/dto/intuitAPI.dto' +import { RetryableError } from '@/utils/error' import CustomLogger from '@/utils/logger' import httpStatus from 'http-status' @@ -768,15 +771,13 @@ export default class IntuitAPI { return purchase } - async _getCompanyInfo() { + async _getCompanyInfo(): Promise { CustomLogger.info({ message: `IntuitAPI#getCompanyInfo | Company Info query start for realmId: ${this.tokens.intuitRealmId}.`, }) const query = `SELECT * FROM CompanyInfo maxresults 1` const companyInfo = await this.customQuery(query) - if (!companyInfo) return null - if (companyInfo?.Fault) { CustomLogger.error({ obj: companyInfo.Fault?.Error, message: 'Error: ' }) throw new APIError( @@ -786,7 +787,15 @@ export default class IntuitAPI { ) } - return companyInfo.CompanyInfo?.[0] + if (!companyInfo) + throw new RetryableError( + httpStatus.BAD_REQUEST, + 'No company info found', + true, + ) + + const parsedCompanyInfo = CompanyInfoSchema.parse(companyInfo) + return parsedCompanyInfo.CompanyInfo?.[0] } private wrapWithRetry( From ff1679c980e92945cf2c3b1745668056f4ab0b9d Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 5 Feb 2026 11:57:01 +0545 Subject: [PATCH 2/4] fix(OUT-3071): use correct status code --- src/utils/intuitAPI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/intuitAPI.ts b/src/utils/intuitAPI.ts index 0ac1f07b..6b9ee8a6 100644 --- a/src/utils/intuitAPI.ts +++ b/src/utils/intuitAPI.ts @@ -789,7 +789,7 @@ export default class IntuitAPI { if (!companyInfo) throw new RetryableError( - httpStatus.BAD_REQUEST, + httpStatus.NOT_FOUND, 'No company info found', true, ) From b3cf563fbadfae487903d824155c0d9ea003260a Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 5 Feb 2026 12:32:01 +0545 Subject: [PATCH 3/4] refactor(OUT-3071): move retry logic before error checking --- src/utils/intuitAPI.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/intuitAPI.ts b/src/utils/intuitAPI.ts index 6b9ee8a6..595d0354 100644 --- a/src/utils/intuitAPI.ts +++ b/src/utils/intuitAPI.ts @@ -778,6 +778,13 @@ export default class IntuitAPI { const query = `SELECT * FROM CompanyInfo maxresults 1` const companyInfo = await this.customQuery(query) + if (!companyInfo) + throw new RetryableError( + httpStatus.NOT_FOUND, + 'No company info found', + true, + ) + if (companyInfo?.Fault) { CustomLogger.error({ obj: companyInfo.Fault?.Error, message: 'Error: ' }) throw new APIError( @@ -787,13 +794,6 @@ export default class IntuitAPI { ) } - if (!companyInfo) - throw new RetryableError( - httpStatus.NOT_FOUND, - 'No company info found', - true, - ) - const parsedCompanyInfo = CompanyInfoSchema.parse(companyInfo) return parsedCompanyInfo.CompanyInfo?.[0] } From 7b79ed293ba3b78f063f763a7202395be71dd1d1 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 5 Feb 2026 13:09:15 +0545 Subject: [PATCH 4/4] refactor(OUT-3071): clean code - remove optional chaining --- src/utils/intuitAPI.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/intuitAPI.ts b/src/utils/intuitAPI.ts index 595d0354..6dd117f8 100644 --- a/src/utils/intuitAPI.ts +++ b/src/utils/intuitAPI.ts @@ -785,7 +785,7 @@ export default class IntuitAPI { true, ) - if (companyInfo?.Fault) { + if (companyInfo.Fault) { CustomLogger.error({ obj: companyInfo.Fault?.Error, message: 'Error: ' }) throw new APIError( companyInfo.Fault?.Error?.code || httpStatus.BAD_REQUEST, @@ -795,7 +795,7 @@ export default class IntuitAPI { } const parsedCompanyInfo = CompanyInfoSchema.parse(companyInfo) - return parsedCompanyInfo.CompanyInfo?.[0] + return parsedCompanyInfo.CompanyInfo[0] } private wrapWithRetry(