Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const ORIGINAL_ENV = process.env

afterEach(() => {
jest.resetModules()
jest.dontMock('@/lib/db')
jest.dontMock('@/utils/CopilotAPI')
process.env = ORIGINAL_ENV
})

describe('ValidateCountService', () => {
it('skips reconciliation when the Tasks app id is not configured', async () => {
const getClientNotifications = jest.fn()

jest.resetModules()
process.env = {
...ORIGINAL_ENV,
COPILOT_API_KEY: 'api-key',
COPILOT_APP_ID: undefined,
COPILOT_APP_API_KEY: undefined,
NEXT_PUBLIC_ASSEMBLY_API_DOMAIN: 'https://api.example.com',
}

jest.doMock('@/lib/db', () => ({
__esModule: true,
default: {
getInstance: jest.fn(() => ({})),
},
}))
jest.doMock('@/utils/CopilotAPI', () => ({
CopilotAPI: jest.fn(() => ({ getClientNotifications })),
}))

const { ValidateCountService } = await import('./validateCount.service')
const service = new ValidateCountService({
token: 'token',
clientId: '11111111-1111-4111-8111-111111111111',
companyId: '22222222-2222-4222-8222-222222222222',
workspaceId: 'workspace-id',
} as any)

await service.fixClientNotificationCount(
'11111111-1111-4111-8111-111111111111',
'22222222-2222-4222-8222-222222222222',
'workspace-id',
)

expect(getClientNotifications).not.toHaveBeenCalled()
})
})

