mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Add freight demo data seeder and permissions management
- Introduced `DemoFreightDataSeeder` to seed demo freight data including wagons, approval rules, and staff users. - Added `seed:freight-demo` script to `package.json` for easy execution. - Updated permissions for operations officer and added permission checks in various components. - Enhanced sidebar and booking actions to respect user permissions.
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"type-check": "tsc --noEmit",
|
||||
"seed:demo-scheduling": "ts-node -r tsconfig-paths/register src/scripts/seed-demo-scheduling.ts",
|
||||
"seed:freight-demo": "ts-node -r tsconfig-paths/register src/scripts/seed-freight-demo.ts",
|
||||
"seed:fleet-wagons": "bash ../../../docs/new/seeds/seed-fleet-wagons.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -48,6 +48,7 @@ import { DemoBookingsSeeder } from "./seed/demo-bookings.seeder";
|
||||
import { PricingDataSeeder } from "./seed/pricing-data.seeder";
|
||||
import { FileUploadSettingsSeeder } from "./seed/file-upload-settings.seeder";
|
||||
import { FreightPermissionKeyMigrationSeeder } from "./seed/freight-permission-key-migration.seeder";
|
||||
import { DemoFreightDataSeeder } from "./seed/demo-freight-data.seeder";
|
||||
//New Trains, Wagons, Container and Cargo management modules
|
||||
import { TrainsModule } from "./modules/trains/trains.module";
|
||||
import { WagonsModule } from './modules/wagons/wagons.module';
|
||||
@@ -121,6 +122,7 @@ import { OverviewModule } from './modules/overview/overview.module';
|
||||
PricingDataSeeder,
|
||||
FileUploadSettingsSeeder,
|
||||
FreightPermissionKeyMigrationSeeder,
|
||||
DemoFreightDataSeeder,
|
||||
],
|
||||
})
|
||||
export class AppModule implements OnApplicationBootstrap {
|
||||
@@ -133,6 +135,7 @@ export class AppModule implements OnApplicationBootstrap {
|
||||
private readonly pricingDataSeeder: PricingDataSeeder,
|
||||
private readonly fileUploadSettingsSeeder: FileUploadSettingsSeeder,
|
||||
private readonly freightPermissionKeyMigrationSeeder: FreightPermissionKeyMigrationSeeder,
|
||||
private readonly demoFreightDataSeeder: DemoFreightDataSeeder,
|
||||
) { }
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
@@ -144,5 +147,8 @@ export class AppModule implements OnApplicationBootstrap {
|
||||
await this.demoBookingsSeeder.run();
|
||||
await this.pricingDataSeeder.run();
|
||||
await this.fileUploadSettingsSeeder.run();
|
||||
// Idempotent demo data: ≥100 wagons/type, approval chains, 4 staff users.
|
||||
// Each block self-guards on an empty-table check, so this is safe every boot.
|
||||
await this.demoFreightDataSeeder.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,8 @@ export class TrainSchedulingController {
|
||||
}
|
||||
|
||||
@Get("bookable-schedules")
|
||||
// @TrainSchedulingView()
|
||||
// No staff guard: customers hit this while creating a booking to find OPEN
|
||||
// same-route schedules. Do not attach train_scheduling permissions here.
|
||||
@ApiOperation({
|
||||
summary: "OPEN same-route schedules a new booking can target",
|
||||
})
|
||||
|
||||
28
apps/edr-freight-api/src/scripts/seed-freight-demo.ts
Normal file
28
apps/edr-freight-api/src/scripts/seed-freight-demo.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'reflect-metadata';
|
||||
import { config } from 'dotenv';
|
||||
import { resolve } from 'path';
|
||||
|
||||
config({ path: resolve(__dirname, '../../.env') });
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from '../app.module';
|
||||
import { DemoFreightDataSeeder } from '../seed/demo-freight-data.seeder';
|
||||
|
||||
async function main() {
|
||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||
logger: ['error', 'warn', 'log'],
|
||||
});
|
||||
|
||||
try {
|
||||
const seeder = app.get(DemoFreightDataSeeder);
|
||||
await seeder.run();
|
||||
console.log('Freight demo data seeded (wagons, approval rules, staff users).');
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Freight demo seed failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
180
apps/edr-freight-api/src/seed/demo-freight-data.seeder.ts
Normal file
180
apps/edr-freight-api/src/seed/demo-freight-data.seeder.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { WagonStatus } from '@edr/types';
|
||||
import { hashPassword } from '@tria-plc/api-common/utils/argon';
|
||||
import { EUserStatus } from '@tria-plc/api-common/utils/enums/user.enum';
|
||||
import {
|
||||
Employee,
|
||||
Organization,
|
||||
Role,
|
||||
User,
|
||||
UserCredential,
|
||||
UserRole,
|
||||
} from '@tria-plc/iamapi-common';
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
|
||||
import { Wagon } from '../modules/wagons/entities/wagon.entity';
|
||||
import { WagonType } from '../modules/wagon-types/entities/wagon-type.entity';
|
||||
import { ApprovalRule } from '../modules/rule-engine/entities/approval-rule.entity';
|
||||
import { DEFAULT_APPROVAL_RULE_ROWS } from '../modules/rule-engine/approval-rules.defaults';
|
||||
|
||||
const EDR_ORG_KEY = 'edr_freight';
|
||||
const MIN_WAGONS_PER_TYPE = 100;
|
||||
|
||||
/** The four demo staff users, each mapped to a seeded freight role. */
|
||||
const DEMO_STAFF_USERS = [
|
||||
{ email: 'marketing@edr.local', username: 'marketing', roleKey: 'edr_marketing' },
|
||||
{ email: 'operations@edr.local', username: 'operations', roleKey: 'edr_operations_officer' },
|
||||
{ email: 'director@edr.local', username: 'director', roleKey: 'edr_director' },
|
||||
{ email: 'ceo@edr.local', username: 'ceo', roleKey: 'edr_ceo' },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* One-shot demo data: at least 100 wagons per wagon type, the default approval
|
||||
* chains, and four staff users with distinct permissions. Every block guards on
|
||||
* an "is it already populated?" check, so this is safe to run on every boot and
|
||||
* does nothing once the data exists.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DemoFreightDataSeeder {
|
||||
private readonly logger = new Logger(DemoFreightDataSeeder.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async run() {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await this.seedWagons(manager);
|
||||
await this.seedApprovalRules(manager);
|
||||
await this.seedStaffUsers(manager);
|
||||
});
|
||||
}
|
||||
|
||||
/** Ensure every wagon type has at least MIN_WAGONS_PER_TYPE wagons. */
|
||||
private async seedWagons(manager: EntityManager) {
|
||||
const wagonTypeRepo = manager.getRepository(WagonType);
|
||||
const wagonRepo = manager.getRepository(Wagon);
|
||||
|
||||
const wagonTypes = await wagonTypeRepo.find();
|
||||
if (wagonTypes.length === 0) {
|
||||
this.logger.warn('No wagon types found; skipping wagon seed');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const type of wagonTypes) {
|
||||
const existing = await wagonRepo.count({ where: { wagonTypeId: type.id } });
|
||||
if (existing >= MIN_WAGONS_PER_TYPE) {
|
||||
this.logger.log(
|
||||
`Wagon type ${type.code} already has ${existing} wagons; skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const toCreate = MIN_WAGONS_PER_TYPE - existing;
|
||||
const tare = Number(type.tareWeightTons ?? 20);
|
||||
const maxPayload = Number(type.capacityTons ?? 60);
|
||||
const rows = Array.from({ length: toCreate }, (_, i) => {
|
||||
const seq = existing + i + 1;
|
||||
return wagonRepo.create({
|
||||
wagonNumber: `${type.code}-${String(seq).padStart(4, '0')}`,
|
||||
wagonTypeId: type.id,
|
||||
tareWeight: tare,
|
||||
maxPayloadWeight: maxPayload,
|
||||
status: WagonStatus.Available,
|
||||
});
|
||||
});
|
||||
await wagonRepo.save(rows);
|
||||
this.logger.log(`Seeded ${toCreate} wagons for type ${type.code}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Seed the default approval chains when the table is empty. */
|
||||
private async seedApprovalRules(manager: EntityManager) {
|
||||
const repo = manager.getRepository(ApprovalRule);
|
||||
const count = await repo.count();
|
||||
if (count > 0) {
|
||||
this.logger.log(`Approval rules already populated (${count}); skipping`);
|
||||
return;
|
||||
}
|
||||
await repo.save(DEFAULT_APPROVAL_RULE_ROWS.map((row) => repo.create(row)));
|
||||
this.logger.log(`Seeded ${DEFAULT_APPROVAL_RULE_ROWS.length} approval rules`);
|
||||
}
|
||||
|
||||
/** Create the four demo staff users with their roles (idempotent per email). */
|
||||
private async seedStaffUsers(manager: EntityManager) {
|
||||
const organization = await manager.getRepository(Organization).findOne({
|
||||
where: { key: EDR_ORG_KEY },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
if (!organization) {
|
||||
this.logger.warn(`Missing organization ${EDR_ORG_KEY}; skipping staff users`);
|
||||
return;
|
||||
}
|
||||
|
||||
const roleRepo = manager.getRepository(Role);
|
||||
const userRepo = manager.getRepository(User);
|
||||
const credentialRepo = manager.getRepository(UserCredential);
|
||||
const userRoleRepo = manager.getRepository(UserRole);
|
||||
const employeeRepo = manager.getRepository(Employee);
|
||||
|
||||
const password = process.env.DEFAULT_PASSWORD?.trim() || '12345678';
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
for (const staff of DEMO_STAFF_USERS) {
|
||||
const role = await roleRepo.findOne({
|
||||
where: { key: staff.roleKey },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
if (!role) {
|
||||
this.logger.warn(`Missing role ${staff.roleKey}; skipping ${staff.email}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let user = await userRepo.findOne({
|
||||
where: { email: staff.email },
|
||||
select: { id: true, email: true },
|
||||
});
|
||||
if (!user) {
|
||||
user = await userRepo.save(
|
||||
userRepo.create({
|
||||
email: staff.email,
|
||||
username: staff.username,
|
||||
name: { en: staff.username },
|
||||
isActive: true,
|
||||
hasSetPassword: true,
|
||||
status: EUserStatus.ACCEPTED,
|
||||
}),
|
||||
);
|
||||
this.logger.log(`Seeded staff user ${staff.email}`);
|
||||
}
|
||||
|
||||
const hasCredential = await credentialRepo.exists({
|
||||
where: { userId: user.id, isActive: true },
|
||||
});
|
||||
if (!hasCredential) {
|
||||
await credentialRepo.insert({
|
||||
userId: user.id,
|
||||
password: hashedPassword,
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
|
||||
await userRoleRepo.upsert(
|
||||
{ userId: user.id, roleId: role.id, organizationId: organization.id },
|
||||
{ conflictPaths: { userId: true, roleId: true } },
|
||||
);
|
||||
|
||||
const hasEmployee = await employeeRepo.exists({
|
||||
where: { userId: user.id, organizationId: organization.id, isCurrent: true },
|
||||
});
|
||||
if (!hasEmployee) {
|
||||
await employeeRepo.insert({
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
isCurrent: true,
|
||||
name: { en: staff.username },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log('Ensured demo staff users (marketing@, operations@, director@, ceo@)');
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,11 @@ export const EDR_FREIGHT_ROLES: FreightSeedRole[] = [
|
||||
name: { en: "EDR Line Staff" },
|
||||
permissionKeys: [...ROLE_PERMISSION_PRESETS.lineStaff],
|
||||
},
|
||||
{
|
||||
key: "edr_operations_officer",
|
||||
name: { en: "EDR Operations Officer" },
|
||||
permissionKeys: [...ROLE_PERMISSION_PRESETS.operationsOfficer],
|
||||
},
|
||||
{
|
||||
key: "edr_director",
|
||||
name: { en: "EDR Director" },
|
||||
|
||||
@@ -121,6 +121,9 @@ const allRuleEngineViewKeys = () =>
|
||||
RULE_ENGINE_RESOURCE_SLUGS.map((s) => FREIGHT_PERMS.ruleEngine.view(s));
|
||||
|
||||
export const ROLE_PERMISSION_PRESETS = {
|
||||
// Marketing / line staff: drives a booking from intake through line-staff
|
||||
// approval and contract generation/signing — i.e. until the contract is ready
|
||||
// and signed. No director/CEO approval, no scheduling, no operations.
|
||||
lineStaff: [
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
FREIGHT_PERMS.bookings.staffAccept,
|
||||
@@ -129,6 +132,12 @@ export const ROLE_PERMISSION_PRESETS = {
|
||||
FREIGHT_PERMS.bookings.approveLineStaff,
|
||||
FREIGHT_PERMS.bookings.rejectApproval,
|
||||
FREIGHT_PERMS.bookings.cancel,
|
||||
...allRuleEngineViewKeys(),
|
||||
],
|
||||
// Operations Officer: train scheduling + wagon allocation + transit/complete.
|
||||
operationsOfficer: [
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
FREIGHT_PERMS.bookings.operations,
|
||||
FREIGHT_PERMS.trainScheduling.view,
|
||||
FREIGHT_PERMS.trainScheduling.manage,
|
||||
...allRuleEngineViewKeys(),
|
||||
@@ -147,8 +156,15 @@ export const ROLE_PERMISSION_PRESETS = {
|
||||
...allRuleEngineViewKeys(),
|
||||
],
|
||||
finance: [FREIGHT_PERMS.bookings.view],
|
||||
// Marketing handles intake through contract (same as line staff here).
|
||||
marketing: [
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
FREIGHT_PERMS.bookings.staffAccept,
|
||||
FREIGHT_PERMS.bookings.requestChanges,
|
||||
FREIGHT_PERMS.bookings.reject,
|
||||
FREIGHT_PERMS.bookings.approveLineStaff,
|
||||
FREIGHT_PERMS.bookings.rejectApproval,
|
||||
FREIGHT_PERMS.bookings.cancel,
|
||||
FREIGHT_PERMS.bookings.generateContract,
|
||||
FREIGHT_PERMS.bookings.signStaff,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user