diff --git a/CHANGELOG.md b/CHANGELOG.md index e20ca9534..b4f95a4a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Increased the socket auto-disconnect timeout after event emission to ensure more reliable delivery and prevent premature disconnections during high-latency operations. - Updated the `build:prod` script to execute `postbuild` after the build process, ensuring all necessary post-build steps are consistently applied in production builds. - Method `findById` of PredicateService now returns the `email` and `notify` fields of predicate members. +- Refactored vault notification logic: moved email sending logic from PredicateController to NotificationService's `vaultCreate` method. ### Fixed diff --git a/packages/api/src/modules/notification/services.ts b/packages/api/src/modules/notification/services.ts index 70e86ed55..08563e244 100644 --- a/packages/api/src/modules/notification/services.ts +++ b/packages/api/src/modules/notification/services.ts @@ -10,13 +10,15 @@ import { IFilterNotificationParams, INotificationService, IUpdateNotificationPayload, + IVaultCreateNotificationPayload, } from './types'; import { DeepPartial } from 'typeorm'; import { TransactionService } from '../transaction/services'; +import { UserService } from '../user/service'; import { EmailTemplateType, sendMail } from '@src/utils/EmailSender'; import { Network } from 'fuels'; import { SocketEvents, SocketUsernames } from '@src/socket/types'; -import { PredicateService } from '../predicate/services'; + import { logger } from '@src/config/logger'; const { API_URL } = process.env; @@ -151,29 +153,73 @@ export class NotificationService implements INotificationService { }); } - async vaultUpdate(vaultId: string) { - const vault = await new PredicateService().findById(vaultId); + async vaultCreate({ + vaultId, + vaultName, + workspaceId, + network, + membersToNotify, + }: IVaultCreateNotificationPayload) { + try { + if (membersToNotify.length === 0) { + logger.info('[VAULT_CREATE] No members to notify, skipping notifications'); + return; + } - if (!vault) { - return; - } + const membersNotificationInfos = await new UserService().getNotificationInfo( + membersToNotify, + ); - const members = vault.members; + const notifyContent = { + vaultId, + vaultName, + workspaceId, + }; - // Parallelize socket notifications for all members - await Promise.all( - members.map(member => { - const socketClient = new SocketClient(member.id, API_URL); - socketClient.emit(SocketEvents.NOTIFICATION, { - sessionId: member.id, - to: SocketUsernames.UI, - request_id: undefined, - type: SocketEvents.VAULT_UPDATE, - data: {}, - }); - return Promise.resolve(); - }), - ); + // Create notifications and send emails in parallel + await Promise.all( + membersNotificationInfos.map(async member => { + await this.create({ + title: NotificationTitle.NEW_VAULT_CREATED, + user_id: member.id, + summary: notifyContent, + network, + }); + + if (member.notify && member.email) { + await sendMail(EmailTemplateType.VAULT_CREATED, { + to: member.email, + data: { + summary: { ...notifyContent, name: member?.name ?? '' }, + }, + }); + } + }), + ); + + // Parallelize socket notifications for all members + await Promise.all( + membersNotificationInfos.map(member => { + const socketClient = new SocketClient(member.id, API_URL); + socketClient.emit(SocketEvents.NOTIFICATION, { + sessionId: member.id, + to: SocketUsernames.UI, + request_id: undefined, + type: SocketEvents.VAULT_UPDATE, + data: {}, + }); + return Promise.resolve(); + }), + ); + } catch (error) { + logger.error( + { + vaultId, + error: error?.message || error, + }, + '[VAULT_CREATE] Error on vault create notifications', + ); + } } async transactionUpdate(txId: string) { diff --git a/packages/api/src/modules/notification/types.ts b/packages/api/src/modules/notification/types.ts index 9f2a598cc..725b0bece 100644 --- a/packages/api/src/modules/notification/types.ts +++ b/packages/api/src/modules/notification/types.ts @@ -41,6 +41,14 @@ interface IListNotificationsRequestSchema extends ValidatedRequestSchema { }; } +export interface IVaultCreateNotificationPayload { + vaultId: string; + vaultName: string; + workspaceId: string; + network: Network; + membersToNotify: string[]; +} + type IReadAllNotificationsRequestSchema = ValidatedRequestSchema; export type IListNotificationsRequest = AuthValidatedRequest; @@ -58,4 +66,5 @@ export interface INotificationService { networkUrl: string, payload: IUpdateNotificationPayload, ) => Promise; + vaultCreate: (payload: IVaultCreateNotificationPayload) => Promise; } diff --git a/packages/api/src/modules/predicate/controller.ts b/packages/api/src/modules/predicate/controller.ts index 4a96e2309..2599aae6b 100644 --- a/packages/api/src/modules/predicate/controller.ts +++ b/packages/api/src/modules/predicate/controller.ts @@ -3,9 +3,6 @@ import { logger } from '@src/config/logger'; import { Predicate } from '@src/models/Predicate'; import { Workspace } from '@src/models/Workspace'; -import { EmailTemplateType, sendMail } from '@src/utils/EmailSender'; - -import { NotificationTitle } from '@models/index'; import { error, ErrorTypes, NotFound } from '@utils/error'; import { @@ -36,8 +33,8 @@ import { PredicateWithHidden, } from './types'; -import { NotificationService } from '../notification/services'; import { PredicateService } from './services'; + const { FUEL_PROVIDER } = process.env; export class PredicateController { @@ -103,48 +100,21 @@ export class PredicateController { '[PREDICATE_CREATE] Predicate created successfully', ); - const notifyDestination = predicate.members.filter( - member => user.id !== member.id, - ); - const notifyContent = { - vaultId: predicate.id, - vaultName: predicate.name, - workspaceId: effectiveWorkspace.id, - }; - - await Promise.all( - notifyDestination.map(async member => { - try { - await this.notificationService.create({ - title: NotificationTitle.NEW_VAULT_CREATED, - user_id: member.id, - summary: notifyContent, - network, - }); - - if (member.notify && member.email) { - await sendMail(EmailTemplateType.VAULT_CREATED, { - to: member.email, - data: { - summary: { ...notifyContent, name: member?.name ?? '' }, - }, - }); - } - } catch (e) { - logger.error( - { - memberId: member?.id, - to: member.email, - predicateId: predicate.id, - error: e, - }, - '[PREDICATE_CREATE] Failed to process member notification', - ); - } - }), - ); - - await new NotificationService().vaultUpdate(predicate.id); + const membersToNotify = predicate.members + .filter(member => user.id !== member.id) + .map(member => member.id); + + this.notificationService + .vaultCreate({ + vaultId: predicate.id, + vaultName: predicate.name, + workspaceId: effectiveWorkspace.id, + network, + membersToNotify, + }) + .catch(e => + logger.error({ error: e }, '[PREDICATE_CREATE] Send notification failed'), + ); return successful(predicate, Responses.Created); } catch (e) { diff --git a/packages/api/src/modules/predicate/services.ts b/packages/api/src/modules/predicate/services.ts index 1fcc686e1..48dcb1a07 100644 --- a/packages/api/src/modules/predicate/services.ts +++ b/packages/api/src/modules/predicate/services.ts @@ -190,8 +190,6 @@ export class PredicateService implements IPredicateService { 'members.avatar', 'members.address', 'members.type', - 'members.notify', - 'members.email', 'owner.id', 'owner.address', 'owner.type', diff --git a/packages/api/src/modules/user/service.ts b/packages/api/src/modules/user/service.ts index cd6316a8d..14235b2a5 100644 --- a/packages/api/src/modules/user/service.ts +++ b/packages/api/src/modules/user/service.ts @@ -22,6 +22,7 @@ import { IUserPayload, IValidateNameResponse, IFindByNameResponse, + INotificationInfo, } from './types'; import App from '@src/server/app'; @@ -441,4 +442,31 @@ export class UserService implements IUserService { }); } } + + /** + * Retrieves notification preferences and email for specified users + * Used internally for sending notifications without exposing sensitive data + * @param ids - Array of user IDs to fetch notification info for + * @returns Promise - User notification data (id, name, notify, email) + */ + async getNotificationInfo(ids: string[]): Promise { + if (ids.length === 0) { + return []; + } + + try { + const users = await User.createQueryBuilder('u') + .select(['u.id', 'u.name', 'u.notify', 'u.email']) + .where('u.id IN (:...ids)', { ids }) + .getMany(); + + return users; + } catch (error) { + throw new Internal({ + type: ErrorTypes.Internal, + title: 'Error on get notification info', + detail: error?.message || error, + }); + } + } } diff --git a/packages/api/src/modules/user/types.ts b/packages/api/src/modules/user/types.ts index 2d1c8bda1..7568bce38 100644 --- a/packages/api/src/modules/user/types.ts +++ b/packages/api/src/modules/user/types.ts @@ -49,6 +49,13 @@ export interface IValidateNameResponse { type: TypeUser; } +export interface INotificationInfo { + id: string; + name?: string; + notify: boolean; + email?: string; +} + interface ICreateRequestSchema extends ValidatedRequestSchema { [ContainerTypes.Body]: IUserPayload; } @@ -164,4 +171,5 @@ export interface IUserService { workspaceId: string; network: Network; }) => Promise; + getNotificationInfo(ids: string[]): Promise; } diff --git a/packages/api/src/tests/transaction.tests.ts b/packages/api/src/tests/transaction.tests.ts index 4d3375a05..c28dd150e 100644 --- a/packages/api/src/tests/transaction.tests.ts +++ b/packages/api/src/tests/transaction.tests.ts @@ -193,9 +193,7 @@ test('Transaction Endpoints', async t => { const member = predicate.members.find( member => member.id === element.owner.id, ); - for (const key of Object.keys(element.owner)) { - assert.deepStrictEqual(member[key], element.owner[key]); - } + assert.deepStrictEqual(element.owner, member); }); }, );