export {}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MAX_NOTIFICATIONS_COUNT } from '@/constants/notifications'
import { APP_ID } from '@/config'
import { DuplicateNotificationsQuerySchema } from '@/types/client-notifications'
import { getArrayDifference } from '@/utils/array'
import { copilotBottleneck } from '@/utils/bottleneck'
Expand All @@ -13,6 +14,11 @@ export class ValidateCountService extends NotificationService {
* @param {string} clientId - Copilot client id for which notification fix has to be done
*/
async fixClientNotificationCount(clientId: string, companyId: string, workspaceId: string): Promise<void> {
if (!APP_ID) {
console.info('ValidateCount :: Skipping notification validation because COPILOT_APP_ID is not configured')
return
}

const notifications = await this.copilot.getClientNotifications(clientId, companyId, workspaceId, {
limit: MAX_NOTIFICATIONS_COUNT,
})
Expand Down
8 changes: 7 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ export const supabaseBucket = process.env.NEXT_PUBLIC_SUPABASE_BUCKET || ''
// (OUT-3864). Empty falls back to the project URL, so behaviour is unchanged until it's configured.
export const supabaseStorageDomain = process.env.NEXT_PUBLIC_SUPABASE_STORAGE_DOMAIN || ''
export const cronSecret = process.env.CRON_SECRET || ''
export const APP_ID = process.env.COPILOT_APP_API_KEY

const parseOptionalUuid = (value?: string) => {
const parsed = z.string().uuid().safeParse(value)
return parsed.success ? parsed.data : undefined
}

export const APP_ID = parseOptionalUuid(process.env.COPILOT_APP_ID ?? process.env.COPILOT_APP_API_KEY)

export const ScrapImageExpiryPeriod = +(process.env.SCRAP_IMAGE_EXPIRY_PERIOD || '604800000')

Expand Down
92 changes: 92 additions & 0 deletions src/utils/CopilotAPI.notifications.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const ORIGINAL_ENV = process.env

const APP_ID = '11111111-1111-4111-8111-111111111111'
const OTHER_APP_ID = '22222222-2222-4222-8222-222222222222'
const COMPANY_ID = '33333333-3333-4333-8333-333333333333'
const OTHER_COMPANY_ID = '44444444-4444-4444-8444-444444444444'

const loadCopilotAPI = async (env: Record<string, string | undefined> = {}) => {
jest.resetModules()
process.env = {
...ORIGINAL_ENV,
COPILOT_API_KEY: 'api-key',
NEXT_PUBLIC_ASSEMBLY_API_DOMAIN: 'https://api.example.com',
...env,
}

jest.doMock('copilot-node-sdk', () => ({
copilotApi: jest.fn(() => ({})),
}))
jest.doMock('@/app/api/core/utils/withRetry', () => ({
withRetry: jest.fn((fn, args) => fn(...args)),
}))

return import('@/utils/CopilotAPI')
}

afterEach(() => {
jest.resetModules()
jest.dontMock('copilot-node-sdk')
jest.dontMock('@/app/api/core/utils/withRetry')
process.env = ORIGINAL_ENV
})

describe('CopilotAPI#getClientNotifications', () => {
it('does not call Copilot when the Tasks app id is not configured', async () => {
const { CopilotAPI } = await loadCopilotAPI({
COPILOT_APP_ID: undefined,
COPILOT_APP_API_KEY: undefined,
})
const copilot = new CopilotAPI('token') as any
copilot.manualFetch = jest.fn()

await expect(copilot._getClientNotifications('client-id', COMPANY_ID, 'workspace-id', { limit: 100 })).resolves.toEqual(
[],
)
expect(copilot.manualFetch).not.toHaveBeenCalled()
})

it('filters Copilot notifications to the configured Tasks app and recipient company', async () => {
const { CopilotAPI } = await loadCopilotAPI({ COPILOT_APP_ID: APP_ID })
const copilot = new CopilotAPI('token') as any
copilot.manualFetch = jest.fn().mockResolvedValue({
data: [
{
id: 'matching-recipient-company',
appId: APP_ID,
createdAt: '2026-06-24T00:00:00.000Z',
recipientCompanyId: COMPANY_ID,
},
{ id: 'matching-company', appId: APP_ID, createdAt: '2026-06-24T00:00:00.000Z', companyId: COMPANY_ID },
{
id: 'different-company',
appId: APP_ID,
createdAt: '2026-06-24T00:00:00.000Z',
recipientCompanyId: OTHER_COMPANY_ID,
},
{ id: 'different-app', appId: OTHER_APP_ID, createdAt: '2026-06-24T00:00:00.000Z', recipientCompanyId: COMPANY_ID },
],
})

await expect(copilot._getClientNotifications('client-id', COMPANY_ID, 'workspace-id', { limit: 100 })).resolves.toEqual([
{
id: 'matching-recipient-company',
appId: APP_ID,
createdAt: '2026-06-24T00:00:00.000Z',
recipientCompanyId: COMPANY_ID,
},
{ id: 'matching-company', appId: APP_ID, createdAt: '2026-06-24T00:00:00.000Z', companyId: COMPANY_ID },
])
expect(copilot.manualFetch).toHaveBeenCalledWith(
'notifications',
{
recipientClientId: 'client-id',
recipientCompanyId: COMPANY_ID,
limit: '100',
},
'workspace-id',
)
})
})

export {}
7 changes: 6 additions & 1 deletion src/utils/CopilotAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ export class CopilotAPI {
} = { limit: 100 },
) {
console.info('CopilotAPI#_getClientNotifications', this.token)
if (!APP_ID) {
console.info('CopilotAPI#_getClientNotifications | Skipping lookup because COPILOT_APP_ID is not configured')
return []
}

const response = await this.manualFetch(
'notifications',
{
Expand All @@ -304,7 +309,7 @@ export class CopilotAPI {
const notifications = z.array(NotificationCreatedResponseSchema).parse(response.data)
// Return only all notifications triggered by tasks-app
return notifications
.filter((notification) => notification.appId === z.string({ message: 'Missing AppID in environment' }).parse(APP_ID))
.filter((notification) => notification.appId === APP_ID)
.filter((notification) => {
const isSameRecipientCompanyId =
notification.recipientCompanyId && notification.recipientCompanyId === recipientCompanyId
Expand Down
Loading