Package passenger numbers and pricing updates

This commit is contained in:
Stephanos A
2026-07-05 18:14:42 +03:00
parent 9e77ca7865
commit afd30c36a0
20 changed files with 537 additions and 105 deletions

View File

@@ -0,0 +1,109 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../common/prisma.service';
const SEED_FLAG = 'SEED_SEGMENT_FARES';
/**
* Seeds SegmentFareRule rows for every origin→destination pair on the
* Addis AbabaDjibouti route across all active seat classes.
*
* Skip-if-loaded: uses Prisma upsert on the unique constraint
* (routeId, originStopSequence, destinationStopSequence, seatClassId, nationality).
* Re-running is safe — existing rows are updated in-place.
*/
@Injectable()
export class SegmentFareSeeder {
private readonly logger = new Logger(SegmentFareSeeder.name);
constructor(private readonly prisma: PrismaService) {}
async run() {
if (process.env[SEED_FLAG]?.trim().toLowerCase() !== 'true') {
this.logger.log(`Skipping segment fare seed — set ${SEED_FLAG}=true to enable`);
return;
}
const route = await this.prisma.route.findFirst({
where: { active: true },
include: { stops: { orderBy: { sequence: 'asc' } } },
});
if (!route) {
this.logger.warn('No active route found — skipping segment fare seed');
return;
}
const seatClasses = await this.prisma.seatClass.findMany({
where: { isActive: true },
});
if (!seatClasses.length) {
this.logger.warn('No active seat classes found — skipping segment fare seed');
return;
}
const stops = route.stops;
const validFrom = new Date('2025-01-01T00:00:00.000Z');
// Rate table: ETB minor units per km, keyed by (nationalityType, bedPosition)
// null bedPosition = regular seat
const rateTable: Record<string, Record<string | 'null', number>> = {
LOCAL: { null: 3000, UPPER: 4000, MIDDLE: 5500, LOWER: 6000 },
INTERNATIONAL: { null: 6000, UPPER: 8000, MIDDLE: 11000, LOWER: 12000 },
};
let upserted = 0;
for (const seatClass of seatClasses) {
const natType = seatClass.nationalityType ?? 'LOCAL';
const bedPos = seatClass.bedPosition ?? 'null';
const ratePerKm = rateTable[natType]?.[bedPos] ?? rateTable['LOCAL']['null'];
for (let i = 0; i < stops.length - 1; i++) {
for (let j = i + 1; j < stops.length; j++) {
const origin = stops[i];
const dest = stops[j];
// Approximate distance: sum of per-stop distanceKm if available,
// otherwise fall back to sequence-gap × 50 km.
let distanceKm = 0;
for (let k = i; k < j; k++) {
distanceKm += stops[k + 1].distanceKm ?? 50;
}
const baseFareMinor = Math.round(distanceKm * ratePerKm);
await this.prisma.segmentFareRule.upsert({
where: {
routeId_originStopSequence_destinationStopSequence_seatClassId_nationality: {
routeId: route.id,
originStopSequence: origin.sequence,
destinationStopSequence: dest.sequence,
seatClassId: seatClass.id,
nationality: natType,
},
},
update: { baseFareMinor, validFrom },
create: {
routeId: route.id,
originStopSequence: origin.sequence,
destinationStopSequence: dest.sequence,
seatClassId: seatClass.id,
nationality: natType,
baseFareMinor,
currency: 'ETB',
validFrom,
},
});
upserted++;
}
}
}
this.logger.log(
`Segment fare seed complete — ${upserted} rules upserted ` +
`(${stops.length} stops × ${seatClasses.length} seat classes)`,
);
}
}