From c120c91f9574a1242f7755c9b84541a1cc129311 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Tue, 19 May 2026 15:38:05 +0545 Subject: [PATCH 1/4] fix(OUT-3754): correct duplicate doc number code from 6240 to 6140 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Intuit docs, QBO error 6140 is "Duplicate Document Number" and 6240 is "Duplicate Name Exists". isQBODuplicateDocNumberError was matching 6240, so the suffix-retry path in webhookInvoiceCreated never fired on real doc-number collisions — every one fell through to FAILED. Swap 6240 -> 6140 in the detector, the log/breadcrumb strings, JSDoc, and the docNumber unit + integration test fixtures. 6240 references that legitimately refer to Duplicate Name (customer DisplayName guard) or are generic examples are left alone. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/quickbooks/invoice/invoice.service.ts | 8 ++++---- src/app/api/quickbooks/invoice/invoice.utils.ts | 14 +++++++------- src/utils/intuitAPI.ts | 2 +- .../invoiceCreated/qboDocNumberCollision.test.ts | 2 +- .../invoice/invoice.service.docNumber.test.ts | 4 ++-- .../api/quickbooks/invoice/invoice.utils.test.ts | 14 +++++++------- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/app/api/quickbooks/invoice/invoice.service.ts b/src/app/api/quickbooks/invoice/invoice.service.ts index fd69504f..91d71718 100644 --- a/src/app/api/quickbooks/invoice/invoice.service.ts +++ b/src/app/api/quickbooks/invoice/invoice.service.ts @@ -550,7 +550,7 @@ export class InvoiceService extends BaseService { /** * Pre-flights QBO for invoices whose DocNumber starts with the Assembly * invoice number and returns the lowest free slot (``, `-1`, …). - * Used by webhookInvoiceCreated to dodge 6240 collisions when a customer + * Used by webhookInvoiceCreated to dodge 6140 collisions when a customer * has already created an invoice with the same DocNumber in QBO manually. */ private async resolveAvailableDocNumber( @@ -771,7 +771,7 @@ export class InvoiceService extends BaseService { customer?.Id || existingCustomer?.qbCustomerId // Resolve a DocNumber that won't collide in QBO. Pre-flight a prefix - // query, pick the lowest free slot (``, `-1`, `-2`, …). On 6240 + // query, pick the lowest free slot (``, `-1`, `-2`, …). On 6140 // race (customer manually created the slot we picked between our query // and our create), re-walk once and retry. After that, throw and let // resync handle it. @@ -820,13 +820,13 @@ export class InvoiceService extends BaseService { } catch (err) { if (!isQBODuplicateDocNumberError(err)) throw err console.info( - `InvoiceService#webhookInvoiceCreated | 6240 on DocNumber=${docNumber}; re-walking once`, + `InvoiceService#webhookInvoiceCreated | 6140 on DocNumber=${docNumber}; re-walking once`, ) docNumber = await this.resolveAvailableDocNumber( intuitApiService, assemblyInvoiceNumber, ) - addSyncBreadcrumb('Retrying invoice creation in QBO after 6240', { + addSyncBreadcrumb('Retrying invoice creation in QBO after 6140', { invoiceNumber: assemblyInvoiceNumber, docNumber, }) diff --git a/src/app/api/quickbooks/invoice/invoice.utils.ts b/src/app/api/quickbooks/invoice/invoice.utils.ts index 3fe4bf8d..c859ff3e 100644 --- a/src/app/api/quickbooks/invoice/invoice.utils.ts +++ b/src/app/api/quickbooks/invoice/invoice.utils.ts @@ -40,7 +40,7 @@ export const findNextAvailableDocNumber = ( } /** - * Recognizes QBO Error 6240 "Duplicate Document Number". Reads `.status`, + * Recognizes QBO Error 6140 "Duplicate Document Number". Reads `.status`, * `.code`, `errors[].code`, and message text so it works whether the caller * surfaces the parsed APIError directly or wraps/normalises it. */ @@ -60,16 +60,16 @@ export const isQBODuplicateDocNumberError = (err: unknown): boolean => { Detail?: string Message?: string } - if (fault.code === 6240 || fault.code === '6240') return true + if (fault.code === 6140 || fault.code === '6140') return true if ( - /6240|Duplicate Document Number/i.test(fault.Detail ?? '') || - /6240|Duplicate Document Number/i.test(fault.Message ?? '') + /6140|Duplicate Document Number/i.test(fault.Detail ?? '') || + /6140|Duplicate Document Number/i.test(fault.Message ?? '') ) { return true } } } - if (e.status === 6240 || e.status === '6240') return true - if (e.code === 6240 || e.code === '6240') return true - return /6240|Duplicate Document Number/i.test(e.message ?? '') + if (e.status === 6140 || e.status === '6140') return true + if (e.code === 6140 || e.code === '6140') return true + return /6140|Duplicate Document Number/i.test(e.message ?? '') } diff --git a/src/utils/intuitAPI.ts b/src/utils/intuitAPI.ts index 492292db..8acaa172 100644 --- a/src/utils/intuitAPI.ts +++ b/src/utils/intuitAPI.ts @@ -734,7 +734,7 @@ export default class IntuitAPI { * Returns all QBO invoices whose DocNumber starts with `prefix`. Used by * findNextAvailableDocNumber to detect collisions and pick the next free * suffix before createInvoice. Caps at maxresults=100; if a single prefix - * has more matches, the caller falls back to catch-6240 retry semantics. + * has more matches, the caller falls back to catch-6140 retry semantics. */ async _findInvoicesByDocNumberPrefix( prefix: string, diff --git a/test/integration/quickbooks/invoiceCreated/qboDocNumberCollision.test.ts b/test/integration/quickbooks/invoiceCreated/qboDocNumberCollision.test.ts index 64af9013..89de0580 100644 --- a/test/integration/quickbooks/invoiceCreated/qboDocNumberCollision.test.ts +++ b/test/integration/quickbooks/invoiceCreated/qboDocNumberCollision.test.ts @@ -68,7 +68,7 @@ describe('POST /api/quickbooks/webhook — invoice.created (an invoice with this const duplicateError = Object.assign( new Error('Duplicate Document Number'), { - errors: [{ code: '6240', Detail: 'Duplicate Document Number error' }], + errors: [{ code: '6140', Detail: 'Duplicate Document Number error' }], }, ) const createInvoice = vi diff --git a/test/unit/app/api/quickbooks/invoice/invoice.service.docNumber.test.ts b/test/unit/app/api/quickbooks/invoice/invoice.service.docNumber.test.ts index 0bcafa06..a9b20aef 100644 --- a/test/unit/app/api/quickbooks/invoice/invoice.service.docNumber.test.ts +++ b/test/unit/app/api/quickbooks/invoice/invoice.service.docNumber.test.ts @@ -1,7 +1,7 @@ /** * Unit coverage for InvoiceService#resolveAvailableDocNumber — the pre-flight - * helper used by webhookInvoiceCreated to dodge 6240 collisions. The public - * retry-after-6240 branch is covered end-to-end in + * helper used by webhookInvoiceCreated to dodge 6140 collisions. The public + * retry-after-6140 branch is covered end-to-end in * test/integration/quickbooks/invoiceCreated/qboDocNumberCollision.test.ts; * this file pins the focused pieces in isolation. * diff --git a/test/unit/app/api/quickbooks/invoice/invoice.utils.test.ts b/test/unit/app/api/quickbooks/invoice/invoice.utils.test.ts index 3c27128c..84334da3 100644 --- a/test/unit/app/api/quickbooks/invoice/invoice.utils.test.ts +++ b/test/unit/app/api/quickbooks/invoice/invoice.utils.test.ts @@ -81,13 +81,13 @@ describe('findNextAvailableDocNumber', () => { describe('isQBODuplicateDocNumberError', () => { it('matches the real APIError shape thrown by intuitAPI._createInvoice', () => { // This is the actual shape: status=400, message=boilerplate, errors=array - // of QBO fault objects. The 6240 code lives in errors[i].code. + // of QBO fault objects. The 6140 code lives in errors[i].code. const realApiError = { status: 400, message: '#IntuitAPIErrorMessage#createInvoice', errors: [ { - code: '6240', + code: '6140', Message: 'Duplicate Document Number Error', Detail: 'Duplicate Document Number Error : You must specify a different number. This number has already been used.', @@ -102,7 +102,7 @@ describe('isQBODuplicateDocNumberError', () => { expect( isQBODuplicateDocNumberError({ status: 400, - errors: [{ code: 6240 }], + errors: [{ code: 6140 }], }), ).toBe(true) }) @@ -116,12 +116,12 @@ describe('isQBODuplicateDocNumberError', () => { ).toBe(true) }) - it('matches top-level .status as 6240 (defense-in-depth)', () => { - expect(isQBODuplicateDocNumberError({ status: 6240 })).toBe(true) + it('matches top-level .status as 6140 (defense-in-depth)', () => { + expect(isQBODuplicateDocNumberError({ status: 6140 })).toBe(true) }) - it('matches top-level .code as 6240 (defense-in-depth)', () => { - expect(isQBODuplicateDocNumberError({ code: '6240' })).toBe(true) + it('matches top-level .code as 6140 (defense-in-depth)', () => { + expect(isQBODuplicateDocNumberError({ code: '6140' })).toBe(true) }) it('matches top-level .message text (defense-in-depth)', () => { From bddfada0898768df0a11d0d82749ee68eaeaca2b Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Tue, 19 May 2026 15:55:05 +0545 Subject: [PATCH 2/4] refactor(OUT-3754): replace hardcoded QBO 6140 with QBOErrorCodes constant - Extend src/constant/intuitErrorCode.ts with a named QBOErrorCodes map covering DUPLICATE_DOC_NUMBER, DUPLICATE_NAME_EXISTS, BUSINESS_VALIDATION, and ACCOUNT_SUSPENDED. Derive the existing AccountErrorCodes array from it to avoid drift. - Update isQBODuplicateDocNumberError to derive its numeric, string, and regex checks from QBOErrorCodes.DUPLICATE_DOC_NUMBER. - Reference the constant from both the unit recognizer tests and the integration test's mock 6140 fault envelope. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/quickbooks/invoice/invoice.utils.ts | 30 ++++++++++++++----- src/constant/intuitErrorCode.ts | 15 ++++++++-- .../qboDocNumberCollision.test.ts | 8 ++++- .../quickbooks/invoice/invoice.utils.test.ts | 22 +++++++++----- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/app/api/quickbooks/invoice/invoice.utils.ts b/src/app/api/quickbooks/invoice/invoice.utils.ts index c859ff3e..b8fc638f 100644 --- a/src/app/api/quickbooks/invoice/invoice.utils.ts +++ b/src/app/api/quickbooks/invoice/invoice.utils.ts @@ -1,3 +1,5 @@ +import { QBOErrorCodes } from '@/constant/intuitErrorCode' + export const formatAssemblyInvoicePrivateNote = ( invoiceNumber: string, ): string => `Assembly invoice: ${invoiceNumber}` @@ -39,8 +41,15 @@ export const findNextAvailableDocNumber = ( ) } +const DUP_DOC_NUMBER_CODE = QBOErrorCodes.DUPLICATE_DOC_NUMBER +const DUP_DOC_NUMBER_CODE_STR = String(DUP_DOC_NUMBER_CODE) +const DUP_DOC_NUMBER_PATTERN = new RegExp( + `${DUP_DOC_NUMBER_CODE}|Duplicate Document Number`, + 'i', // case insensitive +) + /** - * Recognizes QBO Error 6140 "Duplicate Document Number". Reads `.status`, + * Recognizes QBO's Duplicate Document Number error. Reads `.status`, * `.code`, `errors[].code`, and message text so it works whether the caller * surfaces the parsed APIError directly or wraps/normalises it. */ @@ -60,16 +69,23 @@ export const isQBODuplicateDocNumberError = (err: unknown): boolean => { Detail?: string Message?: string } - if (fault.code === 6140 || fault.code === '6140') return true if ( - /6140|Duplicate Document Number/i.test(fault.Detail ?? '') || - /6140|Duplicate Document Number/i.test(fault.Message ?? '') + fault.code === DUP_DOC_NUMBER_CODE || + fault.code === DUP_DOC_NUMBER_CODE_STR + ) { + return true + } + if ( + DUP_DOC_NUMBER_PATTERN.test(fault.Detail ?? '') || + DUP_DOC_NUMBER_PATTERN.test(fault.Message ?? '') ) { return true } } } - if (e.status === 6140 || e.status === '6140') return true - if (e.code === 6140 || e.code === '6140') return true - return /6140|Duplicate Document Number/i.test(e.message ?? '') + if (e.status === DUP_DOC_NUMBER_CODE || e.status === DUP_DOC_NUMBER_CODE_STR) + return true + if (e.code === DUP_DOC_NUMBER_CODE || e.code === DUP_DOC_NUMBER_CODE_STR) + return true + return DUP_DOC_NUMBER_PATTERN.test(e.message ?? '') } diff --git a/src/constant/intuitErrorCode.ts b/src/constant/intuitErrorCode.ts index f142f737..db1718a8 100644 --- a/src/constant/intuitErrorCode.ts +++ b/src/constant/intuitErrorCode.ts @@ -1,7 +1,16 @@ // Doc: https://developer.intuit.com/app/developer/qbo/docs/develop/troubleshooting/error-codes -export const AccountErrorCodes = [ - 6190, // account suspended - 6000, // business validation error +export const QBOErrorCodes = { + BUSINESS_VALIDATION: 6000, + DUPLICATE_DOC_NUMBER: 6140, + ACCOUNT_SUSPENDED: 6190, + DUPLICATE_NAME_EXISTS: 6240, // customer/vendor/employee name collision +} as const + +export type QBOErrorCode = (typeof QBOErrorCodes)[keyof typeof QBOErrorCodes] + +export const AccountErrorCodes: readonly number[] = [ + QBOErrorCodes.ACCOUNT_SUSPENDED, + QBOErrorCodes.BUSINESS_VALIDATION, ] export const OAuthErrorCodes = { diff --git a/test/integration/quickbooks/invoiceCreated/qboDocNumberCollision.test.ts b/test/integration/quickbooks/invoiceCreated/qboDocNumberCollision.test.ts index 89de0580..608eee3b 100644 --- a/test/integration/quickbooks/invoiceCreated/qboDocNumberCollision.test.ts +++ b/test/integration/quickbooks/invoiceCreated/qboDocNumberCollision.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest' import { db } from '@/db' import { QBInvoiceSync } from '@/db/schema/qbInvoiceSync' +import { QBOErrorCodes } from '@/constant/intuitErrorCode' import invoiceCreatedPayload from '@test/fixtures/invoiceCreated.webhook' import { @@ -68,7 +69,12 @@ describe('POST /api/quickbooks/webhook — invoice.created (an invoice with this const duplicateError = Object.assign( new Error('Duplicate Document Number'), { - errors: [{ code: '6140', Detail: 'Duplicate Document Number error' }], + errors: [ + { + code: String(QBOErrorCodes.DUPLICATE_DOC_NUMBER), + Detail: 'Duplicate Document Number error', + }, + ], }, ) const createInvoice = vi diff --git a/test/unit/app/api/quickbooks/invoice/invoice.utils.test.ts b/test/unit/app/api/quickbooks/invoice/invoice.utils.test.ts index 84334da3..e01e3058 100644 --- a/test/unit/app/api/quickbooks/invoice/invoice.utils.test.ts +++ b/test/unit/app/api/quickbooks/invoice/invoice.utils.test.ts @@ -5,6 +5,10 @@ import { isQBODuplicateDocNumberError, MAX_SUFFIX_ATTEMPTS, } from '@/app/api/quickbooks/invoice/invoice.utils' +import { QBOErrorCodes } from '@/constant/intuitErrorCode' + +const DUP_DOC_NUMBER = QBOErrorCodes.DUPLICATE_DOC_NUMBER +const DUP_DOC_NUMBER_STR = String(DUP_DOC_NUMBER) describe('formatAssemblyInvoicePrivateNote', () => { it('formats an invoice number into the canonical PrivateNote string', () => { @@ -80,14 +84,14 @@ describe('findNextAvailableDocNumber', () => { describe('isQBODuplicateDocNumberError', () => { it('matches the real APIError shape thrown by intuitAPI._createInvoice', () => { - // This is the actual shape: status=400, message=boilerplate, errors=array - // of QBO fault objects. The 6140 code lives in errors[i].code. + // Actual shape: status=400, message=boilerplate, errors=array of QBO + // fault objects. The duplicate code lives in errors[i].code. const realApiError = { status: 400, message: '#IntuitAPIErrorMessage#createInvoice', errors: [ { - code: '6140', + code: DUP_DOC_NUMBER_STR, Message: 'Duplicate Document Number Error', Detail: 'Duplicate Document Number Error : You must specify a different number. This number has already been used.', @@ -102,7 +106,7 @@ describe('isQBODuplicateDocNumberError', () => { expect( isQBODuplicateDocNumberError({ status: 400, - errors: [{ code: 6140 }], + errors: [{ code: DUP_DOC_NUMBER }], }), ).toBe(true) }) @@ -116,12 +120,14 @@ describe('isQBODuplicateDocNumberError', () => { ).toBe(true) }) - it('matches top-level .status as 6140 (defense-in-depth)', () => { - expect(isQBODuplicateDocNumberError({ status: 6140 })).toBe(true) + it('matches the duplicate code at the top-level .status (defense-in-depth)', () => { + expect(isQBODuplicateDocNumberError({ status: DUP_DOC_NUMBER })).toBe(true) }) - it('matches top-level .code as 6140 (defense-in-depth)', () => { - expect(isQBODuplicateDocNumberError({ code: '6140' })).toBe(true) + it('matches the duplicate code at the top-level .code (defense-in-depth)', () => { + expect(isQBODuplicateDocNumberError({ code: DUP_DOC_NUMBER_STR })).toBe( + true, + ) }) it('matches top-level .message text (defense-in-depth)', () => { From 54caf46d7a4e52651dc094d0dea3edbce3963477 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Tue, 19 May 2026 16:07:53 +0545 Subject: [PATCH 3/4] refactor(OUT-3754): simplify isQBODuplicateDocNumberError numeric checks Number() collapses the string + number branches into one comparison since Number('6140') === Number(6140) and unparseable inputs return NaN (never equal to the target). Drops the redundant DUP_DOC_NUMBER_CODE_STR constant and merges the two top-level .status / .code checks into a single short-circuiting expression. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/quickbooks/invoice/invoice.utils.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/app/api/quickbooks/invoice/invoice.utils.ts b/src/app/api/quickbooks/invoice/invoice.utils.ts index b8fc638f..066d52cf 100644 --- a/src/app/api/quickbooks/invoice/invoice.utils.ts +++ b/src/app/api/quickbooks/invoice/invoice.utils.ts @@ -42,7 +42,6 @@ export const findNextAvailableDocNumber = ( } const DUP_DOC_NUMBER_CODE = QBOErrorCodes.DUPLICATE_DOC_NUMBER -const DUP_DOC_NUMBER_CODE_STR = String(DUP_DOC_NUMBER_CODE) const DUP_DOC_NUMBER_PATTERN = new RegExp( `${DUP_DOC_NUMBER_CODE}|Duplicate Document Number`, 'i', // case insensitive @@ -69,12 +68,7 @@ export const isQBODuplicateDocNumberError = (err: unknown): boolean => { Detail?: string Message?: string } - if ( - fault.code === DUP_DOC_NUMBER_CODE || - fault.code === DUP_DOC_NUMBER_CODE_STR - ) { - return true - } + if (Number(fault.code) === DUP_DOC_NUMBER_CODE) return true if ( DUP_DOC_NUMBER_PATTERN.test(fault.Detail ?? '') || DUP_DOC_NUMBER_PATTERN.test(fault.Message ?? '') @@ -83,9 +77,10 @@ export const isQBODuplicateDocNumberError = (err: unknown): boolean => { } } } - if (e.status === DUP_DOC_NUMBER_CODE || e.status === DUP_DOC_NUMBER_CODE_STR) - return true - if (e.code === DUP_DOC_NUMBER_CODE || e.code === DUP_DOC_NUMBER_CODE_STR) + if ( + Number(e.status) === DUP_DOC_NUMBER_CODE || + Number(e.code) === DUP_DOC_NUMBER_CODE + ) return true return DUP_DOC_NUMBER_PATTERN.test(e.message ?? '') } From 71bdb9058f52ab8069b0d8d16d29d42e68b922d4 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Tue, 19 May 2026 16:10:42 +0545 Subject: [PATCH 4/4] chore(OUT-3754): address review nits on QBO error-code refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - intuitAPI.ts: swap the JSDoc example fault code from 6240 to 6140 so it doesn't conflate with QBOErrorCodes.DUPLICATE_NAME_EXISTS. - invoice.utils.ts: document that DUP_DOC_NUMBER_PATTERN relies on QBOErrorCode being a numeric literal union — no regex escaping needed today, but the comment flags the assumption for future maintainers. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/quickbooks/invoice/invoice.utils.ts | 3 +++ src/utils/intuitAPI.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/api/quickbooks/invoice/invoice.utils.ts b/src/app/api/quickbooks/invoice/invoice.utils.ts index 066d52cf..8fd7c3e1 100644 --- a/src/app/api/quickbooks/invoice/invoice.utils.ts +++ b/src/app/api/quickbooks/invoice/invoice.utils.ts @@ -42,6 +42,9 @@ export const findNextAvailableDocNumber = ( } const DUP_DOC_NUMBER_CODE = QBOErrorCodes.DUPLICATE_DOC_NUMBER +// QBOErrorCode is a numeric literal union, so the interpolation below has +// no regex-special characters and needs no escaping. If that type ever +// widens to a string, escape the interpolation before using it here. const DUP_DOC_NUMBER_PATTERN = new RegExp( `${DUP_DOC_NUMBER_CODE}|Duplicate Document Number`, 'i', // case insensitive diff --git a/src/utils/intuitAPI.ts b/src/utils/intuitAPI.ts index 8acaa172..c8d3f5c2 100644 --- a/src/utils/intuitAPI.ts +++ b/src/utils/intuitAPI.ts @@ -68,7 +68,7 @@ export type IntuitAPITokensType = Pick< export const IntuitAPIErrorMessage = '#IntuitAPIErrorMessage#' -// APIError.status carries the QBO fault code (e.g. 6240) when available, +// APIError.status carries the QBO fault code (e.g. 6140) when available, // else BAD_REQUEST. withErrorHandler clamps non-HTTP codes downstream. export function assertNotQBFault(raw: unknown, opName: string): void { const result = QBFaultSchema.safeParse(raw)