From 4d90ca4a1ba1cefb9fbe49625e9d5c9c5c643ce6 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Tue, 7 Apr 2026 16:21:07 +0545 Subject: [PATCH] fix(OUT-3527): allow single quotes in names when querying QBO Preserve apostrophes in customer, item, and account names instead of replacing them with hyphens, and escape them in QBO query strings to prevent syntax errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/utils/intuitAPI.ts | 13 ++++++++----- src/utils/string.ts | 10 +++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/utils/intuitAPI.ts b/src/utils/intuitAPI.ts index 33ed520e..a5b74ccc 100644 --- a/src/utils/intuitAPI.ts +++ b/src/utils/intuitAPI.ts @@ -28,6 +28,7 @@ import { QBItemsResponseSchema, SingleIdAndTokenResponseSchema, } from '@/type/dto/intuitAPI.dto' +import { escapeForQBQuery } from '@/utils/string' import { RetryableError } from '@/utils/error' import CustomLogger from '@/utils/logger' import httpStatus from 'http-status' @@ -282,7 +283,8 @@ export default class IntuitAPI { ) } - const sanitizedDisplayName = displayName && displayName.trim() + const sanitizedDisplayName = + displayName && escapeForQBQuery(displayName.trim()) let queryCondition = sanitizedDisplayName ? `DisplayName IN ('${sanitizedDisplayName}', '${this.getNameWithDeleted(sanitizedDisplayName)}')` : `Id = '${id}'` @@ -317,7 +319,7 @@ export default class IntuitAPI { obj: { email }, message: `IntuitAPI#getCustomerByEmail | Customer query start for realmId: ${this.tokens.intuitRealmId}. Email: ${email}`, }) - const customerQuery = `SELECT Id, SyncToken, Active, CompanyName, PrimaryEmailAddr FROM Customer WHERE PrimaryEmailAddr = '${email}' AND Active in (true, false)` + const customerQuery = `SELECT Id, SyncToken, Active, CompanyName, PrimaryEmailAddr FROM Customer WHERE PrimaryEmailAddr = '${escapeForQBQuery(email)}' AND Active in (true, false)` const qbCustomers = await this.customQuery(customerQuery) if (!qbCustomers) return @@ -361,7 +363,7 @@ export default class IntuitAPI { ) } - const sanitizedName = name && name.trim() + const sanitizedName = name && escapeForQBQuery(name.trim()) let queryCondition = sanitizedName ? `Name IN ('${sanitizedName}', '${this.getNameWithDeleted(sanitizedName)}')` : `Id = '${id}'` @@ -578,7 +580,7 @@ export default class IntuitAPI { obj: { invoiceNumber }, message: `IntuitAPI#getInvoice | invoice query start for realmId: ${this.tokens.intuitRealmId}. `, }) - const query = `select Id, SyncToken, DocNumber from Invoice where DocNumber = '${invoiceNumber}' maxresults 1` + const query = `select Id, SyncToken, DocNumber from Invoice where DocNumber = '${escapeForQBQuery(invoiceNumber)}' maxresults 1` const invoice = await this.customQuery(query) if (!invoice) @@ -724,7 +726,8 @@ export default class IntuitAPI { message: 'IntuitAPI#getAnAccount | Account query start for realmId: ', }) - const sanitizedAccountName = accountName && accountName.trim() + const sanitizedAccountName = + accountName && escapeForQBQuery(accountName.trim()) let queryCondition = sanitizedAccountName ? `Name IN ('${sanitizedAccountName}', '${this.getNameWithDeleted(sanitizedAccountName)}')` : `Id = '${id}'` diff --git a/src/utils/string.ts b/src/utils/string.ts index 49e031b3..2c18a627 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -28,6 +28,14 @@ export function replaceBeforeParens( } } +/** + * Escapes single quotes for use in QBO query strings. + * QBO query language uses backslash to escape single quotes: \\' + */ +export function escapeForQBQuery(input: string) { + return input.replace(/'/g, "\\'") +} + export function replaceSpecialCharsForQB(input: string) { // list of allowed characters in QB. // Doc: https://quickbooks.intuit.com/learn-support/en-us/help-article/account-management/acceptable-characters-quickbooks-online/L3CiHlD9J_US_en_US @@ -37,7 +45,7 @@ export function replaceSpecialCharsForQB(input: string) { '@', '&', '!', - // "'", even though included as allowed list in above docs, single quote is not allowed as this throws error. + "'", '*', '(', ')',