Export interchange document — a generation error after Djibouti unloading failed the whole request and left the document missing until a manual rerun. Generation is now best-effort (unload never fails on paperwork) and reruns backfill the document.

This commit is contained in:
Hagernesh
2026-07-23 11:31:00 +00:00
parent 4df17f36ce
commit 73f36fe46e
12 changed files with 694 additions and 113 deletions

View File

@@ -8,7 +8,12 @@ import { MaintenanceIntervalRepository } from './maintenance-interval.repository
import { MaintenanceSchedule, MaintenanceStatus, MaintenanceType } from './entities/maintenance-schedule.entity';
import { MaintenanceCost } from './entities/maintenance-cost.entity';
import { Vehicle, VehicleAvailability, VehicleStatus } from '../vehicles/entities/vehicle.entity';
import { CreateMaintenanceScheduleDto, CreateMaintenanceCostDto, UpdateMaintenanceScheduleDto } from './dto/create-maintenance.dto';
import {
CreateMaintenanceScheduleDto,
CreateMaintenanceCostDto,
UpdateMaintenanceScheduleDto,
UpsertMaintenanceIntervalDto,
} from './dto/create-maintenance.dto';
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';
@Injectable()
@@ -107,6 +112,10 @@ export class MaintenanceService {
id: string,
dto: UpdateMaintenanceScheduleDto,
): Promise<MaintenanceSchedule> {
// Status BEFORE the write: completing an already-COMPLETED schedule again
// must not auto-create a second "next" schedule.
const before = await this.scheduleRepository.findOneBy({ id });
await this.scheduleRepository.update(id, {
...dto,
completedDate: dto.completedDate ? new Date(dto.completedDate) : undefined,
@@ -122,8 +131,12 @@ export class MaintenanceService {
// Maintenance finished/aborted → vehicle back in service.
await this.setVehicleMaintenanceState(updated.vehicleId, false);
// If completed, schedule the next maintenance based on interval
if (dto.status === MaintenanceStatus.COMPLETED && updated.odometerReading != null) {
// First transition into COMPLETED with an odometer → auto-schedule next.
if (
dto.status === MaintenanceStatus.COMPLETED &&
before?.status !== MaintenanceStatus.COMPLETED &&
updated.odometerReading != null
) {
await this.scheduleNextMaintenance(updated);
}
} else if (dto.status === MaintenanceStatus.IN_PROGRESS) {
@@ -135,52 +148,75 @@ export class MaintenanceService {
return updated!;
}
/** Define/adjust how often a vehicle needs a service ("oil change every 10,000 km"). */
async upsertInterval(dto: UpsertMaintenanceIntervalDto) {
return this.intervalRepository.upsertInterval(
dto.vehicleId,
dto.maintenanceType,
dto.serviceItem ?? null,
dto.intervalKm ?? null,
dto.intervalDays ?? null,
dto.description ?? null,
);
}
async getIntervals(vehicleId: string) {
return this.intervalRepository.getActiveIntervals(vehicleId);
}
async deactivateInterval(id: string): Promise<{ id: string; deactivated: boolean }> {
await this.intervalRepository.deactivate(id);
return { id, deactivated: true };
}
/**
* Auto-schedule the next service after a completion: matched on the
* completed schedule's (type, serviceItem) interval; one SCHEDULED row
* carrying BOTH thresholds when the interval defines km and days —
* whichever is crossed first makes it due.
*/
private async scheduleNextMaintenance(completed: MaintenanceSchedule): Promise<void> {
try {
// Get maintenance interval for this type
const interval = await this.intervalRepository.getByVehicleAndType(
completed.vehicleId,
completed.maintenanceType as MaintenanceType,
completed.serviceItem,
);
if (!interval) return; // No interval defined, skip auto-scheduling
const now = new Date();
const completedKm = Number(completed.odometerReading ?? 0);
const intervalKm = Number(interval.intervalKm ?? 0);
const intervalDays = Number(interval.intervalDays ?? 0);
if (intervalKm <= 0 && intervalDays <= 0) return;
// Calculate next due based on KM interval
if (interval.intervalKm && interval.intervalKm > 0) {
const nextDueKm = completedKm + Number(interval.intervalKm);
const nextDueKm = intervalKm > 0 ? completedKm + intervalKm : undefined;
const nextDueDate =
intervalDays > 0
? new Date(now.getTime() + intervalDays * 24 * 60 * 60 * 1000)
: undefined;
// Create next scheduled maintenance
const nextSchedule = this.scheduleRepository.create({
const label = interval.serviceItem ? `${interval.serviceItem}: ` : '';
const due = [
nextDueKm != null ? `${nextDueKm} km` : null,
nextDueDate != null ? nextDueDate.toISOString().slice(0, 10) : null,
]
.filter(Boolean)
.join(' / ');
await this.scheduleRepository.save(
this.scheduleRepository.create({
vehicleId: completed.vehicleId,
maintenanceType: completed.maintenanceType,
description: `${interval.description || completed.description} (Next interval: ${nextDueKm} km)`,
serviceItem: completed.serviceItem ?? interval.serviceItem ?? null,
description: `${label}${interval.description || completed.description} (next due: ${due})`,
scheduledDate: now,
nextDueKm,
nextDueDate,
status: MaintenanceStatus.SCHEDULED,
});
await this.scheduleRepository.save(nextSchedule);
}
// Calculate next due based on date interval
if (interval.intervalDays && interval.intervalDays > 0) {
const nextDueDate = new Date(now.getTime() + interval.intervalDays * 24 * 60 * 60 * 1000);
// If no KM-based next maintenance was created, use date-based
if (!interval.intervalKm) {
const nextSchedule = this.scheduleRepository.create({
vehicleId: completed.vehicleId,
maintenanceType: completed.maintenanceType,
description: completed.description,
scheduledDate: now,
nextDueDate,
status: MaintenanceStatus.SCHEDULED,
});
await this.scheduleRepository.save(nextSchedule);
}
}
}),
);
} catch (err) {
this.logger.error(
`Failed to schedule next maintenance for vehicle ${completed.vehicleId}: ${(err as Error).message}`,