mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: ( audit ) resolve the actor from the session and audit all backoffice mutations
This commit is contained in:
@@ -6,9 +6,59 @@ import { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateSchedule
|
||||
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
|
||||
import { parseEthiopianTime, startOfDayEAT, startOfNextDayEAT } from '../../common/utils/timezone.utils';
|
||||
import { AuditService } from '../../common/audit.service';
|
||||
import { AUDIT_ACTIONS, AUDIT_ENTITIES } from '../../common/audit.actions';
|
||||
import { snapshot } from '../../common/audit-snapshot';
|
||||
import { LiveService } from '../live/live.service';
|
||||
import { computePlannedStopTimes } from '../../common/utils/schedule-times.utils';
|
||||
|
||||
const SCHEDULE_AUDIT_FIELDS = [
|
||||
'trainId',
|
||||
'routeId',
|
||||
'originStationId',
|
||||
'destinationStationId',
|
||||
'departureAt',
|
||||
'arrivalAt',
|
||||
'durationMinutes',
|
||||
'stopsCount',
|
||||
'status',
|
||||
] as const;
|
||||
const FARE_RULE_AUDIT_FIELDS = [
|
||||
'tripId',
|
||||
'seatClassId',
|
||||
'baseFareMinor',
|
||||
'currency',
|
||||
'nationality',
|
||||
'validFrom',
|
||||
'validUntil',
|
||||
] as const;
|
||||
const SEGMENT_FARE_AUDIT_FIELDS = [
|
||||
'routeId',
|
||||
'seatClassId',
|
||||
'originStopSequence',
|
||||
'destinationStopSequence',
|
||||
'baseFareMinor',
|
||||
'currency',
|
||||
'validFrom',
|
||||
'validUntil',
|
||||
] as const;
|
||||
const ROUTE_FARE_AUDIT_FIELDS = [
|
||||
'routeId',
|
||||
'seatClassId',
|
||||
'passengerCategory',
|
||||
'baseFareMinor',
|
||||
'surchargeMinor',
|
||||
'validFrom',
|
||||
'validUntil',
|
||||
] as const;
|
||||
const STOP_TIME_AUDIT_FIELDS = [
|
||||
'scheduleId',
|
||||
'stationId',
|
||||
'sequence',
|
||||
'plannedArrivalAt',
|
||||
'plannedDepartureAt',
|
||||
'status',
|
||||
] as const;
|
||||
|
||||
@Injectable()
|
||||
export class SchedulesService {
|
||||
private readonly logger = new Logger(SchedulesService.name);
|
||||
@@ -102,6 +152,24 @@ export class SchedulesService {
|
||||
currentDate = new Date(currentDate.getTime() + dto.repeatEveryDays * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
// One row for the whole sweep, not one per schedule — the operator performed a single
|
||||
// action and the created ids are the interesting part.
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.BULK_CREATE,
|
||||
entityType: AUDIT_ENTITIES.Schedule,
|
||||
entityId: dto.routeId,
|
||||
newData: {
|
||||
routeId: dto.routeId,
|
||||
trainId: dto.trainId,
|
||||
startDateTime: dto.startDateTime,
|
||||
forNextDays: dto.forNextDays,
|
||||
repeatEveryDays: dto.repeatEveryDays,
|
||||
schedulesCreated: scheduleCount,
|
||||
scheduleIds,
|
||||
errorCount: errors.length,
|
||||
},
|
||||
});
|
||||
|
||||
return { schedulesCreated: scheduleCount, errors, scheduleIds };
|
||||
}
|
||||
|
||||
@@ -234,7 +302,12 @@ export class SchedulesService {
|
||||
}
|
||||
|
||||
const result = await this.getSchedule(schedule.id);
|
||||
await this.auditService.log({ action: 'CREATE', entityType: 'Schedule', entityId: schedule.id, newData: { trainId: dto.trainId, routeId: dto.routeId, departureAt: dep } });
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.CREATE,
|
||||
entityType: AUDIT_ENTITIES.Schedule,
|
||||
entityId: schedule.id,
|
||||
newData: snapshot(schedule, SCHEDULE_AUDIT_FIELDS),
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -352,12 +425,37 @@ export class SchedulesService {
|
||||
await this.routesService.applyRouteToSchedule(dto.routeId, id, plannedTimesMap);
|
||||
|
||||
const result = await this.getSchedule(id);
|
||||
await this.auditService.log({ action: 'UPDATE', entityType: 'Schedule', entityId: id, newData: { trainId: dto.trainId, departureAt: dep } });
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.UPDATE,
|
||||
entityType: AUDIT_ENTITIES.Schedule,
|
||||
entityId: id,
|
||||
oldData: snapshot(schedule, SCHEDULE_AUDIT_FIELDS),
|
||||
newData: snapshot(result as any, SCHEDULE_AUDIT_FIELDS),
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateScheduleStatus(id: string, dto: UpdateScheduleStatusDto) {
|
||||
return this.prisma.trainSchedule.update({ where: { id }, data: { status: dto.status } });
|
||||
const schedule = await this.prisma.trainSchedule.findUnique({ where: { id } });
|
||||
if (!schedule) throw new NotFoundException('Schedule not found');
|
||||
|
||||
const updated = await this.prisma.trainSchedule.update({
|
||||
where: { id },
|
||||
data: { status: dto.status },
|
||||
});
|
||||
|
||||
// CANCELLED is the one transition an operator is asked to justify after the fact, so it
|
||||
// gets its own verb; everything else is a plain status move.
|
||||
await this.auditService.log({
|
||||
action:
|
||||
dto.status === 'CANCELLED' ? AUDIT_ACTIONS.CANCEL : AUDIT_ACTIONS.STATUS_CHANGE,
|
||||
entityType: AUDIT_ENTITIES.Schedule,
|
||||
entityId: id,
|
||||
oldData: { status: schedule.status },
|
||||
newData: { status: updated.status, departureAt: updated.departureAt.toISOString() },
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteSchedule(id: string, cascade = false) {
|
||||
@@ -438,7 +536,13 @@ export class SchedulesService {
|
||||
await this.prisma.travelPackage.deleteMany({ where: { id: { in: packageIds } } });
|
||||
}
|
||||
await this.prisma.trainSchedule.delete({ where: { id } });
|
||||
await this.auditService.log({ action: 'DELETE', entityType: 'Schedule', entityId: id });
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.DELETE,
|
||||
entityType: AUDIT_ENTITIES.Schedule,
|
||||
entityId: id,
|
||||
oldData: snapshot(schedule, SCHEDULE_AUDIT_FIELDS),
|
||||
newData: { cascade, bookingsAffected: (schedule as any)._count?.bookings ?? 0 },
|
||||
});
|
||||
return { deleted: true, id };
|
||||
}
|
||||
|
||||
@@ -456,7 +560,7 @@ export class SchedulesService {
|
||||
});
|
||||
if (!stop) throw new NotFoundException(`Stop at sequence ${sequence} not found on schedule`);
|
||||
|
||||
return this.prisma.tripStopTime.update({
|
||||
const updated = await this.prisma.tripStopTime.update({
|
||||
where: { scheduleId_sequence: { scheduleId, sequence } },
|
||||
data: {
|
||||
plannedArrivalAt: dto.plannedArrivalAt ? parseEthiopianTime(dto.plannedArrivalAt) : undefined,
|
||||
@@ -465,6 +569,14 @@ export class SchedulesService {
|
||||
},
|
||||
include: { station: true },
|
||||
});
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.UPDATE,
|
||||
entityType: AUDIT_ENTITIES.Schedule,
|
||||
entityId: scheduleId,
|
||||
oldData: snapshot(stop, STOP_TIME_AUDIT_FIELDS),
|
||||
newData: snapshot(updated, STOP_TIME_AUDIT_FIELDS),
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,10 +634,19 @@ export class SchedulesService {
|
||||
await this.liveService.updateLiveStatus(scheduleId, { delayMinutes: accumulatedDelayMinutes });
|
||||
|
||||
await this.auditService.log({
|
||||
action: 'UPDATE',
|
||||
entityType: 'Schedule',
|
||||
action: AUDIT_ACTIONS.UPDATE,
|
||||
entityType: AUDIT_ENTITIES.Schedule,
|
||||
entityId: scheduleId,
|
||||
newData: { delayMinutes: dto.delayMinutes, fromSequence: dto.fromSequence, accumulatedDelayMinutes },
|
||||
oldData: {
|
||||
delayMinutes: currentLive?.delayMinutes ?? 0,
|
||||
departureAt: schedule.departureAt.toISOString(),
|
||||
},
|
||||
newData: {
|
||||
delayMinutes: dto.delayMinutes,
|
||||
fromSequence: dto.fromSequence,
|
||||
accumulatedDelayMinutes,
|
||||
stopsShifted: stopsToShift.length,
|
||||
},
|
||||
});
|
||||
|
||||
return this.getSchedule(scheduleId);
|
||||
@@ -547,7 +668,12 @@ export class SchedulesService {
|
||||
const validFrom = dto.validFrom ? parseEthiopianTime(dto.validFrom) : now;
|
||||
const validUntil = dto.validUntil ? parseEthiopianTime(dto.validUntil) : null;
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const superseded = await this.prisma.fareRule.findFirst({
|
||||
where: { tripId: scheduleId, seatClassId, validUntil: null },
|
||||
orderBy: { validFrom: 'desc' },
|
||||
});
|
||||
|
||||
const created = await this.prisma.$transaction(async (tx) => {
|
||||
await tx.fareRule.updateMany({
|
||||
where: { tripId: scheduleId, seatClassId, validUntil: null },
|
||||
data: { validUntil: now },
|
||||
@@ -557,11 +683,27 @@ export class SchedulesService {
|
||||
include: { seatClass: true },
|
||||
});
|
||||
});
|
||||
|
||||
// A schedule fare is versioned rather than edited, so the audit row pairs the rule that was
|
||||
// closed off with the one that replaced it — otherwise the price change is invisible.
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.UPDATE,
|
||||
entityType: AUDIT_ENTITIES.ScheduleFare,
|
||||
entityId: created.id,
|
||||
oldData: snapshot(superseded, FARE_RULE_AUDIT_FIELDS),
|
||||
newData: {
|
||||
...snapshot(created, FARE_RULE_AUDIT_FIELDS),
|
||||
scheduleId,
|
||||
seatClassName: seatClass.name,
|
||||
},
|
||||
});
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
createFareRule(dto: CreateFareRuleDto) {
|
||||
async createFareRule(dto: CreateFareRuleDto) {
|
||||
const { validFrom, validUntil, scheduleId, nationality, passengerCategory, ...rest } = dto;
|
||||
const result = this.prisma.fareRule.create({
|
||||
const rule = await this.prisma.fareRule.create({
|
||||
data: {
|
||||
...rest,
|
||||
tripId: scheduleId,
|
||||
@@ -571,8 +713,15 @@ export class SchedulesService {
|
||||
},
|
||||
include: { seatClass: true },
|
||||
});
|
||||
result.then(r => this.auditService.log({ action: 'CREATE', entityType: 'FareRule', entityId: r.id, newData: { seatClassId: r.seatClassId, baseFareMinor: r.baseFareMinor } }));
|
||||
return result;
|
||||
// Awaited, not a floating .then(): an unhandled rejection there could outlive the response,
|
||||
// and the row could land after the caller had already moved on.
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.CREATE,
|
||||
entityType: AUDIT_ENTITIES.FareRule,
|
||||
entityId: rule.id,
|
||||
newData: snapshot(rule, FARE_RULE_AUDIT_FIELDS),
|
||||
});
|
||||
return rule;
|
||||
}
|
||||
|
||||
async updateFareRule(id: string, dto: Partial<CreateFareRuleDto>) {
|
||||
@@ -580,7 +729,7 @@ export class SchedulesService {
|
||||
if (!existing) throw new NotFoundException('Fare rule not found');
|
||||
|
||||
const { validFrom, validUntil, scheduleId, nationality, passengerCategory, ...rest } = dto;
|
||||
return this.prisma.fareRule.update({
|
||||
const updated = await this.prisma.fareRule.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...rest,
|
||||
@@ -591,19 +740,32 @@ export class SchedulesService {
|
||||
},
|
||||
include: { seatClass: true },
|
||||
});
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.UPDATE,
|
||||
entityType: AUDIT_ENTITIES.FareRule,
|
||||
entityId: id,
|
||||
oldData: snapshot(existing, FARE_RULE_AUDIT_FIELDS),
|
||||
newData: snapshot(updated, FARE_RULE_AUDIT_FIELDS),
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteFareRule(id: string) {
|
||||
const existing = await this.prisma.fareRule.findUnique({ where: { id } });
|
||||
if (!existing) throw new NotFoundException('Fare rule not found');
|
||||
await this.prisma.fareRule.delete({ where: { id } });
|
||||
await this.auditService.log({ action: 'DELETE', entityType: 'FareRule', entityId: id });
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.DELETE,
|
||||
entityType: AUDIT_ENTITIES.FareRule,
|
||||
entityId: id,
|
||||
oldData: snapshot(existing, FARE_RULE_AUDIT_FIELDS),
|
||||
});
|
||||
return { deleted: true, id };
|
||||
}
|
||||
|
||||
createSegmentFareRule(dto: any) {
|
||||
async createSegmentFareRule(dto: any) {
|
||||
const { validFrom, validUntil, passengerCategory, ...rest } = dto;
|
||||
return this.prisma.segmentFareRule.create({
|
||||
const rule = await this.prisma.segmentFareRule.create({
|
||||
data: {
|
||||
...rest,
|
||||
validFrom: parseEthiopianTime(validFrom),
|
||||
@@ -611,6 +773,13 @@ export class SchedulesService {
|
||||
},
|
||||
include: { seatClass: true, route: true },
|
||||
});
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.CREATE,
|
||||
entityType: AUDIT_ENTITIES.SegmentFareRule,
|
||||
entityId: rule.id,
|
||||
newData: snapshot(rule, SEGMENT_FARE_AUDIT_FIELDS),
|
||||
});
|
||||
return rule;
|
||||
}
|
||||
|
||||
getSegmentFares(routeId: string) {
|
||||
@@ -621,13 +790,25 @@ export class SchedulesService {
|
||||
});
|
||||
}
|
||||
|
||||
deleteSegmentFareRule(id: string) {
|
||||
return this.prisma.segmentFareRule.delete({ where: { id } });
|
||||
async deleteSegmentFareRule(id: string) {
|
||||
const existing = await this.prisma.segmentFareRule.findUnique({ where: { id } });
|
||||
if (!existing) throw new NotFoundException('Segment fare rule not found');
|
||||
const deleted = await this.prisma.segmentFareRule.delete({ where: { id } });
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.DELETE,
|
||||
entityType: AUDIT_ENTITIES.SegmentFareRule,
|
||||
entityId: id,
|
||||
oldData: snapshot(existing, SEGMENT_FARE_AUDIT_FIELDS),
|
||||
});
|
||||
return deleted;
|
||||
}
|
||||
|
||||
updateSegmentFareRule(id: string, dto: any) {
|
||||
async updateSegmentFareRule(id: string, dto: any) {
|
||||
const existing = await this.prisma.segmentFareRule.findUnique({ where: { id } });
|
||||
if (!existing) throw new NotFoundException('Segment fare rule not found');
|
||||
|
||||
const { validFrom, validUntil, passengerCategory, ...rest } = dto;
|
||||
return this.prisma.segmentFareRule.update({
|
||||
const updated = await this.prisma.segmentFareRule.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...rest,
|
||||
@@ -636,6 +817,14 @@ export class SchedulesService {
|
||||
},
|
||||
include: { seatClass: true, route: true },
|
||||
});
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.UPDATE,
|
||||
entityType: AUDIT_ENTITIES.SegmentFareRule,
|
||||
entityId: id,
|
||||
oldData: snapshot(existing, SEGMENT_FARE_AUDIT_FIELDS),
|
||||
newData: snapshot(updated, SEGMENT_FARE_AUDIT_FIELDS),
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
async getFareRules(scheduleId?: string) {
|
||||
@@ -700,6 +889,15 @@ export class SchedulesService {
|
||||
}
|
||||
}
|
||||
|
||||
// One row for the sweep: the operator pressed sync once, and every fare it rewrote is
|
||||
// reconstructable from the FareRule versions it created.
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.SYNC,
|
||||
entityType: AUDIT_ENTITIES.ScheduleFare,
|
||||
entityId: scheduleId,
|
||||
newData: { scheduleId, synced, errorCount: errors.length },
|
||||
});
|
||||
|
||||
return { synced, errors };
|
||||
}
|
||||
|
||||
@@ -718,6 +916,16 @@ export class SchedulesService {
|
||||
);
|
||||
const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
|
||||
await this.routesService.applyRouteToSchedule(schedule.routeId, scheduleId, plannedTimesMap);
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.BULK_UPDATE,
|
||||
entityType: AUDIT_ENTITIES.Schedule,
|
||||
entityId: scheduleId,
|
||||
newData: {
|
||||
recalculatedStopTimes: true,
|
||||
routeId: schedule.routeId,
|
||||
stopCount: plannedTimes.length,
|
||||
},
|
||||
});
|
||||
return { recalculated: true, scheduleId, stopCount: plannedTimes.length };
|
||||
}
|
||||
|
||||
@@ -731,6 +939,12 @@ export class SchedulesService {
|
||||
const inactiveCoach = existingCoaches.find(c => c.status !== 'ACTIVE');
|
||||
if (inactiveCoach) throw new BadRequestException(`Coach ${inactiveCoach.number} is not active`);
|
||||
|
||||
const previous = await this.prisma.coachAssignment.findMany({
|
||||
where: { scheduleId },
|
||||
orderBy: { positionNumber: 'asc' },
|
||||
select: { coachId: true, positionNumber: true },
|
||||
});
|
||||
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId } });
|
||||
|
||||
const data = coaches.map((c) => ({
|
||||
@@ -741,6 +955,20 @@ export class SchedulesService {
|
||||
}));
|
||||
|
||||
await this.prisma.coachAssignment.createMany({ data });
|
||||
|
||||
// Assignment is a wholesale replacement, so both compositions go on one row rather than a
|
||||
// delete row per coach followed by a create row per coach.
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.ASSIGN,
|
||||
entityType: AUDIT_ENTITIES.CoachAssignment,
|
||||
entityId: scheduleId,
|
||||
oldData: { scheduleId, coaches: previous },
|
||||
newData: {
|
||||
scheduleId,
|
||||
coaches: coaches.map((c) => ({ coachId: c.coachId, positionNumber: c.positionNumber })),
|
||||
},
|
||||
});
|
||||
|
||||
return { message: 'Coaches assigned successfully', count: coaches.length };
|
||||
}
|
||||
|
||||
@@ -795,19 +1023,55 @@ export class SchedulesService {
|
||||
|
||||
if (dto.coaches !== undefined) {
|
||||
if (dto.coaches.length > 0) {
|
||||
// Logs its own ASSIGN row; this method only audits the schedule's own fields, so the
|
||||
// two rows describe two facts rather than double-reporting one.
|
||||
await this.assignCoaches(id, dto.coaches);
|
||||
} else {
|
||||
const cleared = await this.prisma.coachAssignment.findMany({
|
||||
where: { scheduleId: id },
|
||||
select: { coachId: true, positionNumber: true },
|
||||
});
|
||||
await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: id } });
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.UNASSIGN,
|
||||
entityType: AUDIT_ENTITIES.CoachAssignment,
|
||||
entityId: id,
|
||||
oldData: { scheduleId: id, coaches: cleared },
|
||||
newData: { scheduleId: id, coaches: [] },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this.getSchedule(id);
|
||||
const result = await this.getSchedule(id);
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
const statusChanged = dto.status !== undefined && dto.status !== schedule.status;
|
||||
await this.auditService.log({
|
||||
action: statusChanged
|
||||
? dto.status === 'CANCELLED'
|
||||
? AUDIT_ACTIONS.CANCEL
|
||||
: AUDIT_ACTIONS.STATUS_CHANGE
|
||||
: AUDIT_ACTIONS.UPDATE,
|
||||
entityType: AUDIT_ENTITIES.Schedule,
|
||||
entityId: id,
|
||||
oldData: snapshot(schedule, SCHEDULE_AUDIT_FIELDS),
|
||||
newData: snapshot(result as any, SCHEDULE_AUDIT_FIELDS),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async removeCoachAssignment(scheduleId: string, coachId: string) {
|
||||
const assignment = await this.prisma.coachAssignment.findFirst({ where: { scheduleId, coachId } });
|
||||
if (!assignment) throw new NotFoundException('Coach assignment not found');
|
||||
await this.prisma.coachAssignment.delete({ where: { id: assignment.id } });
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.UNASSIGN,
|
||||
entityType: AUDIT_ENTITIES.CoachAssignment,
|
||||
entityId: assignment.id,
|
||||
oldData: { scheduleId, coachId, positionNumber: assignment.positionNumber },
|
||||
});
|
||||
return { message: 'Coach assignment removed' };
|
||||
}
|
||||
|
||||
@@ -846,14 +1110,23 @@ export class SchedulesService {
|
||||
},
|
||||
include: { seatClass: true, route: true },
|
||||
});
|
||||
await this.auditService.log({ action: 'CREATE', entityType: 'RouteFareRule', entityId: rule.id, newData: { routeId: dto.routeId, seatClassId: dto.seatClassId, baseFareMinor: dto.baseFareMinor } });
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.CREATE,
|
||||
entityType: AUDIT_ENTITIES.RouteFareRule,
|
||||
entityId: rule.id,
|
||||
newData: {
|
||||
...snapshot(rule, ROUTE_FARE_AUDIT_FIELDS),
|
||||
routeCode: route.code,
|
||||
seatClassName: seatClass.name,
|
||||
},
|
||||
});
|
||||
return rule;
|
||||
}
|
||||
|
||||
async updateRouteFareRule(id: string, dto: { baseFareMinor?: number; surchargeMinor?: number; validFrom?: string; validUntil?: string }) {
|
||||
const rule = await this.prisma.routeFareRule.findUnique({ where: { id } });
|
||||
if (!rule) throw new NotFoundException('Route fare rule not found');
|
||||
return this.prisma.routeFareRule.update({
|
||||
const updated = await this.prisma.routeFareRule.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.baseFareMinor !== undefined && { baseFareMinor: dto.baseFareMinor }),
|
||||
@@ -863,13 +1136,26 @@ export class SchedulesService {
|
||||
},
|
||||
include: { seatClass: true, route: true },
|
||||
});
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.UPDATE,
|
||||
entityType: AUDIT_ENTITIES.RouteFareRule,
|
||||
entityId: id,
|
||||
oldData: snapshot(rule, ROUTE_FARE_AUDIT_FIELDS),
|
||||
newData: snapshot(updated, ROUTE_FARE_AUDIT_FIELDS),
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteRouteFareRule(id: string) {
|
||||
const rule = await this.prisma.routeFareRule.findUnique({ where: { id } });
|
||||
if (!rule) throw new NotFoundException('Route fare rule not found');
|
||||
await this.prisma.routeFareRule.delete({ where: { id } });
|
||||
await this.auditService.log({ action: 'DELETE', entityType: 'RouteFareRule', entityId: id });
|
||||
await this.auditService.log({
|
||||
action: AUDIT_ACTIONS.DELETE,
|
||||
entityType: AUDIT_ENTITIES.RouteFareRule,
|
||||
entityId: id,
|
||||
oldData: snapshot(rule, ROUTE_FARE_AUDIT_FIELDS),
|
||||
});
|
||||
return { deleted: true, id };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user