feat(server): send invitation accepted notification (#10421)

This commit is contained in:
fengmk2
2025-03-24 02:27:23 +00:00
parent d7b3dc683b
commit ed888d6507
7 changed files with 157 additions and 25 deletions

View File

@@ -85,3 +85,32 @@ test('should create invitation notification', async t => {
t.is(spy.firstCall.args[0].body.workspaceId, workspace.id);
t.is(spy.firstCall.args[0].body.createdByUserId, owner.id);
});
test('should create invitation accepted notification when user accepts the invitation', async t => {
const { notificationJob, notificationService, models } = t.context;
const invite = await models.workspaceUser.set(
workspace.id,
member.id,
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted
);
const spy = Sinon.spy(notificationService, 'createInvitationAccepted');
await notificationJob.sendInvitationAccepted({
inviterId: owner.id,
inviteId: invite.id,
});
t.is(spy.callCount, 1);
t.is(spy.firstCall.args[0].userId, owner.id);
t.is(spy.firstCall.args[0].body.workspaceId, workspace.id);
t.is(spy.firstCall.args[0].body.createdByUserId, member.id);
});
test('should ignore send invitation accepted notification when inviteId not exists', async t => {
const { notificationJob, notificationService } = t.context;
const spy = Sinon.spy(notificationService, 'createInvitationAccepted');
await notificationJob.sendInvitationAccepted({
inviterId: owner.id,
inviteId: `not-exists-${randomUUID()}`,
});
t.is(spy.callCount, 0);
});

View File

@@ -123,7 +123,7 @@ test('should not create invitation notification if user is already a member', as
t.is(notification, undefined);
});
test('should create invitation accepted notification', async t => {
test('should create invitation accepted notification and email', async t => {
const { notificationService } = t.context;
const inviteId = randomUUID();
const notification = await notificationService.createInvitationAccepted({
@@ -140,6 +140,50 @@ test('should create invitation accepted notification', async t => {
t.is(notification!.body.workspaceId, workspace.id);
t.is(notification!.body.createdByUserId, member.id);
t.is(notification!.body.inviteId, inviteId);
// should send email
const invitationAcceptedMail = t.context.module.mails.last('MemberAccepted');
t.is(invitationAcceptedMail.to, owner.email);
});
test('should not send invitation accepted email if user settings is not receive invitation email', async t => {
const { notificationService } = t.context;
const inviteId = randomUUID();
// should not send email if user settings is not receive invitation email
await t.context.models.settings.set(owner.id, {
receiveInvitationEmail: false,
});
const invitationAcceptedMailCount =
t.context.module.mails.count('MemberAccepted');
const notification = await notificationService.createInvitationAccepted({
userId: owner.id,
body: {
workspaceId: workspace.id,
createdByUserId: member.id,
inviteId,
},
});
t.truthy(notification);
// no new invitation accepted email should be sent
t.is(
t.context.module.mails.count('MemberAccepted'),
invitationAcceptedMailCount
);
});
test('should not create invitation accepted notification if user is not an active member', async t => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => null);
const notification = await notificationService.createInvitationAccepted({
userId: owner.id,
body: {
workspaceId: workspace.id,
createdByUserId: member.id,
inviteId,
},
});
t.is(notification, undefined);
});
test('should create invitation blocked notification', async t => {

View File

@@ -12,6 +12,10 @@ declare global {
inviterId: string;
inviteId: string;
};
'notification.sendInvitationAccepted': {
inviterId: string;
inviteId: string;
};
}
}
@@ -57,4 +61,23 @@ export class NotificationJob {
},
});
}
@OnJob('notification.sendInvitationAccepted')
async sendInvitationAccepted({
inviterId,
inviteId,
}: Jobs['notification.sendInvitationAccepted']) {
const invite = await this.models.workspaceUser.getById(inviteId);
if (!invite) {
return;
}
await this.service.createInvitationAccepted({
userId: inviterId,
body: {
workspaceId: invite.workspaceId,
createdByUserId: invite.userId,
inviteId,
},
});
}
}

View File

@@ -130,11 +130,49 @@ export class NotificationService {
}
async createInvitationAccepted(input: InvitationNotificationCreate) {
await this.ensureWorkspaceContentExists(input.body.workspaceId);
return await this.models.notification.createInvitation(
const workspaceId = input.body.workspaceId;
const userId = input.userId;
if (!(await this.isActiveWorkspaceUser(workspaceId, userId))) {
return;
}
await this.ensureWorkspaceContentExists(workspaceId);
const notification = await this.models.notification.createInvitation(
input,
NotificationType.InvitationAccepted
);
await this.sendInvitationAcceptedEmail(input);
return notification;
}
private async sendInvitationAcceptedEmail(
input: InvitationNotificationCreate
) {
const inviterUserId = input.userId;
const inviteeUserId = input.body.createdByUserId;
const workspaceId = input.body.workspaceId;
const userSetting = await this.models.settings.get(inviterUserId);
if (!userSetting.receiveInvitationEmail) {
return;
}
const inviter = await this.models.user.getWorkspaceUser(inviterUserId);
if (!inviter) {
return;
}
await this.mailer.send({
name: 'MemberAccepted',
to: inviter.email,
props: {
user: {
$$userId: inviteeUserId,
},
workspace: {
$$workspaceId: workspaceId,
},
},
});
this.logger.log(
`Invitation accepted email sent to user ${inviter.id} for workspace ${workspaceId}`
);
}
async createInvitationBlocked(input: InvitationNotificationCreate) {