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
19 changes: 19 additions & 0 deletions src/action/quickbooks.action.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
'use server'

import { AuthStatus } from '@/app/api/core/types/auth'
import { PortalConnectionWithSettingType } from '@/db/schema/qbPortalConnections'
import { QBSettingsSelectSchemaType } from '@/db/schema/qbSettings'
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(
Expand All @@ -29,6 +33,21 @@ export async function checkSyncStatus(portalId: string): Promise<boolean> {
}
}

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',
})

return companyInfo?.Country !== 'US'
}

export async function reconnectIfCta(type?: string) {
if (!type) {
return false
Expand Down
3 changes: 3 additions & 0 deletions src/app/context/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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<
Expand Down
15 changes: 14 additions & 1 deletion src/components/dashboard/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ export const Main = () => {
syncFlag,
handleConnect,
isConnecting,
nonUsCompanyChecking,
} = useDashboardMain()

const { enableAppIndicator } = useApp()
const { enableAppIndicator, nonUsCompany } = useApp()

if (portalConnectionStatus === null) {
return (
Expand All @@ -89,6 +90,16 @@ export const Main = () => {
</div>
) : (
<main className="main-section px-8 sm:px-[100px] lg:px-[220px] pb-[54px] pt-6">
{nonUsCompany && (
<div className="mb-4">
<Callout
title="Support Limited to U.S. QuickBooks Online Accounts"
description="At this time, the integration only supports US-based QuickBooks Online accounts. To use the integration, please disconnect your current account and reconnect using a US QuickBooks account."
variant={CalloutVariant.ERROR}
/>
</div>
)}

<Callout
title={dashboardCallout.title}
description={dashboardCallout.description}
Expand All @@ -101,6 +112,8 @@ export const Main = () => {
: dashboardCallout.actionLabel,
onClick: buttonAction,
disabled:
nonUsCompanyChecking ||
nonUsCompany || // disable the button if non-us company
isReconnecting ||
isConnecting ||
(status === CalloutVariant.WARNING && !enableAppIndicator),
Expand Down
20 changes: 20 additions & 0 deletions src/db/service/token.service.ts
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -55,3 +57,21 @@ export const getWorkspaceInfo = async (
): Promise<WorkspaceResponse> => {
return await new CopilotAPI(token).getWorkspace()
}

export const getPortalTokens = async (
portalId: string,
): Promise<IntuitAPITokensType> => {
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,
}
}
33 changes: 32 additions & 1 deletion src/hook/useDashboard.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -14,6 +16,8 @@ export const useDashboardMain = () => {
lastSyncTimestamp,
isEnabled,
portalConnectionStatus,
qbTokens,
setAppParams,
} = useApp()

const {
Expand All @@ -33,6 +37,31 @@ export const useDashboardMain = () => {
const [buttonAction, setButtonAction] = useState<
(() => Promise<NodeJS.Timeout>) | 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)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [syncFlag])

useEffect(() => {
if (syncFlag) {
checkCompanyCountry()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [syncFlag])

useEffect(() => {
let timeout: NodeJS.Timeout
Expand Down Expand Up @@ -61,6 +90,7 @@ export const useDashboardMain = () => {
}
setIsLoading(false)
return () => clearTimeout(timeout)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [syncFlag, isEnabled])

return {
Expand All @@ -73,5 +103,6 @@ export const useDashboardMain = () => {
syncFlag,
isConnecting,
handleConnect,
nonUsCompanyChecking,
}
}
22 changes: 22 additions & 0 deletions src/utils/intuitAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Args extends unknown[], R>(
fn: (...args: Args) => Promise<R>,
): (...args: Args) => Promise<R> {
Expand Down Expand Up @@ -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)
}
Loading