feat: ( audit ) resolve the actor from the session and audit all backoffice mutations

This commit is contained in:
Abubeker Yasin
2026-08-18 15:13:22 +03:00
parent 33100a31ae
commit 0eb64ad9a1
32 changed files with 3068 additions and 420 deletions

View File

@@ -4,6 +4,37 @@ import { CreateTrainDto, CreateCoachDto, UpdateCoachDto, AssignCoachDto, ListCoa
import { SeatKind } from '@prisma/client';
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
import { AuditService } from '../../common/audit.service';
import { AUDIT_ACTIONS, AUDIT_ENTITIES } from '../../common/audit.actions';
import { snapshot } from '../../common/audit-snapshot';
/** Fields carried into audit rows, per entity. Deliberately narrow — see audit-snapshot.ts. */
const COACH_TYPE_AUDIT_FIELDS = ['code', 'name', 'type'] as const;
const SEAT_CLASS_AUDIT_FIELDS = [
'coachTypeId',
'name',
'description',
'baseFareMinor',
'premiumMinor',
'insuranceFeeMinor',
'isActive',
] as const;
const TRAIN_AUDIT_FIELDS = [
'number',
'name',
'operatorId',
'operatorName',
'description',
'isActive',
] as const;
const COACH_AUDIT_FIELDS = [
'number',
'coachTypeId',
'arrangement',
'capacity',
'status',
'sequence',
] as const;
const COACH_ASSIGNMENT_AUDIT_FIELDS = ['scheduleId', 'coachId', 'positionNumber', 'isOperational'] as const;
// Parses '2+2' → [2, 2], '2+2+2' → [2, 2, 2]
function parseArrangement(arrangement: string): number[] {
@@ -151,7 +182,7 @@ export class FleetService {
constructor(private prisma: PrismaService, private auditService: AuditService) {}
async createCoachType(dto: CreateCoachTypeDto) {
return this.prisma.coachType.create({
const coachType = await this.prisma.coachType.create({
data: {
code: dto.code,
name: dto.name,
@@ -162,6 +193,13 @@ export class FleetService {
coaches: true,
},
});
await this.auditService.log({
action: AUDIT_ACTIONS.CREATE,
entityType: AUDIT_ENTITIES.CoachType,
entityId: coachType.id,
newData: snapshot(coachType, COACH_TYPE_AUDIT_FIELDS),
});
return coachType;
}
async getCoachTypes() {
@@ -183,7 +221,7 @@ export class FleetService {
if (dto.name !== undefined) data.name = dto.name;
if (dto.type !== undefined) data.type = dto.type;
return this.prisma.coachType.update({
const updated = await this.prisma.coachType.update({
where: { id },
data,
include: {
@@ -191,6 +229,14 @@ export class FleetService {
coaches: true,
},
});
await this.auditService.log({
action: AUDIT_ACTIONS.UPDATE,
entityType: AUDIT_ENTITIES.CoachType,
entityId: id,
oldData: snapshot(coachType, COACH_TYPE_AUDIT_FIELDS),
newData: snapshot(updated, COACH_TYPE_AUDIT_FIELDS),
});
return updated;
}
async deleteCoachType(id: string) {
@@ -224,11 +270,18 @@ export class FleetService {
throw new DeleteOperationException('Coach Type', coachType.name, constraints);
}
return this.prisma.coachType.delete({ where: { id } });
const deleted = await this.prisma.coachType.delete({ where: { id } });
await this.auditService.log({
action: AUDIT_ACTIONS.DELETE,
entityType: AUDIT_ENTITIES.CoachType,
entityId: id,
oldData: snapshot(coachType, COACH_TYPE_AUDIT_FIELDS),
});
return deleted;
}
async createClass(dto: CreateClassDto) {
return this.prisma.seatClass.create({
const seatClass = await this.prisma.seatClass.create({
data: {
coachTypeId: dto.coachTypeId,
name: dto.name,
@@ -239,6 +292,13 @@ export class FleetService {
...(dto.isActive !== undefined && { isActive: dto.isActive }),
},
});
await this.auditService.log({
action: AUDIT_ACTIONS.CREATE,
entityType: AUDIT_ENTITIES.SeatClass,
entityId: seatClass.id,
newData: snapshot(seatClass, SEAT_CLASS_AUDIT_FIELDS),
});
return seatClass;
}
async getClasses(coachTypeId?: string) {
@@ -267,11 +327,19 @@ export class FleetService {
updateData.isActive = dto.isActive;
}
return this.prisma.seatClass.update({
const updated = await this.prisma.seatClass.update({
where: { id },
data: updateData,
include: { coachType: true },
});
await this.auditService.log({
action: AUDIT_ACTIONS.UPDATE,
entityType: AUDIT_ENTITIES.SeatClass,
entityId: id,
oldData: snapshot(seatClass, SEAT_CLASS_AUDIT_FIELDS),
newData: snapshot(updated, SEAT_CLASS_AUDIT_FIELDS),
});
return updated;
}
async deleteClass(id: string, cascade = false) {
@@ -308,7 +376,15 @@ export class FleetService {
await this.prisma.segmentFareRule.deleteMany({ where: { seatClassId: id } });
}
return this.prisma.seatClass.delete({ where: { id } });
const deleted = await this.prisma.seatClass.delete({ where: { id } });
await this.auditService.log({
action: AUDIT_ACTIONS.DELETE,
entityType: AUDIT_ENTITIES.SeatClass,
entityId: id,
oldData: snapshot(seatClass, SEAT_CLASS_AUDIT_FIELDS),
newData: { cascade },
});
return deleted;
}
createSeatClass(dto: CreateClassDto) {
@@ -345,7 +421,12 @@ export class FleetService {
isActive: dto.isActive ?? true,
},
});
await this.auditService.log({ action: 'CREATE', entityType: 'Train', entityId: train.id, newData: { number: train.number, name: train.name } });
await this.auditService.log({
action: AUDIT_ACTIONS.CREATE,
entityType: AUDIT_ENTITIES.Train,
entityId: train.id,
newData: snapshot(train, TRAIN_AUDIT_FIELDS),
});
return train;
}
@@ -363,7 +444,13 @@ export class FleetService {
...(dto.isActive !== undefined && { isActive: dto.isActive }),
},
});
await this.auditService.log({ action: 'UPDATE', entityType: 'Train', entityId: id, newData: { number: dto.number, name: dto.name } });
await this.auditService.log({
action: AUDIT_ACTIONS.UPDATE,
entityType: AUDIT_ENTITIES.Train,
entityId: id,
oldData: snapshot(train, TRAIN_AUDIT_FIELDS),
newData: snapshot(updated, TRAIN_AUDIT_FIELDS),
});
return updated;
}
@@ -442,14 +529,28 @@ export class FleetService {
}
const deleted = await this.prisma.train.delete({ where: { id } });
await this.auditService.log({ action: 'DELETE', entityType: 'Train', entityId: id, oldData: { number: train.number, name: train.name } });
await this.auditService.log({
action: AUDIT_ACTIONS.DELETE,
entityType: AUDIT_ENTITIES.Train,
entityId: id,
oldData: snapshot(train, TRAIN_AUDIT_FIELDS),
newData: { cascade },
});
return deleted;
}
async restoreTrain(id: string) {
const train = await this.prisma.train.findUnique({ where: { id } });
if (!train) throw new NotFoundException('Train not found');
return this.prisma.train.update({ where: { id }, data: { isActive: true } });
const restored = await this.prisma.train.update({ where: { id }, data: { isActive: true } });
await this.auditService.log({
action: AUDIT_ACTIONS.RESTORE,
entityType: AUDIT_ENTITIES.Train,
entityId: id,
oldData: { isActive: train.isActive },
newData: { isActive: true },
});
return restored;
}
async getCoach(id: string) {
@@ -526,7 +627,12 @@ export class FleetService {
await this.prisma.seat.createMany({ data: seats });
}
await this.auditService.log({ action: 'CREATE', entityType: 'Coach', entityId: coach.id, newData: { number: coach.number, capacity: coach.capacity } });
await this.auditService.log({
action: AUDIT_ACTIONS.CREATE,
entityType: AUDIT_ENTITIES.Coach,
entityId: coach.id,
newData: snapshot(coach, COACH_AUDIT_FIELDS),
});
return coach;
}
@@ -545,7 +651,13 @@ export class FleetService {
},
include: { coachType: true },
});
await this.auditService.log({ action: 'UPDATE', entityType: 'Coach', entityId: id, newData: { number: dto.number, status: dto.status } });
await this.auditService.log({
action: AUDIT_ACTIONS.UPDATE,
entityType: AUDIT_ENTITIES.Coach,
entityId: id,
oldData: snapshot(coach, COACH_AUDIT_FIELDS),
newData: snapshot(updated, COACH_AUDIT_FIELDS),
});
return updated;
}
@@ -622,7 +734,13 @@ export class FleetService {
await this.prisma.seat.deleteMany({ where: { coachId: id } });
const deleted = await this.prisma.coach.delete({ where: { id } });
await this.auditService.log({ action: 'DELETE', entityType: 'Coach', entityId: id, oldData: { number: coach.number } });
await this.auditService.log({
action: AUDIT_ACTIONS.DELETE,
entityType: AUDIT_ENTITIES.Coach,
entityId: id,
oldData: snapshot(coach, COACH_AUDIT_FIELDS),
newData: { cascade },
});
return deleted;
}
@@ -634,13 +752,30 @@ export class FleetService {
if (!schedule) throw new NotFoundException('Schedule not found');
if (!coach) throw new NotFoundException('Coach not found');
if (coach.status !== 'ACTIVE') throw new BadRequestException('Coach is not active');
return this.prisma.coachAssignment.create({ data: dto });
const assignment = await this.prisma.coachAssignment.create({ data: dto });
await this.auditService.log({
action: AUDIT_ACTIONS.ASSIGN,
entityType: AUDIT_ENTITIES.CoachAssignment,
entityId: assignment.id,
newData: {
...snapshot(assignment, COACH_ASSIGNMENT_AUDIT_FIELDS),
coachNumber: coach.number,
},
});
return assignment;
}
async removeAssignment(id: string) {
const assignment = await this.prisma.coachAssignment.findUnique({ where: { id } });
if (!assignment) throw new NotFoundException('Assignment not found');
return this.prisma.coachAssignment.delete({ where: { id } });
const deleted = await this.prisma.coachAssignment.delete({ where: { id } });
await this.auditService.log({
action: AUDIT_ACTIONS.UNASSIGN,
entityType: AUDIT_ENTITIES.CoachAssignment,
entityId: id,
oldData: snapshot(assignment, COACH_ASSIGNMENT_AUDIT_FIELDS),
});
return deleted;
}
async generateSeatMapPreview(dto: GenerateSeatMapDto) {