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
17 changes: 1 addition & 16 deletions src/action/quickbooks.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@

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 { getPortalConnection } from '@/db/service/token.service'
import IntuitAPI from '@/utils/intuitAPI'
import CustomLogger from '@/utils/logger'
import {
Expand All @@ -26,17 +22,6 @@ export async function checkPortalConnection(
}
}

export async function checkSyncStatus(portalId: string): Promise<boolean> {
try {
const syncedPortal: QBSettingsSelectSchemaType | null =
await getPortalSettings(portalId)
return syncedPortal?.syncFlag || false
} catch (err) {
console.error('checkSyncStatus#getPortalSettings | Error =', err)
return false
}
}

export async function checkForNonUsCompany(portalId: string): Promise<boolean> {
CustomLogger.info({
message: 'checkForNonUsCompany | Checking for non-US company',
Expand Down
73 changes: 34 additions & 39 deletions src/app/(home)/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { getTokenPayload } from '@/action/copilot.action'
import {
checkPortalConnection,
checkSyncStatus,
reconnectIfCta,
} from '@/action/quickbooks.action'
import HomeClient from '@/app/(home)/HomeClient'
import User from '@/app/api/core/models/User.model'
import { SyncLogService } from '@/app/api/quickbooks/syncLog/syncLog.service'
import { AppProvider } from '@/app/context/AppContext'
import { SilentError } from '@/components/template/SilentError'
import { apiUrl } from '@/config'
import { getWorkspaceInfo } from '@/db/service/token.service'
import { z } from 'zod'

export async function getLatestSuccesLog(token: string) {
const response = await fetch(
`${apiUrl}/api/quickbooks/syncLog/success?token=${token}`,
)
return (await response.json()).data
}

export default async function Main({
searchParams,
}: {
Expand All @@ -44,39 +37,41 @@ export default async function Main({
return <SilentError message="No access to the user" />
}

const portalConnection = await checkPortalConnection(tokenPayload.workspaceId)
const portalConnectionStatus =
portalConnection && Object.keys(portalConnection).length > 0 ? true : false
const syncLogService = new SyncLogService(new User(token, tokenPayload))

let reconnect = false,
syncFlag = false,
successLog = null,
isEnabled = false
if (portalConnectionStatus) {
syncFlag = await checkSyncStatus(tokenPayload.workspaceId)
isEnabled = portalConnection?.setting?.isEnabled || false
const [portalConnection, workspace, latestSuccessLog] = await Promise.all([
checkPortalConnection(tokenPayload.workspaceId),
getWorkspaceInfo(token),
syncLogService.getLatestSyncSuccessLog().catch((err) => {
console.error('Home#getLatestSyncSuccessLog | Error =', err)
return null
}),
])

if (!syncFlag) {
reconnect = await reconnectIfCta(type)
} else {
successLog = await getLatestSuccesLog(token)
}
}
const portalConnectionStatus = !!(
portalConnection && Object.keys(portalConnection).length
)
const syncFlag = portalConnection?.setting?.syncFlag ?? false
Comment thread
SandipBajracharya marked this conversation as resolved.
const isEnabled = portalConnection?.setting?.isEnabled ?? false
const reconnect =
portalConnectionStatus && !syncFlag ? await reconnectIfCta(type) : false
const lastSyncTimestamp =
portalConnectionStatus && syncFlag
? (latestSuccessLog?.updatedAt?.toISOString() ?? null)
: null

return (
<>
<AppProvider
token={token}
tokenPayload={tokenPayload}
syncFlag={syncFlag}
reconnect={reconnect}
portalConnectionStatus={portalConnectionStatus}
isEnabled={isEnabled}
lastSyncTimestamp={successLog?.updatedAt || null}
workspace={await getWorkspaceInfo(token)}
>
<HomeClient />
</AppProvider>
</>
<AppProvider
token={token}
tokenPayload={tokenPayload}
syncFlag={syncFlag}
reconnect={reconnect}
portalConnectionStatus={portalConnectionStatus}
isEnabled={isEnabled}
lastSyncTimestamp={lastSyncTimestamp}
workspace={workspace}
>
<HomeClient />
</AppProvider>
)
}
9 changes: 9 additions & 0 deletions src/app/(home)/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Spinner } from 'copilot-design-system'

export default function Loading() {
return (
<div className="loading-spinner h-screen flex items-center justify-center">
<Spinner size={10} />
</div>
)
}
4 changes: 2 additions & 2 deletions src/app/api/quickbooks/product/flatten/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { withErrorHandler } from '@/app/api/core/utils/withErrorHandler'
import { getFlattenProducts } from '@/app/api/quickbooks/product/product.controller'
import { getProductsWithPrices } from '@/app/api/quickbooks/product/product.controller'

export const maxDuration = 300 // 5 minutes

export const GET = withErrorHandler(getFlattenProducts)
export const GET = withErrorHandler(getProductsWithPrices)
7 changes: 2 additions & 5 deletions src/app/api/quickbooks/product/product.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import { ProductService } from '@/app/api/quickbooks/product/product.service'
import { ProductMappingSchema } from '@/db/schema/qbProductSync'
import { NextRequest, NextResponse } from 'next/server'

export async function getFlattenProducts(req: NextRequest) {
export async function getProductsWithPrices(req: NextRequest) {
const user = await authenticate(req)
const productService = new ProductService(user)
const searchParams = req.nextUrl.searchParams
const nextToken = searchParams.get('nextToken') || undefined
const limit = Number(searchParams.get('limit')) || MAX_PRODUCT_LIST_LIMIT
const products = await productService.getFlattenProductList(limit, nextToken)
const products = await productService.getProductsWithPrices()
return NextResponse.json(products)
}

Expand Down
100 changes: 63 additions & 37 deletions src/app/api/quickbooks/product/product.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import {
ProductChangedItemReferenceType,
ProductMappingSchemaType,
} from '@/db/schema/qbProductSync'
import { ProductResponse, WhereClause } from '@/type/common'
import { PriceResponse, WhereClause } from '@/type/common'
import { ProductFlattenArrayResponseType } from '@/type/dto/api.dto'
import { bottleneck } from '@/utils/bottleneck'
import { QBItemFullUpdatePayloadType } from '@/type/dto/intuitAPI.dto'
import {
PriceCreatedResponseType,
Expand All @@ -41,6 +40,7 @@ import {
} from '@/utils/string'
import { AccountTypeObj } from '@/constant/qbConnection'
import { TokenService } from '@/app/api/quickbooks/token/token.service'
import { MAX_PRODUCT_LIST_LIMIT } from '@/app/api/core/constants/limit'

export type ProductSyncTokenResponse = {
id: string
Expand Down Expand Up @@ -319,47 +319,73 @@ export class ProductService extends BaseService {
return await intuitApi.createItem(qbItemPayload)
}

async getFlatMapforAProduct(product: ProductResponse, copilot: CopilotAPI) {
const prices = await copilot.getPrices(product.id)
return (prices?.data ?? [])
.map((price) => ({
...product,
description: convert(product.description),
priceId: price.id,
amount: price.amount,
type: price.type,
interval: price.interval,
intervalCount: price.intervalCount,
currency: price.currency,
}))
.sort((a, b) => a.amount - b.amount) // sort by amount in asc order
async getProductsWithPrices(): Promise<ProductFlattenArrayResponseType> {
const copilot = new CopilotAPI(this.user.token)

const [products, pricesByProduct] = await Promise.all([
copilot.getProducts(undefined, undefined, MAX_PRODUCT_LIST_LIMIT),
this.fetchAllPricesGroupedByProduct(copilot),
])

const flattened = (products?.data ?? []).flatMap((product) => {
const prices = pricesByProduct.get(product.id) ?? []
const productDescription = convert(product.description)
return prices
.map((price) => ({
...product,
description: productDescription,
priceId: price.id,
amount: price.amount,
type: price.type,
interval: price.interval,
intervalCount: price.intervalCount,
currency: price.currency,
}))
.sort((a, b) => a.amount - b.amount) // sort by amount in asc order
})

return { products: flattened }
}

async getFlattenProductList(
limit: number,
nextToken?: string,
): Promise<ProductFlattenArrayResponseType> {
// get all the products from copilot
const copilot = new CopilotAPI(this.user.token)
const products = await copilot.getProducts(undefined, nextToken, limit)
let flattenProductsPrice: ProductFlattenArrayResponseType = {
products: [],
}
const flatmapProductPrice = []
if (products?.data) {
for (const product of products.data) {
flatmapProductPrice.push(
bottleneck.schedule(() => {
return this.getFlatMapforAProduct(product, copilot)
}),
/**
* Walks every page of the workspace's /prices endpoint and groups by
* productId. Replaces the prior bottleneck-throttled N+1 per-product fetch
* with ceil(totalPrices / MAX_PRODUCT_LIST_LIMIT) sequential calls, which is
* dramatically faster for the single-page workload getProductsWithPrices
* actually serves. If product pagination is ever reintroduced, revisit:
* caller would repeat this full walk per page with no cross-call cache.
*/
private async fetchAllPricesGroupedByProduct(
copilot: CopilotAPI,
): Promise<Map<string, PriceResponse[]>> {
const grouped = new Map<string, PriceResponse[]>()
let nextToken: string | undefined
do {
const page = await copilot.getPrices(
undefined,
nextToken,
MAX_PRODUCT_LIST_LIMIT.toString(),
)
if (!page) {
// Transient SDK failure: bail rather than silently dropping every
// product on the page from the flattened response.
console.warn(
'fetchAllPricesGroupedByProduct | getPrices returned undefined; aborting pagination',
)
break
}
flattenProductsPrice = {
products: (await Promise.all(flatmapProductPrice)).flat(),
for (const price of page.data ?? []) {
const list = grouped.get(price.productId)
if (list) {
list.push(price)
} else {
grouped.set(price.productId, [price])
}
}
}
nextToken = page.nextToken
} while (nextToken)

return flattenProductsPrice
return grouped
}
Comment thread
SandipBajracharya marked this conversation as resolved.

/**
Expand Down
13 changes: 3 additions & 10 deletions src/components/dashboard/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
'use client'
import Loading from '@/app/(home)/loading'
import SettingAccordion from '@/components/dashboard/settings/SettingAccordion'
import { CalloutVariant } from '@/components/type/callout'
import Divider from '@/components/ui/Divider'
import { useDashboardMain } from '@/hook/useDashboard'

import {
ButtonProps,
Callout,
Heading,
IconType,
Spinner,
} from 'copilot-design-system'
import { ButtonProps, Callout, Heading, IconType } from 'copilot-design-system'
import LastSyncAt from '@/components/dashboard/LastSyncAt'
import { SilentError } from '@/components/template/SilentError'
import { useApp } from '@/app/context/AppContext'
Expand Down Expand Up @@ -85,9 +80,7 @@ export const Main = () => {
return (
<>
{isLoading ? (
<div className="loading-spinner h-screen flex items-center justify-center">
<Spinner size={10} />
</div>
<Loading />
) : (
<main className="main-section px-8 sm:px-[100px] lg:px-[220px] pb-[54px] pt-6">
{nonUsCompany && (
Expand Down
Loading