import { MiddlewareConsumer, Module, OnApplicationBootstrap, RequestMethod, } from "@nestjs/common"; import { ConfigModule, ConfigService } from "@nestjs/config"; import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm"; import { ScheduleModule } from "@nestjs/schedule"; import { EventEmitterModule } from "@nestjs/event-emitter"; import { DataSource, DataSourceOptions } from "typeorm"; import { ensurePostgresSchemas, APPLICATION_SEARCH_PATH, } from "./config/ensure-postgres-schemas"; import { IamBaselineSeeder, IamSeedModule } from "@edr/iam-seed"; import { IamModule } from "@tria-plc/iamapi-common"; import { SharedAuthModule } from "@tria-plc/api-common/modules/auth/shared-auth.module"; import { MezgebModule } from "@tria-plc/auditlog"; import appConfig from "./config/app.config"; import databaseConfig from "./config/database.config"; import telebirrConfig from "./config/telebirr.config"; import rabbitmqConfig from "./config/rabbitmq.config"; import faydaConfig from "./config/fayda.config"; import eimsConfig from "./config/eims.config"; import { BookingsModule } from "./modules/bookings/bookings.module"; import { ContractsModule } from "./modules/contracts/contracts.module"; import { SignaturesModule } from "./modules/signatures/signatures.module"; import { FilesModule } from "./modules/files/files.module"; import { ConsignmentsModule } from "./modules/consignments/consignments.module"; // import { TrainsModule } from "./modules/trains/trains.module"; import { LocomotivesModule } from "./modules/locomotives/locomotives.module"; import { TruckTypesModule } from "./modules/truck-types/truck-types.module"; import { TransitAgentsModule } from "./modules/transit-agents/transit-agents.module"; import { WagonTypesModule } from "./modules/wagon-types/wagon-types.module"; import { TrainSetsModule } from "./modules/train-sets/train-sets.module"; import { TrainSchedulesModule } from "./modules/train-schedules/train-schedules.module"; import { TrainSchedulingModule } from "./modules/train-scheduling/train-scheduling.module"; import { SchedulingRescheduleModule } from "./modules/scheduling-reschedule/scheduling-reschedule.module"; import { CompaniesModule } from "./modules/companies/companies.module"; import { TrackingModule } from "./modules/tracking/tracking.module"; import { BillingModule } from "./modules/billing/billing.module"; import { NotificationsModule } from "./modules/notifications/notifications.module"; import { NotificationInboxModule } from "./modules/notification-inbox/notification-inbox.module"; import { SupportChatModule } from "./modules/support-chat/support-chat.module"; import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module"; import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module"; import { ExchangeSettingsModule } from "./modules/exchange-settings/exchange-settings.module"; import { ContractTemplatesModule } from "./modules/contract-templates/contract-templates.module"; import { SupportContentModule } from "./modules/support-content/support-content.module"; import { OtpModule } from "./modules/otp/otp.module"; import { HealthModule } from "./modules/health/health.module"; import { RuleEngineModule } from "./modules/rule-engine/rule-engine.module"; import { BackofficeModule } from "./modules/backoffice/backoffice.module"; import { FreightAuthModule } from "./modules/auth/freight-auth.module"; import { EDR_FREIGHT_APPLICATION, EDR_FREIGHT_PERMISSIONS, } from "./seed/edr-freight.seed"; import { EdrOrgSeeder } from "./seed/edr-org.seeder"; import { FreightPositionsSeeder } from "./seed/freight-positions.seeder"; // Disabled seeds — imports commented out with their provider/injection/run below. // import { DemoUsersSeeder } from "./seed/demo-users.seeder"; // import { FreightStaffUsersSeeder } from "./seed/freight-staff-users.seeder"; import { PaymentModule } from "./modules/payment/payment.module"; // import { PricingDataSeeder } from "./seed/pricing-data.seeder"; import { FileUploadSettingsSeeder } from "./seed/file-upload-settings.seeder"; import { SupportContentSeeder } from "./seed/support-content.seeder"; // import { YardFacilitiesSeeder } from "./seed/yard-facilities.seeder"; // import { IndodeFacilitySeeder } from "./seed/indode-facility.seeder"; // import { Batch14TestDataSeeder } from "./seed/batch1-4-test-data.seeder"; // import { Batch5TestDataSeeder } from "./seed/batch5-test-data.seeder"; // import { Batch7TestDataSeeder } from "./seed/batch7-test-data.seeder"; // import { Batch8TestDataSeeder } from "./seed/batch8-test-data.seeder"; // import { WarehouseDemoSeeder } from "./seed/warehouse-demo.seeder"; // import { ExportDjiboutiInterchangeDemoSeeder } from "./seed/export-djibouti-interchange-demo.seeder"; // import { MarshallingDemoTrainsSeeder } from "./seed/marshalling-demo-trains.seeder"; import { FreightPermissionKeyMigrationSeeder } from "./seed/freight-permission-key-migration.seeder"; import { FreightNotificationPermissionsSeeder } from "./seed/freight-notification-permissions.seeder"; // import { DemoFreightDataSeeder } from "./seed/demo-freight-data.seeder"; // import { GovCompaniesSeeder } from "./seed/gov-companies.seeder"; import { ApprovedFirstLastMileDemoBookingsSeeder } from "./seed/approved-first-lastmile-demo-bookings.seeder"; import { PaidImportExportMileDemoSeeder } from "./seed/paid-import-export-mile-demo.seeder"; //New Trains, Wagons, Container and Cargo management modules import { TrainsModule } from "./modules/trains/trains.module"; import { VerifaydaModule } from "./modules/verifayda/verifayda.module"; import { EimsModule } from "./modules/eims/eims.module"; import { FleetHistoryModule } from "./modules/fleet-history/fleet-history.module"; import { WagonsModule } from "./modules/wagons/wagons.module"; import { ContainersModule } from "./modules/container-management/containers.module"; import { CargoesModule } from "./modules/cargoes/cargoes.module"; import { RoutesModule } from "./modules/routes/routes.module"; import { WarehousesModule } from "./modules/warehouses/warehouses.module"; import { OverviewModule } from "./modules/overview/overview.module"; import { ReportsModule } from "./modules/reports/reports.module"; import { UserTradeAccessModule } from "./modules/user-trade-access/user-trade-access.module"; import { VehiclesModule } from "./modules/vehicles/vehicles.module"; import { DriversModule } from "./modules/drivers/drivers.module"; import { FuelModule } from "./modules/fuel/fuel.module"; import { MaintenanceModule } from "./modules/maintenance/maintenance.module"; import { ComplianceModule } from "./modules/compliance/compliance.module"; import { IncidentsModule } from "./modules/incidents/incidents.module"; import { ProcurementModule } from "./modules/procurement/procurement.module"; import { FacilitiesModule } from "./modules/facilities/facilities.module"; import { GpsTrackingModule } from "./modules/gps-tracking/gps-tracking.module"; import { FirstMileModule } from "./modules/first-mile/first-mile.module"; import { LastMileModule } from "./modules/last-mile/last-mile.module"; import { LastMileRequestsModule } from "./modules/last-mile-requests/last-mile-requests.module"; import { InterchangeDocumentsModule } from "./modules/interchange-documents/interchange-documents.module"; import { ImportOperationsModule } from "./modules/import-operations/import-operations.module"; import { AiModule } from "./modules/ai/ai.module"; import { AuditModule } from "./modules/audit/audit.module"; import { LoggerMiddleware } from "./logger.middleware"; import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middleware"; import { PositionTypePermissionsCache } from "./common/position-type-permissions.cache"; if (!process.env.APPLICATION_NAME) { process.env.APPLICATION_NAME = "freight"; } @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [ appConfig, databaseConfig, telebirrConfig, rabbitmqConfig, faydaConfig, eimsConfig, ], }), ScheduleModule.forRoot(), EventEmitterModule.forRoot(), TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService): TypeOrmModuleOptions => config.get("database")!, dataSourceFactory: async (options) => { if (!options) { throw new Error("Missing TypeORM DataSource options"); } await ensurePostgresSchemas(options as DataSourceOptions); const dataSource = new DataSource(options as DataSourceOptions); await dataSource.initialize(); // The remote edr_dev DB sits behind a connection pooler/proxy that rejects // the Postgres `options` startup parameter (08P01). Instead of setting // search_path at connect time, apply it per physical connection: the pg // Pool emits `connect` for every new client (initial fill, pool growth, // reconnect), so every backend session gets the schema search order. const pool = (dataSource.driver as { master?: unknown }).master as | { on?: (event: string, cb: (client: unknown) => void) => void } | undefined; if (pool?.on) { pool.on("connect", (client) => { (client as { query: (sql: string) => Promise }) .query(`SET search_path TO ${APPLICATION_SEARCH_PATH}`) .catch(() => { /* connection will be validated on first real query */ }); }); } return dataSource; }, }), // Request + entity-level audit logging over RabbitMQ (@tria-plc/auditlog). // Must come after TypeOrmModule above so it picks up this app's DataSource. // rmqUrl falls back the same way notifications.module.ts's RABBITMQ_URL // does: the dev broker only provisions the `edr` user on the `payment` // vhost (docker-compose's RABBITMQ_DEFAULT_USER/VHOST), so an unset // RABBITMQ_URL must land there too, not on guest@'/' (403 ACCESS_REFUSED). MezgebModule.forRoot({ applicationName: "freight-api", rmqUrl: process.env.RABBITMQ_URL ?? process.env.PAYMENT_RABBITMQ_URL ?? "amqp://localhost:5672", }), SharedAuthModule, IamModule.forRoot({ applications: [EDR_FREIGHT_APPLICATION], permissions: EDR_FREIGHT_PERMISSIONS, }), // Replaces the package's DataSeeder. Shared with edr-passenger-api, which // seeds the same `iam` schema — see packages/iam-seed. IamSeedModule.forRoot({ superAdmin: { username: "superadmin", name: { am: "ሱፐር አድሚን", en: "Super Admin" }, roleKey: "super_admin", organizationKey: "edr_freight", unitKey: "edr_freight_app", fallbackEmail: "superadmin@tria.com", }, }), BookingsModule, ContractsModule, SignaturesModule, FilesModule, ConsignmentsModule, LocomotivesModule, TruckTypesModule, TransitAgentsModule, WagonTypesModule, TrainSetsModule, TrainSchedulesModule, TrainSchedulingModule, SchedulingRescheduleModule, CompaniesModule, TrackingModule, BillingModule, NotificationsModule, NotificationInboxModule, SupportChatModule, FileUploadSettingsModule, DropdownSettingsModule, ExchangeSettingsModule, ContractTemplatesModule, SupportContentModule, OtpModule, HealthModule, RuleEngineModule, BackofficeModule, FreightAuthModule, PaymentModule, //New Modules TrainsModule, WagonsModule, ContainersModule, CargoesModule, RoutesModule, WarehousesModule, OverviewModule, ReportsModule, UserTradeAccessModule, VehiclesModule, DriversModule, FuelModule, MaintenanceModule, ComplianceModule, IncidentsModule, ProcurementModule, FacilitiesModule, GpsTrackingModule, FirstMileModule, LastMileModule, LastMileRequestsModule, InterchangeDocumentsModule, ImportOperationsModule, VerifaydaModule, EimsModule, FleetHistoryModule, AiModule, AuditModule, ], providers: [ EdrOrgSeeder, FreightPositionsSeeder, FileUploadSettingsSeeder, SupportContentSeeder, // YardFacilitiesSeeder, FreightPermissionKeyMigrationSeeder, FreightNotificationPermissionsSeeder, // Disabled seeds — providers commented out (imports/injection/run too): // DemoUsersSeeder, // FreightStaffUsersSeeder, // PricingDataSeeder, // DemoFreightDataSeeder, // GovCompaniesSeeder, // IndodeFacilitySeeder, // Batch14TestDataSeeder, // Batch5TestDataSeeder, // Batch7TestDataSeeder, // Batch8TestDataSeeder, // WarehouseDemoSeeder, // ExportDjiboutiInterchangeDemoSeeder, // MarshallingDemoTrainsSeeder, ApprovedFirstLastMileDemoBookingsSeeder, PaidImportExportMileDemoSeeder, LoginAudienceMiddleware, // Feeds position-TYPE grants to the synchronous permission checks — without // it, staff whose permissions live on their position type resolve to none. PositionTypePermissionsCache, ], }) export class AppModule implements OnApplicationBootstrap { constructor( private readonly iamBaselineSeeder: IamBaselineSeeder, private readonly edrOrgSeeder: EdrOrgSeeder, private readonly freightPositionsSeeder: FreightPositionsSeeder, private readonly fileUploadSettingsSeeder: FileUploadSettingsSeeder, private readonly supportContentSeeder: SupportContentSeeder, // private readonly yardFacilitiesSeeder: YardFacilitiesSeeder, private readonly freightPermissionKeyMigrationSeeder: FreightPermissionKeyMigrationSeeder, private readonly freightNotificationPermissionsSeeder: FreightNotificationPermissionsSeeder, // Disabled seeds — injections commented out (imports/provider/run too): // private readonly demoUsersSeeder: DemoUsersSeeder, // private readonly freightStaffUsersSeeder: FreightStaffUsersSeeder, // private readonly pricingDataSeeder: PricingDataSeeder, // private readonly indodeFacilitySeeder: IndodeFacilitySeeder, // private readonly batch14TestDataSeeder: Batch14TestDataSeeder, // private readonly batch5TestDataSeeder: Batch5TestDataSeeder, // private readonly batch7TestDataSeeder: Batch7TestDataSeeder, // private readonly batch8TestDataSeeder: Batch8TestDataSeeder, // private readonly warehouseDemoSeeder: WarehouseDemoSeeder, // private readonly exportDjiboutiInterchangeDemoSeeder: ExportDjiboutiInterchangeDemoSeeder, // private readonly marshallingDemoTrainsSeeder: MarshallingDemoTrainsSeeder, // private readonly demoFreightDataSeeder: DemoFreightDataSeeder, // private readonly govCompaniesSeeder: GovCompaniesSeeder, ) { } async onApplicationBootstrap() { // ── Enabled: permissions + file-upload settings (+ dropdown settings) only ── // Everything else below is intentionally disabled. Seeders stay registered // as providers and injected; only their .run() calls are commented out, so // re-enabling any of them is a one-line uncomment. // Permissions foundation — keep enabled: // freightPermissionKeyMigration → renames legacy permission keys // edrOrgSeeder → seeds org/unit + the Permission catalog // iamBaselineSeeder → @edr/iam-seed: IAM app, roles, permissions, // position types, organization types + // default units, org/unit settings and the // super-admin account. Replaces the package's // DataSeeder, and is shared with // edr-passenger-api so one writer owns the // `iam` schema. Runs after edrOrgSeeder // because the super admin attaches to the // edr_freight org/unit. // Writes nothing unless SEED_IAM_BASELINE=true. // freightPositionsSeeder → seeds Position + PositionPermission rows // (depends on edrOrgSeeder, must run after) await this.freightPermissionKeyMigrationSeeder.run(); await this.edrOrgSeeder.run(); await this.iamBaselineSeeder.run(); await this.freightPositionsSeeder.run(); // freightNotificationPermissions → seeds the :get_notification // keys and backfills them onto whoever // already holds each desk's anchor // permission. Runs LAST in this block so // it sees a freshly-seeded catalog and // freshly-seeded positions. Unlike the // seeders above it is NOT gated behind // SEED_EDR_ORG — without it every staff // notification resolves to no one. await this.freightNotificationPermissionsSeeder.run(); // File upload settings — keep enabled. await this.fileUploadSettingsSeeder.run(); // Portal help/FAQ/legal copy — keep enabled. Idempotent by emptiness, so // it fills an empty table once and never touches admin edits afterwards. await this.supportContentSeeder.run(); // Flags which yards can load/unload cargo (Indode, Sebeta, Modjo, Adama, // Dire Dawa). Idempotent; creates no yards. // await this.yardFacilitiesSeeder.run(); // Dropdown settings are not seeded on boot; run them with // `pnpm seed:dropdown-settings` (src/scripts/seed-dropdown-settings.ts). // ── Disabled: demo / test / reference data seeds ── // Uncomment a line to re-enable that seed. // await this.demoUsersSeeder.run(); // await this.freightStaffUsersSeeder.run(); // await this.pricingDataSeeder.run(); // IndodeFacilitySeeder keys its warehouses on INDODE_OPEN / INDODE_CLOSED, so // it will not recognise a hand-created Indode warehouse and will seed a second // one alongside it. Only enable it against an Indode that has no warehouse. // await this.indodeFacilitySeeder.run(); // await this.batch14TestDataSeeder.run(); // await this.batch5TestDataSeeder.run(); // await this.batch7TestDataSeeder.run(); // await this.batch8TestDataSeeder.run(); // await this.warehouseDemoSeeder.run(); // await this.exportDjiboutiInterchangeDemoSeeder.run(); // await this.marshallingDemoTrainsSeeder.run(); // demoFreightDataSeeder seeds ONLY the 4 staff users (wagons + approval // rules are already disabled inside the seeder). // await this.demoFreightDataSeeder.run(); // Government entities (importer/exporter profiles) that government bookings // bill to. Idempotent — keyed by fixed IDs. // await this.govCompaniesSeeder.run(); } configure(consumer: MiddlewareConsumer) { consumer.apply(LoggerMiddleware).forRoutes("*"); consumer .apply(LoginAudienceMiddleware) .forRoutes( { path: "auth/login", method: RequestMethod.POST }, { path: "auth/mfa-verify", method: RequestMethod.POST }, ); } }