From 31c600e7d162673fbc78a66a9cca9af9e1d20e1a Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Mon, 5 Jan 2026 19:04:03 +0545 Subject: [PATCH 1/4] feat(OUT-2866): check QBO account company info during initial load - [x] add function that makes api request to intuit to fetch realm company info - [x] check if the QBO account company is non US - [x] if non US then show call out with copy: support US company only --- src/action/quickbooks.action.ts | 22 ++++++++++++++++++++++ src/app/context/AppContext.tsx | 3 +++ src/components/dashboard/Main.tsx | 15 ++++++++++++++- src/db/service/token.service.ts | 20 ++++++++++++++++++++ src/hook/useDashboard.ts | 30 +++++++++++++++++++++++++++++- src/utils/intuitAPI.ts | 22 ++++++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/action/quickbooks.action.ts b/src/action/quickbooks.action.ts index 27418ac5..8d23eddd 100644 --- a/src/action/quickbooks.action.ts +++ b/src/action/quickbooks.action.ts @@ -1,3 +1,5 @@ +'use server' + import { AuthStatus } from '@/app/api/core/types/auth' import { PortalConnectionWithSettingType } from '@/db/schema/qbPortalConnections' import { QBSettingsSelectSchemaType } from '@/db/schema/qbSettings' @@ -5,6 +7,8 @@ import { getPortalConnection, getPortalSettings, } from '@/db/service/token.service' +import IntuitAPI, { IntuitAPITokensType } from '@/utils/intuitAPI' +import CustomLogger from '@/utils/logger' import { z } from 'zod' export async function checkPortalConnection( @@ -29,6 +33,24 @@ export async function checkSyncStatus(portalId: string): Promise { } } +export async function checkForNonUsCompany(tokenInfo: IntuitAPITokensType) { + CustomLogger.info({ + message: 'checkForNonUsCompany | Checking for non-US company', + }) + const intuitApi = new IntuitAPI(tokenInfo) + const companyInfo = await intuitApi.getCompanyInfo() + + CustomLogger.info({ + obj: { companyInfo }, + message: 'checkForNonUsCompany | Company Info', + }) + + if (companyInfo?.Country !== 'US') { + return true + } + return false +} + export async function reconnectIfCta(type?: string) { if (!type) { return false diff --git a/src/app/context/AppContext.tsx b/src/app/context/AppContext.tsx index c3c61351..c785d55d 100644 --- a/src/app/context/AppContext.tsx +++ b/src/app/context/AppContext.tsx @@ -1,6 +1,7 @@ 'use client' import { ProductMappingItemType } from '@/db/schema/qbProductSync' import { Token, WorkspaceResponse } from '@/type/common' +import { IntuitAPITokensType } from '@/utils/intuitAPI' import { createContext, useContext, useState, ReactNode } from 'react' type AppContextType = { @@ -17,6 +18,8 @@ type AppContextType = { initialInvoiceSettingMapFlag?: boolean // flag to determine the initial invoice setting flag initialProductSettingMapFlag?: boolean // flag to determine the initial product setting flag workspace: WorkspaceResponse + nonUsCompany?: boolean + qbTokens?: IntuitAPITokensType } const AppContext = createContext< diff --git a/src/components/dashboard/Main.tsx b/src/components/dashboard/Main.tsx index c925e239..0e29f74d 100644 --- a/src/components/dashboard/Main.tsx +++ b/src/components/dashboard/Main.tsx @@ -65,9 +65,10 @@ export const Main = () => { syncFlag, handleConnect, isConnecting, + nonUsCompanyChecking, } = useDashboardMain() - const { enableAppIndicator } = useApp() + const { enableAppIndicator, nonUsCompany } = useApp() if (portalConnectionStatus === null) { return ( @@ -89,6 +90,16 @@ export const Main = () => { ) : (
+ {nonUsCompany && ( +
+ +
+ )} + { : dashboardCallout.actionLabel, onClick: buttonAction, disabled: + nonUsCompanyChecking || + nonUsCompany || // disable the button if non-us company isReconnecting || isConnecting || (status === CalloutVariant.WARNING && !enableAppIndicator), diff --git a/src/db/service/token.service.ts b/src/db/service/token.service.ts index b9012c04..5f55d9c1 100644 --- a/src/db/service/token.service.ts +++ b/src/db/service/token.service.ts @@ -1,8 +1,10 @@ +'use server' import { db } from '@/db' import { PortalConnectionWithSettingType } from '@/db/schema/qbPortalConnections' import { QBSettingsSelectSchemaType } from '@/db/schema/qbSettings' import { WorkspaceResponse } from '@/type/common' import { CopilotAPI } from '@/utils/copilotAPI' +import { IntuitAPITokensType } from '@/utils/intuitAPI' import { and, eq, isNull } from 'drizzle-orm' export const getPortalConnection = async ( @@ -55,3 +57,21 @@ export const getWorkspaceInfo = async ( ): Promise => { return await new CopilotAPI(token).getWorkspace() } + +export const getPortalTokens = async ( + portalId: string, +): Promise => { + const portalConnection = await getPortalConnection(portalId) + if (!portalConnection) throw new Error('Portal connection not found') + + return { + accessToken: portalConnection.accessToken, + refreshToken: portalConnection.refreshToken, + intuitRealmId: portalConnection.intuitRealmId, + incomeAccountRef: portalConnection.incomeAccountRef, + expenseAccountRef: portalConnection.expenseAccountRef, + assetAccountRef: portalConnection.assetAccountRef, + serviceItemRef: portalConnection.serviceItemRef, + clientFeeRef: portalConnection.clientFeeRef, + } +} diff --git a/src/hook/useDashboard.ts b/src/hook/useDashboard.ts index 1b856771..ea6f17c7 100644 --- a/src/hook/useDashboard.ts +++ b/src/hook/useDashboard.ts @@ -1,9 +1,11 @@ 'use client' +import { checkForNonUsCompany } from '@/action/quickbooks.action' import { AuthStatus } from '@/app/api/core/types/auth' import { useApp } from '@/app/context/AppContext' import { CalloutVariant } from '@/components/type/callout' +import { getPortalTokens } from '@/db/service/token.service' import { useQuickbooks } from '@/hook/useQuickbooks' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' export const useDashboardMain = () => { const { @@ -14,6 +16,8 @@ export const useDashboardMain = () => { lastSyncTimestamp, isEnabled, portalConnectionStatus, + qbTokens, + setAppParams, } = useApp() const { @@ -33,6 +37,29 @@ export const useDashboardMain = () => { const [buttonAction, setButtonAction] = useState< (() => Promise) | undefined >(undefined) + const [nonUsCompanyChecking, setNonUsCompanyChecking] = useState(false) + + const checkCompanyCountry = useCallback(async () => { + setNonUsCompanyChecking(true) + let tempTokenInfo = qbTokens + if (!tempTokenInfo) { + tempTokenInfo = await getPortalTokens(tokenPayload.workspaceId) + } + + const nonUsCompany = await checkForNonUsCompany(tempTokenInfo) + setAppParams((prev) => ({ + ...prev, + qbTokens: tempTokenInfo, + nonUsCompany, + })) + setNonUsCompanyChecking(false) + }, [syncFlag]) + + useEffect(() => { + if (syncFlag) { + checkCompanyCountry() + } + }, [syncFlag]) useEffect(() => { let timeout: NodeJS.Timeout @@ -73,5 +100,6 @@ export const useDashboardMain = () => { syncFlag, isConnecting, handleConnect, + nonUsCompanyChecking, } } diff --git a/src/utils/intuitAPI.ts b/src/utils/intuitAPI.ts index 7b644f66..65e86b7a 100644 --- a/src/utils/intuitAPI.ts +++ b/src/utils/intuitAPI.ts @@ -768,6 +768,27 @@ export default class IntuitAPI { return purchase } + async _getCompanyInfo() { + 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( + companyInfo.Fault?.Error?.code || httpStatus.BAD_REQUEST, + `${IntuitAPIErrorMessage}getCompanyInfo`, + companyInfo.Fault?.Error, + ) + } + + return companyInfo.CompanyInfo?.[0] + } + private wrapWithRetry( fn: (...args: Args) => Promise, ): (...args: Args) => Promise { @@ -842,4 +863,5 @@ export default class IntuitAPI { createPurchase = this.wrapWithRetry(this._createPurchase) deletePayment = this.wrapWithRetry(this._deletePayment) deletePurchase = this.wrapWithRetry(this._deletePurchase) + getCompanyInfo = this.wrapWithRetry(this._getCompanyInfo) } From 2b87efcd0a68e200a6d02acbc002e18325099ac2 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 14 Jan 2026 11:55:00 +0545 Subject: [PATCH 2/4] docs(OUT-2866): update copy for US limited QBO support --- src/components/dashboard/Main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/Main.tsx b/src/components/dashboard/Main.tsx index 0e29f74d..f4c78033 100644 --- a/src/components/dashboard/Main.tsx +++ b/src/components/dashboard/Main.tsx @@ -94,7 +94,7 @@ export const Main = () => {
From e2bc070e65876a3e9a37771accf46e6fad3fa33b Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Mon, 19 Jan 2026 11:46:07 +0545 Subject: [PATCH 3/4] refactor(OUT-2866): changes to improve code quality --- src/action/quickbooks.action.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/action/quickbooks.action.ts b/src/action/quickbooks.action.ts index 8d23eddd..f074cfee 100644 --- a/src/action/quickbooks.action.ts +++ b/src/action/quickbooks.action.ts @@ -45,10 +45,7 @@ export async function checkForNonUsCompany(tokenInfo: IntuitAPITokensType) { message: 'checkForNonUsCompany | Company Info', }) - if (companyInfo?.Country !== 'US') { - return true - } - return false + return companyInfo?.Country !== 'US' } export async function reconnectIfCta(type?: string) { From 438f2fb22bcdb141a4a83350c51f18488f7767aa Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Mon, 19 Jan 2026 12:05:54 +0545 Subject: [PATCH 4/4] refactor(OUT-2866): suppress react hook deps warning --- src/hook/useDashboard.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hook/useDashboard.ts b/src/hook/useDashboard.ts index ea6f17c7..8c1b67cb 100644 --- a/src/hook/useDashboard.ts +++ b/src/hook/useDashboard.ts @@ -53,12 +53,14 @@ export const useDashboardMain = () => { nonUsCompany, })) setNonUsCompanyChecking(false) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [syncFlag]) useEffect(() => { if (syncFlag) { checkCompanyCountry() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [syncFlag]) useEffect(() => { @@ -88,6 +90,7 @@ export const useDashboardMain = () => { } setIsLoading(false) return () => clearTimeout(timeout) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [syncFlag, isEnabled]) return {