Skip to content

Commit ba09f4e

Browse files
committed
integrate audit system to lead applciation
1 parent b1d5e10 commit ba09f4e

7 files changed

Lines changed: 139 additions & 14 deletions

File tree

src/notifications/notifications.service.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ describe('NotificationsService', () => {
66
let service: NotificationsService;
77
let userModel: any;
88
let notificationModel: any;
9+
let notificationAuditModel: any;
910

1011
beforeEach(async () => {
1112
// mokcingi model functions for testing
@@ -18,12 +19,16 @@ describe('NotificationsService', () => {
1819
},
1920
}),
2021
};
22+
notificationAuditModel = {
23+
create: jest.fn(),
24+
};
2125

2226
const module: TestingModule = await Test.createTestingModule({
2327
providers: [
2428
NotificationsService,
2529
{ provide: 'NotificationInfo', useValue: notificationModel },
2630
{ provide: 'User', useValue: userModel },
31+
{ provide: 'NotificationAuditInfo', useValue: notificationAuditModel },
2732
],
2833
}).compile();
2934

src/notifications/notifications.service.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
} from '@nestjs/common';
77
import { Model, Types } from 'mongoose';
88
import { NotificationDto } from 'src/shared/dtos/notificatoin.dto';
9-
import { NotificationInfo, User } from 'src/shared/schema';
9+
import {
10+
NotificationAuditInfo,
11+
NotificationInfo,
12+
User,
13+
} from 'src/shared/schema';
1014

1115
@Injectable()
1216
export class NotificationsService {
@@ -15,6 +19,8 @@ export class NotificationsService {
1519
private readonly notificationModel: Model<NotificationInfo>,
1620
@Inject(User.name)
1721
private readonly userModel: Model<User>,
22+
@Inject(NotificationAuditInfo.name)
23+
private readonly notificationAuditModel: Model<NotificationAuditInfo>,
1824
) {}
1925

2026
async getNotifications(userId: string): Promise<string[]> {
@@ -53,6 +59,21 @@ export class NotificationsService {
5359
};
5460
}
5561

62+
async getNotificationByUserId(userId: string, entityId: string) {
63+
const notification = await this.notificationModel.findOne({
64+
receiverId: userId,
65+
entityId,
66+
});
67+
68+
if (!notification) {
69+
throw new NotFoundException(
70+
'No unread notifications found for this entity',
71+
);
72+
}
73+
74+
return notification;
75+
}
76+
5677
async getUnreadNotificationsByEntity(entityId: string): Promise<any[]> {
5778
const notifications = await this.notificationModel
5879
.find({ entityId })
@@ -146,9 +167,10 @@ export class NotificationsService {
146167
}
147168

148169
async resolveNotification(
170+
notification_id: string,
149171
admin_id: string,
150172
message: string,
151-
notification_id: string,
173+
status: string,
152174
): Promise<string> {
153175
// Fetch the notification to copy its data
154176
const notification = await this.notificationModel
@@ -158,14 +180,23 @@ export class NotificationsService {
158180
if (!notification) {
159181
throw new NotFoundException('Notification not found');
160182
}
183+
const notificationToAudit = {
184+
actorId: admin_id,
185+
notificationSummary: notification.message,
186+
resolutionMessage: message,
187+
resolutionStatus: status,
188+
entityType: notification.notification_type,
189+
entityId: notification.entityId,
190+
};
161191

162192
// Delete the notification
163193
const deleted = await this.deleteNotification(notification_id);
164194
if (!deleted) {
165195
throw new InternalServerErrorException('Error deleting notification');
166196
}
167-
// TODO: use notification to create audit file with admin id
168197

169-
return '';
198+
await this.notificationAuditModel.create(notificationToAudit);
199+
200+
return 'Notification resolved successfully';
170201
}
171202
}

src/shared/schema/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { ContactInfo, ContactInfoSchema } from './contact.info.schema';
66
import { DataLog, DataLogSchema } from './data.log.schema';
77
import { EventSchema } from './events.schema';
88
import { InviteToken, InviteTokenSchema } from './invite-tokens.schema';
9+
import {
10+
NotificationAuditInfo,
11+
NotificationAuditSchema,
12+
} from './notificationAudit.schema';
913
import { NotificationInfo, NotificationSchema } from './notifications.schema';
1014
import { Post, PostSchema } from './post.schema';
1115
import {
@@ -20,6 +24,7 @@ export * from './contact.info.schema';
2024
export * from './data.log.schema';
2125
export * from './events.schema';
2226
export * from './invite-tokens.schema';
27+
export * from './notificationAudit.schema';
2328
export * from './notifications.schema';
2429
export * from './post.schema';
2530
export * from './professional.info.schema';
@@ -39,6 +44,11 @@ const SCHEMA_LIST = [
3944
},
4045
{ name: ContactInfo.name, schema: ContactInfoSchema, dbPrefix: 'APP' },
4146
{ name: NotificationInfo.name, schema: NotificationSchema, dbPrefix: 'APP' },
47+
{
48+
name: NotificationAuditInfo.name,
49+
schema: NotificationAuditSchema,
50+
dbPrefix: 'LOG',
51+
},
4252
];
4353

4454
export const CONNECTION = SCHEMA_LIST.reduce((result, data) => {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2+
import { HydratedDocument } from 'mongoose';
3+
import { NotificationType } from '../interfaces';
4+
5+
export type NotificationAuditInfoDoc = HydratedDocument<NotificationAuditInfo>;
6+
7+
@Schema({ collection: 'notification_audit', timestamps: true })
8+
export class NotificationAuditInfo {
9+
@Prop({ required: true })
10+
actorId: string; // id of the admin who made the change
11+
12+
@Prop({ required: true })
13+
resolutionMessage: string;
14+
15+
@Prop({ required: true, type: String, enum: ['REJECTED', 'APPROVED'] })
16+
resolutionStatus: string;
17+
18+
@Prop({ required: true, type: String, enum: NotificationType })
19+
entityType: NotificationType;
20+
21+
@Prop({ required: true })
22+
entityId: string;
23+
24+
@Prop({ required: true })
25+
notificationSummary: string;
26+
}
27+
28+
export const NotificationAuditSchema = SchemaFactory.createForClass(
29+
NotificationAuditInfo,
30+
);

src/users/users.admin.controller.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Post,
1010
Put,
1111
Query,
12+
Req,
1213
Request,
1314
UseGuards,
1415
} from '@nestjs/common';
@@ -236,8 +237,12 @@ export class UsersAdminsController {
236237
'finds a pending lead application using the email query param and approve that pending application on that account',
237238
})
238239
@Patch('lead/:email/approve')
239-
async approveApplication(@Param('email') email: string): Promise<string> {
240-
return await this.usersService.approveTempApplication(email);
240+
async approveApplication(
241+
@Param('email') email: string,
242+
@Request() req: ApiReq,
243+
): Promise<string> {
244+
const admin_id = req.user._id.toString();
245+
return await this.usersService.approveTempApplication(email, admin_id);
241246
}
242247

243248
// reject a lead request
@@ -247,12 +252,15 @@ export class UsersAdminsController {
247252
@Patch('lead/:email/reject')
248253
async reject(
249254
@Param('email') email: string,
255+
@Request() req: ApiReq,
250256
@Body() rejectApplicationDto: RejectApplicationDto,
251257
): Promise<string> {
252258
const defaultMessage = 'Your application was rejected';
253259
const rejectionMessage = rejectApplicationDto.message || defaultMessage;
260+
const admin_id = req.user._id.toString();
254261
return await this.usersService.rejectTempApplication(
255262
email,
263+
admin_id,
256264
rejectionMessage,
257265
);
258266
}

src/users/users.controller.spec.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -711,52 +711,63 @@ describe('UsersController', () => {
711711
describe('approveApplication', () => {
712712
it('should approve a lead application', async () => {
713713
const email = 'lead@example.com';
714+
const admin_id = 'admin_id';
714715
const expectedResult = 'Application approved successfully';
716+
const mockReq = { user: { _id: admin_id } } as any;
715717

716718
jest
717719
.spyOn(usersService, 'approveTempApplication')
718720
.mockResolvedValue(expectedResult);
719721

720-
const result = await adminController.approveApplication(email);
722+
const result = await adminController.approveApplication(email, mockReq);
721723

722724
expect(result).toEqual(expectedResult);
723-
expect(usersService.approveTempApplication).toHaveBeenCalledWith(email);
725+
expect(usersService.approveTempApplication).toHaveBeenCalledWith(
726+
email,
727+
admin_id,
728+
);
724729
});
725730
});
726731

727732
describe('reject', () => {
728733
it('should reject a lead application with custom message', async () => {
729734
const email = 'lead@example.com';
735+
const admin_id = 'admin_id';
730736
const rejectDto = { message: 'Custom rejection message' };
731737
const expectedResult = 'Application rejected successfully';
738+
const mockReq = { user: { _id: admin_id } } as any;
732739

733740
jest
734741
.spyOn(usersService, 'rejectTempApplication')
735742
.mockResolvedValue(expectedResult);
736743

737-
const result = await adminController.reject(email, rejectDto);
744+
const result = await adminController.reject(email, mockReq, rejectDto);
738745

739746
expect(result).toEqual(expectedResult);
740747
expect(usersService.rejectTempApplication).toHaveBeenCalledWith(
741748
email,
749+
admin_id,
742750
rejectDto.message,
743751
);
744752
});
745753

746754
it('should reject with default message when no message provided', async () => {
747755
const email = 'lead@example.com';
756+
const admin_id = 'admin_id';
748757
const rejectDto = {};
749758
const expectedResult = 'Application rejected successfully';
759+
const mockReq = { user: { _id: admin_id } } as any;
750760

751761
jest
752762
.spyOn(usersService, 'rejectTempApplication')
753763
.mockResolvedValue(expectedResult);
754764

755-
const result = await adminController.reject(email, rejectDto);
765+
const result = await adminController.reject(email, mockReq, rejectDto);
756766

757767
expect(result).toEqual(expectedResult);
758768
expect(usersService.rejectTempApplication).toHaveBeenCalledWith(
759769
email,
770+
admin_id,
760771
'Your application was rejected',
761772
);
762773
});

src/users/users.service.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,11 @@ export class UsersService {
465465
}
466466

467467
// approve a lead application
468-
async approveTempApplication(email: string): Promise<string> {
468+
async approveTempApplication(
469+
admin_id: string,
470+
email: string,
471+
message: string = '',
472+
): Promise<string> {
469473
const userApplication = await this.userModel
470474
.findOne({
471475
email: email,
@@ -488,7 +492,19 @@ export class UsersService {
488492
userApplication.applicationStatus = ApplicationStatus.APPROVED;
489493
userApplication.save();
490494

491-
// TODO: move existing notification to audit
495+
// find notifications for user and resolve them
496+
const existingNotification =
497+
await this.notificationsService.getNotificationByUserId(
498+
userApplication._id.toString(),
499+
userApplication._id.toString(),
500+
);
501+
this.notificationsService.resolveNotification(
502+
existingNotification._id.toString(),
503+
admin_id,
504+
message,
505+
'APPROVED',
506+
);
507+
492508
// create notification for user
493509
const _ = await this.notificationsService.createNotification({
494510
receiverId: userApplication._id.toString(),
@@ -521,13 +537,27 @@ export class UsersService {
521537

522538
// TODO: refactor the createNotification to take a user, message, isAdmin, notificationType and data
523539
// reject a lead application
524-
async rejectTempApplication(email: string, message: string): Promise<string> {
540+
async rejectTempApplication(
541+
email: string,
542+
admin_id: string,
543+
message: string,
544+
): Promise<string> {
525545
const userApplication = await this.viewOneApplication(email);
526546
userApplication.leadPosition = '';
527547
userApplication.applicationStatus = ApplicationStatus.REJECTED;
528548
await userApplication.save();
529549

530-
// TODO: move existing notification to audit
550+
const existingNotification =
551+
await this.notificationsService.getNotificationByUserId(
552+
userApplication._id.toString(),
553+
userApplication._id.toString(),
554+
);
555+
this.notificationsService.resolveNotification(
556+
existingNotification._id.toString(),
557+
admin_id,
558+
message,
559+
'REJECTED',
560+
);
531561
// create notification for user
532562
const _ = await this.notificationsService.createNotification({
533563
receiverId: userApplication._id.toString(),

0 commit comments

Comments
 (0)