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
8 changes: 6 additions & 2 deletions src/app/api/notification/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,16 @@ export class NotificationService extends BaseService {
)
console.info('NotificationService#create | Creating single notification:', notificationDetails)

let notification: NotificationCreatedResponse
let notification: NotificationCreatedResponse | null
try {
notification = await this.copilot.createNotification(notificationDetails)
} catch (e: unknown) {
notification = await this.handleIfSenderCompanyIdError(e, notificationDetails)
}
if (!notification) {
console.info('NotificationService#create | Notification delivery did not create an in-product resource')
return null
}

console.info('NotificationService#create | Created single notification:', notification)

Expand Down Expand Up @@ -169,7 +173,7 @@ export class NotificationService extends BaseService {
)

console.info('NotificationService#bulkCreate | Creating single notification:', notificationDetails)
let notification: NotificationCreatedResponse
let notification: NotificationCreatedResponse | null
try {
notification = await this.copilot.createNotification(notificationDetails)
} catch (e: unknown) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,16 @@ export class ValidateCountService extends NotificationService {
// Now track those to ClientNotifications table
const newClientNotificationData = []
for (const i in newNotifications) {
const notification = newNotifications[i]
if (!notification) {
console.error(
`ValidateCount :: Copilot did not return a notification resource for task ${tasksWithoutNotifications[i].id}`,
)
continue
}

newClientNotificationData.push({
notificationId: newNotifications[i].id,
notificationId: notification.id,
taskId: tasksWithoutNotifications[i].id,
clientId,
companyId: tasksWithoutNotifications[i].companyId,
Expand Down
18 changes: 13 additions & 5 deletions src/app/api/webhook/webhook.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,25 @@ class WebhookService extends BaseService {
// If any of the given action is not present in details obj, that type of notification is not sent
deliveryTargets: { inProduct },
}
createNotificationPromises.push(copilotBottleneck.schedule(() => this.copilot.createNotification(notificationDetails)))
createNotificationPromises.push(
copilotBottleneck
.schedule(() => this.copilot.createNotification(notificationDetails))
.then((notification) => ({ task, notification })),
)
}
const notifications = await Promise.all(createNotificationPromises)
const notificationResults = await Promise.all(createNotificationPromises)

// Now add appropriate records to our ClientNotifications table
const insertPromises = []
const notificationService = new NotificationService(this.user)
for (let i = 0; i < notifications.length; i++) {
for (const { task, notification } of notificationResults) {
if (!notification) {
console.error('WebhookService#handleClientCreated :: Copilot did not return a notification resource for task', task.id)
continue
}

insertPromises.push(
// This is assuming a 1:1 map for tasks and notifications
dbBottleneck.schedule(() => notificationService.addToClientNotifications(tasks[i], notifications[i])),
dbBottleneck.schedule(() => notificationService.addToClientNotifications(task, notification)),
)
}
await Promise.all(insertPromises)
Expand Down
5 changes: 5 additions & 0 deletions src/cmd/backfill-missed-emails/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const dispatchNotification = async (copilot: CopilotAPI, taskId: string, payload
// <<- Emails are triggered here. Proceed with caution ->>

const notification = await copilot.createNotification(payload)
if (!notification) {
console.info('No notification resource returned for email-only delivery:', { taskId, payload })
return
}

await db.clientNotification.create({
data: {
clientId: payload.recipientClientId!,
Expand Down
61 changes: 61 additions & 0 deletions src/utils/CopilotAPI.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const mockCreateNotification = jest.fn()

jest.mock('copilot-node-sdk', () => ({
copilotApi: jest.fn(() => ({
createNotification: mockCreateNotification,
})),
}))

jest.mock('@/config', () => ({
APP_ID: '00000000-0000-0000-0000-000000000000',
assemblyApiDomain: 'https://api.example.com',
copilotAPIKey: 'test-api-key',
}))

jest.mock('@/app/api/core/utils/withRetry', () => ({
withRetry: jest.fn((fn, args) => fn(...args)),
}))

import { CopilotAPI } from './CopilotAPI'

describe('CopilotAPI#createNotification', () => {
beforeEach(() => {
mockCreateNotification.mockReset()
})

it('returns null when Copilot does not create a resource for email-only delivery', async () => {
mockCreateNotification.mockResolvedValueOnce(null)
const copilot = new CopilotAPI('test-token')

await expect(
copilot._createNotification({
senderId: 'sender-id',
senderType: 'internalUser',
recipientClientId: 'recipient-id',
deliveryTargets: {
email: {
subject: 'Reminder',
},
},
}),
).resolves.toBeNull()
})

it('throws a clear error when an in-product delivery does not create a resource', async () => {
mockCreateNotification.mockResolvedValueOnce(null)
const copilot = new CopilotAPI('test-token')

await expect(
copilot._createNotification({
senderId: 'sender-id',
senderType: 'internalUser',
recipientInternalUserId: 'recipient-id',
deliveryTargets: {
inProduct: {
title: 'Reminder',
},
},
}),
).rejects.toThrow('CopilotAPI#createNotification returned no notification for an in-product delivery')
})
})
10 changes: 9 additions & 1 deletion src/utils/CopilotAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,17 @@ export class CopilotAPI {
return InternalUsersSchema.parse(await this.copilot.retrieveInternalUser({ id }))
}

async _createNotification(requestBody: NotificationRequestBody): Promise<NotificationCreatedResponse> {
async _createNotification(requestBody: NotificationRequestBody): Promise<NotificationCreatedResponse | null> {
console.info('CopilotAPI#_createNotification', this.token)
const notification = await this.copilot.createNotification({ requestBody })
if (!notification) {
if (!requestBody.deliveryTargets?.inProduct) {
console.info('CopilotAPI#_createNotification | No notification resource returned for email-only delivery')
return null
}

throw new Error('CopilotAPI#createNotification returned no notification for an in-product delivery')
}
return NotificationCreatedResponseSchema.parse(notification)
}

Expand Down
Loading