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@)'); } }