diff --git a/.claude/skills/edr-db/SKILL.md b/.claude/skills/edr-db/SKILL.md index 47333128d..dde5f4b8a 100644 --- a/.claude/skills/edr-db/SKILL.md +++ b/.claude/skills/edr-db/SKILL.md @@ -11,7 +11,7 @@ One script, runs from anywhere in the repo (resolves `pg` from `apps/edr-freight node .claude/skills/edr-db/query.cjs "SELECT ... " # run SQL, console.table output node .claude/skills/edr-db/query.cjs explain "SELECT ..." # EXPLAIN-validate only (no rows touched) node .claude/skills/edr-db/query.cjs columns # freight.
column list -node .claude/skills/edr-db/query.cjs migrations [like] # public.migrations rows (newest first) +node .claude/skills/edr-db/query.cjs migrations [like] # migration rows, newest first (freight.migrations + iam.typeorm_migrations) node .claude/skills/edr-db/query.cjs drift
# bare column names, for diffing vs the entity ``` @@ -31,7 +31,11 @@ defaulting to the shared dev database (`edr_dev`). (`WHERE NOT EXISTS` guards) — watch-mode API instances race `migrationsRun`, and non-idempotent statements have double-run here before. - Timestamps for new migrations: must be unique across `src/migrations/` AND - higher than `SELECT max(timestamp) FROM public.migrations`. + higher than `SELECT max(timestamp) FROM freight.migrations`. +- Freight and IAM keep separate histories: `freight.migrations` for + `apps/edr-freight-api/src/migrations/*`, `iam.typeorm_migrations` for the + `@tria-plc/iamapi-common` migrations. `public.migrations` is the pre-split + table, left in place for rollback — never write to it. ## Diagnosing a pasted 400/500 (the recurring loop) diff --git a/.claude/skills/edr-db/query.cjs b/.claude/skills/edr-db/query.cjs index b4797bd45..6f41f555b 100644 --- a/.claude/skills/edr-db/query.cjs +++ b/.claude/skills/edr-db/query.cjs @@ -8,7 +8,7 @@ * node .claude/skills/edr-db/query.cjs "SELECT ... " run SQL (console.table) * node .claude/skills/edr-db/query.cjs explain "SELECT..." EXPLAIN-validate only * node .claude/skills/edr-db/query.cjs columns
list freight.
columns - * node .claude/skills/edr-db/query.cjs migrations [like] public.migrations rows + * node .claude/skills/edr-db/query.cjs migrations [like] freight/iam migration rows * node .claude/skills/edr-db/query.cjs drift
columns vs entity check helper * * Connection: DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME env vars, falling @@ -52,12 +52,27 @@ async function main() { console.table(r.rows); } else if (first === 'migrations') { const like = rest[0] ? `%${rest[0]}%` : '%'; - const r = await c.query( - `SELECT id, timestamp, name FROM public.migrations - WHERE name ILIKE $1 ORDER BY id DESC LIMIT 40`, - [like], - ); - console.table(r.rows); + // Histories are split per owner: freight.migrations (this app) and + // iam.typeorm_migrations (@tria-plc/iamapi-common). public.migrations is the + // pre-split table, kept for rollback — read it only if the split has not + // been applied to this DB yet. + const sources = [ + ['freight', 'freight.migrations'], + ['iam', 'iam.typeorm_migrations'], + ['legacy', 'public.migrations'], + ]; + const rows = []; + for (const [owner, table] of sources) { + const present = await c.query(`SELECT to_regclass($1) IS NOT NULL AS ok`, [table]); + if (!present.rows[0].ok) continue; + const r = await c.query( + `SELECT id, timestamp, name FROM ${table} + WHERE name ILIKE $1 ORDER BY id DESC LIMIT 40`, + [like], + ); + rows.push(...r.rows.map((row) => ({ owner, ...row }))); + } + console.table(rows); } else if (first === 'drift') { // Quick drift signal: DB columns for the table. Compare by eye against // the entity's @Column names; a recorded-but-absent column = drift. diff --git a/.gitignore b/.gitignore index b9dc16c3c..977a34353 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,9 @@ test-results/ playwright-report/ blob-report/ RUNNING_LOCALLY.md + +# Generated per-shard compose file for the integration suite (it.mjs). +integration/.it-shards.yaml +branch_structure.json +temp_auto_push.bat +temp_interactive_push.bat diff --git a/apps/edr-freight-api/.env.example b/apps/edr-freight-api/.env.example index d6a39bfed..16ee9cd57 100644 --- a/apps/edr-freight-api/.env.example +++ b/apps/edr-freight-api/.env.example @@ -1,5 +1,17 @@ # Copy to .env for local/docker compose (not committed). PORT=3001 +# @tria-plc/auditlog's client interceptor stamps every AuditLog row's +# `application` from this env var directly, bypassing MezgebModule.forRoot's +# applicationName option (package quirk). audit.controller.ts reads the same +# var when filtering reads, so this can be anything as long as it's set. +APPLICATION_NAME=freight-api +# Also required for @tria-plc/auditlog: its producer (AuditClientModule) +# reads the RMQ URL at package IMPORT time, before MezgebModule.forRoot's +# rmqUrl option ever runs, so only an env var reaches it — an in-code +# override is too late. Without this, audit events are silently dropped +# (no error, nothing published). Point it at whatever broker/vhost your +# RabbitMQ actually has a user provisioned on. +RABBITMQ_URL=amqp://localhost:5672 # GT06 GPS tracker TCP listener port (raw TCP, must be reachable by tracker SIMs). 0 disables. GT06_TCP_PORT=5023 DB_HOST=localhost @@ -30,6 +42,12 @@ FREIGHT_PORTAL_URL=http://localhost:5173 # Point these at the freight portal's public payment result routes. PAYMENT_RETURN_URL=http://localhost:5173/payment/success PAYMENT_FAILURE_URL=http://localhost:5173/payment/failure + +# Drain tail (minutes) added to every booking pay window before anything expires: +# settlement is asynchronous, so a payment made in the window's last seconds lands +# after the deadline. Nothing is expired, no wagons are resold and no window cycle +# concludes until the tail passes. Defaults to 5 when unset. +FREIGHT_PAYMENT_DRAIN_MINUTES=5 # JWT (used by @tria-plc/api-common SharedAuthModule) JWT_SECRET= JWT_ACCESS_TOKEN_SECRET= @@ -92,10 +110,10 @@ FAYDA_PRIVATE_KEY_BASE64= # OAuth redirect_uri for MOBILE clients (must be registered with eSignet) FAYDA_REDIRECT_URI=http://localhost:3001/api/fayda/verification/complete # OAuth redirect_uri for WEB clients. Defaults to FAYDA_REDIRECT_URI when unset. -FAYDA_WEB_REDIRECT_URI=http://localhost:3000/callback +FAYDA_WEB_REDIRECT_URI=http://localhost:3000/fayda/callback # OAuth redirect_uri for the customer portal (its own origin — must also be # registered with eSignet). Defaults to FAYDA_WEB_REDIRECT_URI when unset. -FAYDA_PORTAL_REDIRECT_URI=http://localhost:5173/callback +FAYDA_PORTAL_REDIRECT_URI=http://localhost:5173/fayda/callback CLIENT_ASSERTION_TYPE=urn:ietf:params:oauth:client-assertion-type:jwt-bearer FAYDA_SCOPE=openid profile email phone address FAYDA_ACR_VALUES=mosip:idp:acr:generated-code diff --git a/apps/edr-freight-api/docs/FREIGHT_MASTER_FLOW.md b/apps/edr-freight-api/docs/FREIGHT_MASTER_FLOW.md index ea68e6938..98657843c 100644 --- a/apps/edr-freight-api/docs/FREIGHT_MASTER_FLOW.md +++ b/apps/edr-freight-api/docs/FREIGHT_MASTER_FLOW.md @@ -21,7 +21,7 @@ flowchart TD S0(["Customer visits portal"]):::start S0 --> S1["Signup via IAM
GET /auth/check-availability @Public
POST /otp/send + /otp/verify (P)"]:::port S1 --> S2{"Identity proofing
(VeriFayda)?"}:::dec - S2 -->|"Yes"| S3["POST /fayda/verification/start →
/callback → /complete
upsert iam.users (verified_by=fayda) (P)"]:::port + S2 -->|"Yes"| S3["POST /fayda/verification/start →
/fayda/callback → /complete
upsert iam.users (verified_by=fayda) (P)"]:::port S2 -->|"No"| S4 S3 --> S4["POST /companies/onboarding/start
draft company (placeholder TIN, PENDING) (P)"]:::port S4 --> S4b["Wizard: PATCH /profile, /onboarding-step,
upload license + docs
GET /onboarding/requirements (P)"]:::port diff --git a/apps/edr-freight-api/docs/FREIGHT_SYSTEM_FLOW.md b/apps/edr-freight-api/docs/FREIGHT_SYSTEM_FLOW.md index a6fd8bbeb..b29763060 100644 --- a/apps/edr-freight-api/docs/FREIGHT_SYSTEM_FLOW.md +++ b/apps/edr-freight-api/docs/FREIGHT_SYSTEM_FLOW.md @@ -131,7 +131,7 @@ sequenceDiagram `HasActiveDelegationGuard` as **global `APP_GUARD`s** — *every* route is JWT-protected unless it carries `@Public()`. Fine-grained `FreightPermissionGuard([perm])` decorators add permission checks on staff routes. Explicitly **public** endpoints: `GET /api/files/:fileId`, `POST /api/otp/{send,verify}`, -`GET /api/auth/check-availability`, the `fayda/verification/*` + `/callback` endpoints, +`GET /api/auth/check-availability`, the `fayda/verification/*` + `/fayda/callback` endpoints, `GET /api/payments/{checkout,receipt/:orderId}`, and the service-to-service `POST /api/internal/payments/mark-paid`. Real login / JWT issuance lives in the **external IAM package**, not this repo. (Note: `@edr/api-common`'s `@Public` and `@tria-plc/api-common`'s `@IsPublic` both set the same `"isPublic"` metadata key the guard reads.) @@ -288,7 +288,7 @@ flowchart TD chk --> otp["POST /otp/send + /otp/verify (P) @Public"] otp --> fayda{"Identity proofing?"} fayda -->|"VeriFayda 2.0"| fstart["POST /fayda/verification/start
→ eSignet authorize URL"] - fstart --> fcb["Fayda redirect → GET /callback (ack)
→ GET /fayda/verification/complete
(PKCE code exchange → upsert iam.users)"] + fstart --> fcb["Fayda redirect → GET /fayda/callback (ack)
→ GET /fayda/verification/complete
(PKCE code exchange → upsert iam.users)"] fcb --> onb fayda -->|"skip"| onb @@ -313,7 +313,7 @@ drives the required document set. Booking guards elsewhere `403` if the acting p | POST | `/api/fayda/verification/start` | start eSignet session (PKCE) | `@Public` + OptionalJwt | (B) verifayda.service | | GET | `/api/fayda/verification/complete` | code→identity, upsert `iam.users` | `@Public` | (B) verifayda.service | | GET | `/api/fayda/verification/status` | current user's Fayda link | JwtGuard | — | -| GET | `/callback` | passive Fayda redirect ack (no `/api`) | `@Public` | popup postMessage | +| GET | `/fayda/callback` | passive Fayda redirect ack (no `/api`) | `@Public` | popup postMessage | | GET·PUT | `/api/me/signature` | reusable signature (MinIO, base64) | JwtGuard | (P)(B) signatures.service | | GET | `/api/test_user1` · `/api/test_user2` | permission-guard demo | `PermissionGuard` | (B) demo pages | | GET | `/api/companies/getInfo` · `/profile` · `/dashboard` | company info / KPIs | JwtGuard | (P) companies.service | diff --git a/apps/edr-freight-api/package.json b/apps/edr-freight-api/package.json index d10f19f10..c4837c8a7 100644 --- a/apps/edr-freight-api/package.json +++ b/apps/edr-freight-api/package.json @@ -59,6 +59,7 @@ "@nestjs/typeorm": "^11.0.1", "@nestjs/websockets": "^11.1.27", "@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.6.0.tgz", + "@tria-plc/auditlog": "file:../../local-packages/tria-plc-auditlog-1.1.2.tgz", "@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz", "amqp-connection-manager": "^5.0.0", "amqplib": "^2.0.1", diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index 66f64678a..d41b77dce 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -16,6 +16,7 @@ import { 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"; @@ -46,6 +47,7 @@ import { NotificationInboxModule } from "./modules/notification-inbox/notificati 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 { OtpModule } from "./modules/otp/otp.module"; import { HealthModule } from "./modules/health/health.module"; @@ -65,7 +67,7 @@ import { FreightPositionsSeeder } from "./seed/freight-positions.seeder"; import { PaymentModule } from "./modules/payment/payment.module"; // import { PricingDataSeeder } from "./seed/pricing-data.seeder"; import { FileUploadSettingsSeeder } from "./seed/file-upload-settings.seeder"; -import { YardFacilitiesSeeder } from "./seed/yard-facilities.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"; @@ -89,6 +91,7 @@ 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"; @@ -100,12 +103,18 @@ import { ProcurementModule } from "./modules/procurement/procurement.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"; +if (!process.env.APPLICATION_NAME) { + process.env.APPLICATION_NAME = "freight"; +} + @Module({ imports: [ ConfigModule.forRoot({ @@ -153,6 +162,19 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar 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], @@ -191,6 +213,7 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar SupportChatModule, FileUploadSettingsModule, DropdownSettingsModule, + ExchangeSettingsModule, ContractTemplatesModule, OtpModule, HealthModule, @@ -207,6 +230,7 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar RoutesModule, WarehousesModule, OverviewModule, + ReportsModule, UserTradeAccessModule, VehiclesModule, DriversModule, @@ -218,17 +242,19 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar GpsTrackingModule, FirstMileModule, LastMileModule, + LastMileRequestsModule, InterchangeDocumentsModule, ImportOperationsModule, VerifaydaModule, FleetHistoryModule, AiModule, + AuditModule, ], providers: [ EdrOrgSeeder, FreightPositionsSeeder, FileUploadSettingsSeeder, - YardFacilitiesSeeder, + // YardFacilitiesSeeder, FreightPermissionKeyMigrationSeeder, // Disabled seeds — providers commented out (imports/injection/run too): // DemoUsersSeeder, @@ -255,7 +281,7 @@ export class AppModule implements OnApplicationBootstrap { private readonly edrOrgSeeder: EdrOrgSeeder, private readonly freightPositionsSeeder: FreightPositionsSeeder, private readonly fileUploadSettingsSeeder: FileUploadSettingsSeeder, - private readonly yardFacilitiesSeeder: YardFacilitiesSeeder, + // private readonly yardFacilitiesSeeder: YardFacilitiesSeeder, private readonly freightPermissionKeyMigrationSeeder: FreightPermissionKeyMigrationSeeder, // Disabled seeds — injections commented out (imports/provider/run too): // private readonly demoUsersSeeder: DemoUsersSeeder, @@ -304,7 +330,7 @@ export class AppModule implements OnApplicationBootstrap { // Flags which yards can load/unload cargo (Indode, Sebeta, Modjo, Adama, // Dire Dawa). Idempotent; creates no yards. - await this.yardFacilitiesSeeder.run(); + // await this.yardFacilitiesSeeder.run(); // Dropdown settings are not seeded on boot; run them with // `pnpm seed:dropdown-settings` (src/scripts/seed-dropdown-settings.ts). @@ -335,9 +361,11 @@ export class AppModule implements OnApplicationBootstrap { configure(consumer: MiddlewareConsumer) { consumer.apply(LoggerMiddleware).forRoutes("*"); - consumer.apply(LoginAudienceMiddleware).forRoutes( - { path: "auth/login", method: RequestMethod.POST }, - { path: "auth/mfa-verify", method: RequestMethod.POST }, - ); + consumer + .apply(LoginAudienceMiddleware) + .forRoutes( + { path: "auth/login", method: RequestMethod.POST }, + { path: "auth/mfa-verify", method: RequestMethod.POST }, + ); } } diff --git a/apps/edr-freight-api/src/common/last-mile-charge.util.spec.ts b/apps/edr-freight-api/src/common/last-mile-charge.util.spec.ts new file mode 100644 index 000000000..e2ff1bfd9 --- /dev/null +++ b/apps/edr-freight-api/src/common/last-mile-charge.util.spec.ts @@ -0,0 +1,121 @@ +import { computeLastMileCharge } from './last-mile-charge.util'; +import type { Rate } from '../modules/rule-engine/entities/rate.entity'; + +const rate = (over: Partial): Rate => + ({ + appliesTo: 'LAST_MILE', + status: 'LIVE', + currency: 'ETB', + trigger: 'ALWAYS', + rateType: 'LAST_MILE', + ...over, + }) as Rate; + +const band20a = rate({ + rateUnit: 'PER_KM', + rateValue: 1800, + minKm: 0, + maxKm: 30, + containerType: { sizeFt: 20 } as Rate['containerType'], +}); +const band20b = rate({ + rateUnit: 'PER_KM', + rateValue: 1500, + minKm: 30, + maxKm: null, + containerType: { sizeFt: 20 } as Rate['containerType'], +}); +const band40a = rate({ + rateUnit: 'PER_KM', + rateValue: 2200, + minKm: 0, + maxKm: 30, + containerType: { sizeFt: 40 } as Rate['containerType'], +}); +const bulkRate = rate({ rateUnit: 'PER_TON_KM', rateValue: 25 }); + +describe('computeLastMileCharge', () => { + it('prices containers per band × size × quantity', () => { + const charge = computeLastMileCharge({ + freightType: 'CONTAINER', + tons: 0, + km: 13, + containers: [ + { sizeLabel: '20DC', qty: 5 }, + { sizeLabel: '40HC', qty: 1 }, + ], + liveRates: [band20a, band20b, band40a], + }); + // 13 × 1800 × 5 + 13 × 2200 × 1 + expect(charge).toMatchObject({ mode: 'CONTAINER', total: 117000 + 28600, currency: 'ETB' }); + expect(charge!.lines).toHaveLength(2); + }); + + it('band boundary is half-open: km = 30 falls in the 30+ band', () => { + const charge = computeLastMileCharge({ + freightType: 'CONTAINER', + tons: 0, + km: 30, + containers: [{ sizeLabel: '20DC', qty: 1 }], + liveRates: [band20a, band20b], + }); + expect(charge!.lines[0].unitRate).toBe(1500); + expect(charge!.total).toBe(30 * 1500); + }); + + it('returns null when a size has no matching band', () => { + const charge = computeLastMileCharge({ + freightType: 'CONTAINER', + tons: 0, + km: 50, + containers: [{ sizeLabel: '40HC', qty: 2 }], + liveRates: [band40a], // 40ft only covers 0–30 + }); + expect(charge).toBeNull(); + }); + + it('prices bulk as tons × km × rate', () => { + const charge = computeLastMileCharge({ + freightType: 'BULK', + tons: 60, + km: 26, + containers: [], + liveRates: [bulkRate], + }); + expect(charge).toMatchObject({ mode: 'BULK', total: 60 * 26 * 25, currency: 'ETB' }); + }); + + it('returns null on mixed currencies, unknown km, and uncovered freight types', () => { + const usd40 = rate({ ...band40a, currency: 'USD' }); + expect( + computeLastMileCharge({ + freightType: 'CONTAINER', + tons: 0, + km: 10, + containers: [ + { sizeLabel: '20DC', qty: 1 }, + { sizeLabel: '40HC', qty: 1 }, + ], + liveRates: [band20a, usd40], + }), + ).toBeNull(); + expect( + computeLastMileCharge({ + freightType: 'BULK', + tons: 10, + km: 0, + containers: [], + liveRates: [bulkRate], + }), + ).toBeNull(); + expect( + computeLastMileCharge({ + freightType: 'BREAK_BULK', + tons: 10, + km: 10, + containers: [], + liveRates: [bulkRate], + }), + ).toBeNull(); + }); +}); diff --git a/apps/edr-freight-api/src/common/last-mile-charge.util.ts b/apps/edr-freight-api/src/common/last-mile-charge.util.ts new file mode 100644 index 000000000..706e21238 --- /dev/null +++ b/apps/edr-freight-api/src/common/last-mile-charge.util.ts @@ -0,0 +1,202 @@ +import type { DataSource } from 'typeorm'; +import type { Rate } from '../modules/rule-engine/entities/rate.entity'; +import { bookingContainerSizes } from './truck-load.util'; + +/** One priced line of a rule-based last-mile charge. */ +export interface LastMileChargeLine { + description: string; + quantity: number; + unitRate: number; + amount: number; +} + +/** A fully-resolved rule-based last-mile charge. */ +export interface LastMileCharge { + mode: 'BULK' | 'CONTAINER'; + total: number; + currency: string; + lines: LastMileChargeLine[]; +} + +/** What the last-mile leg is hauling, in the shape the rate rules price. */ +export interface LastMileShipmentShape { + freightType: string | null; + tons: number; + containers: Array<{ sizeLabel: string; qty: number }>; +} + +const round2 = (n: number): number => Math.round(n * 100) / 100; + +/** + * Price a last-mile leg off the LIVE rate rules. Pure — pass the live rates in. + * + * BULK: one PER_TON_KM rate → price = tons × km × rate. + * CONTAINER: per container size, the PER_KM rate whose distance band holds the + * km (bands are half-open [minKm, maxKm), NULL maxKm = open-ended) → price = + * km × rate × quantity, summed across sizes. + * + * Returns null whenever the rules don't fully cover the shipment (no rate, a + * container size without a matching band, mixed currencies, km/tons unknown) — + * callers keep their existing pricing as the fallback. Never throws. + */ +export function computeLastMileCharge(input: { + freightType: string | null; + tons: number; + km: number; + containers: Array<{ sizeLabel: string; qty: number }>; + /** LIVE rates with the containerType relation loaded (findLiveRatesDetailed). */ + liveRates: Rate[]; +}): LastMileCharge | null { + const { freightType, tons, km, containers, liveRates } = input; + if (!km || km <= 0) return null; + + const candidates = liveRates.filter( + (rate) => rate.appliesTo === 'LAST_MILE' && rate.status === 'LIVE', + ); + + if (freightType === 'BULK') { + if (!tons || tons <= 0) return null; + const rate = candidates.find((r) => r.rateUnit === 'PER_TON_KM'); + if (!rate) return null; + const unitRate = Number(rate.rateValue); + const amount = round2(tons * km * unitRate); + return { + mode: 'BULK', + total: amount, + currency: rate.currency, + lines: [ + { + description: `Last-mile bulk delivery — ${tons} t × ${km} km × ${unitRate}/t·km`, + quantity: tons, + unitRate, + amount, + }, + ], + }; + } + + if (freightType === 'CONTAINER') { + if (!containers.length) return null; + const lines: LastMileChargeLine[] = []; + const currencies = new Set(); + for (const group of containers) { + const rate = candidates.find( + (r) => + r.rateUnit === 'PER_KM' && + r.minKm !== null && + r.minKm !== undefined && + r.containerType?.sizeFt !== null && + r.containerType?.sizeFt !== undefined && + group.sizeLabel.includes(String(r.containerType.sizeFt)) && + Number(r.minKm) <= km && + (r.maxKm === null || r.maxKm === undefined || km < Number(r.maxKm)), + ); + // A size the rules don't cover means the rule set can't price this job. + if (!rate) return null; + const unitRate = Number(rate.rateValue); + const amount = round2(km * unitRate * group.qty); + currencies.add(rate.currency); + lines.push({ + description: `Last-mile delivery — ${group.qty} × ${group.sizeLabel} container, ${km} km @ ${unitRate}/km`, + quantity: group.qty, + unitRate, + amount, + }); + } + // A charge can't mix birr and dollar lines on one invoice. + if (currencies.size !== 1) return null; + return { + mode: 'CONTAINER', + total: round2(lines.reduce((sum, line) => sum + line.amount, 0)), + currency: [...currencies][0], + lines, + }; + } + + return null; +} + +/** + * Rule-based charge for an operational last-mile record: prices what its + * trucks actually haul (last_mile_vehicle_containers / weighed net tons) + * against the given km. Shared by setDistances (writes remainingPayment) and + * DELIVERY_FEE invoicing so the two never disagree on the math. Null = the + * rules don't cover this job — callers keep the per-vehicle price/km path. + */ +export async function ruleBasedLastMileCharge( + dataSource: DataSource, + liveRates: Rate[], + lastMileId: string, + km: number, +): Promise { + if (!km || km <= 0) return null; + const [record]: Array<{ bookingId: string }> = await dataSource.query( + `SELECT booking_id AS "bookingId" + FROM freight.last_mile + WHERE id = $1 AND deleted_at IS NULL`, + [lastMileId], + ); + if (!record) return null; + + const containerRows: Array<{ containerNumber: string }> = await dataSource.query( + `SELECT container_number AS "containerNumber" + FROM freight.last_mile_vehicle_containers + WHERE last_mile_id = $1 AND deleted_at IS NULL`, + [lastMileId], + ); + const shape = await lastMileShipmentShape( + dataSource, + record.bookingId, + containerRows.map((r) => r.containerNumber), + ); + + // Bulk: bill the weighed tonnage on this record's trucks when known, + // falling back to the booking's declared VGM total. + const [tonsRow]: Array<{ tons: string | null }> = await dataSource.query( + `SELECT SUM(net_weight_tons) AS "tons" + FROM freight.last_mile_vehicle_assignments + WHERE last_mile_id = $1 AND deleted_at IS NULL`, + [lastMileId], + ); + const weighedTons = Number(tonsRow?.tons ?? 0); + + return computeLastMileCharge({ + ...shape, + tons: weighedTons > 0 ? weighedTons : shape.tons, + km, + liveRates, + }); +} + +/** + * Load a booking's shipment shape for the charge resolver: freight type, bulk + * tonnage, and the container numbers grouped into size × quantity. + */ +export async function lastMileShipmentShape( + dataSource: DataSource, + bookingId: string, + containerNumbers: string[], +): Promise { + const [booking]: Array<{ freightType: string | null; tons: string | null }> = + await dataSource.query( + `SELECT freight_type AS "freightType", cargo_total_weight_vgm AS "tons" + FROM freight.bookings + WHERE id = $1 AND deleted_at IS NULL`, + [bookingId], + ); + const sizes = await bookingContainerSizes( + dataSource, + bookingId, + containerNumbers.map((n) => n.trim().toUpperCase()), + ); + const bySize = new Map(); + for (const size of sizes) { + if (!size) continue; + bySize.set(size, (bySize.get(size) ?? 0) + 1); + } + return { + freightType: booking?.freightType ?? null, + tons: Number(booking?.tons ?? 0), + containers: [...bySize.entries()].map(([sizeLabel, qty]) => ({ sizeLabel, qty })), + }; +} diff --git a/apps/edr-freight-api/src/common/mile-distance.util.spec.ts b/apps/edr-freight-api/src/common/mile-distance.util.spec.ts new file mode 100644 index 000000000..75801a20b --- /dev/null +++ b/apps/edr-freight-api/src/common/mile-distance.util.spec.ts @@ -0,0 +1,17 @@ +import { haversineKm } from './mile-distance.util'; + +describe('haversineKm', () => { + it('is zero for the same point', () => { + expect(haversineKm(8.9, 38.6, 8.9, 38.6)).toBe(0); + }); + + it('matches one degree of longitude at the equator (~111.19 km)', () => { + expect(haversineKm(0, 0, 0, 1)).toBeCloseTo(111.19, 1); + }); + + it('Sebeta yard → Indode yard is roughly 26 km', () => { + const km = haversineKm(8.9096, 38.636, 8.7386, 38.7913); + expect(km).toBeGreaterThan(20); + expect(km).toBeLessThan(35); + }); +}); diff --git a/apps/edr-freight-api/src/common/mile-distance.util.ts b/apps/edr-freight-api/src/common/mile-distance.util.ts new file mode 100644 index 000000000..822a13350 --- /dev/null +++ b/apps/edr-freight-api/src/common/mile-distance.util.ts @@ -0,0 +1,48 @@ +import { DataSource } from 'typeorm'; + +/** Great-circle distance in km between two WGS84 points (haversine). */ +export function haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number { + const toRad = (d: number) => (d * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = + Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; + return 2 * 6371 * Math.asin(Math.sqrt(a)); +} + +/** + * Estimated road-leg distance for a booking's first/last mile: straight-line km + * from the yard (freight.yard_locations) to the customer's pickup/delivery GPS + * point on the booking. FIRST = origin yard → pickup point, LAST = destination + * yard → delivery point. Null when either end has no coordinates. + * + * ponytail: haversine straight-line, not road routing — plug a routing API in + * here if real road km is ever required. + */ +export async function estimateMileKm( + dataSource: DataSource, + bookingId: string, + mile: 'FIRST' | 'LAST', +): Promise { + const [row] = await dataSource.query( + mile === 'LAST' + ? `SELECT b.last_mile_delivery_lat AS lat, b.last_mile_delivery_lng AS lng, + l.latitude AS yard_lat, l.longitude AS yard_lng + FROM freight.bookings b + LEFT JOIN freight.yard_locations l + ON l.yard_id = b.destination_yard_id AND l.deleted_at IS NULL + WHERE b.id = $1 AND b.deleted_at IS NULL` + : `SELECT b.first_mile_pickup_lat AS lat, b.first_mile_pickup_lng AS lng, + l.latitude AS yard_lat, l.longitude AS yard_lng + FROM freight.bookings b + LEFT JOIN freight.yard_locations l + ON l.yard_id = b.origin_yard_id AND l.deleted_at IS NULL + WHERE b.id = $1 AND b.deleted_at IS NULL`, + [bookingId], + ); + if (!row || row.lat == null || row.lng == null || row.yard_lat == null || row.yard_lng == null) { + return null; + } + const km = haversineKm(Number(row.yard_lat), Number(row.yard_lng), Number(row.lat), Number(row.lng)); + return Math.round(km * 100) / 100; +} diff --git a/apps/edr-freight-api/src/config/app.config.ts b/apps/edr-freight-api/src/config/app.config.ts index 050b07145..8e03ac3f0 100644 --- a/apps/edr-freight-api/src/config/app.config.ts +++ b/apps/edr-freight-api/src/config/app.config.ts @@ -24,12 +24,13 @@ export default registerAs("app", () => ({ }, // Consumed by @edr/api-common ExchangeModule.forRootAsync (see bookings.module.ts). cbeExchange: { - /** ethio.forex CBET page — scraped for USD buying/selling rates. */ + /** CBE daily-exchange-rates JSON — USD `transactionalSelling` is used. */ scrapeUrl: process.env.CBE_EXCHANGE_SCRAPE_URL ?? process.env.CBE_EXCHANGE_API_URL ?? - "https://ethio.forex/bank/CBET", - fallbackRate: numberFromEnv("CBE_EXCHANGE_FALLBACK_RATE", 130), + "https://combanketh.et/cbeapi/daily-exchange-rates/?_limit=1&_sort=Date%3ADESC", + // No fallback env var: the fallback lives in freight.exchange_settings, + // maintained by the backoffice and by write-back on every successful fetch. cacheTtlMs: numberFromEnv("CBE_EXCHANGE_CACHE_TTL_MS", 3_600_000), }, })); diff --git a/apps/edr-freight-api/src/config/database.config.ts b/apps/edr-freight-api/src/config/database.config.ts index 0591cd0b1..5de529d15 100644 --- a/apps/edr-freight-api/src/config/database.config.ts +++ b/apps/edr-freight-api/src/config/database.config.ts @@ -56,6 +56,11 @@ import { EmployeePositionActivePeriod } from "@tria-plc/iamapi-common/entities/i import { UnitConfiguration } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-configuration.entity"; import { Site } from "@tria-plc/iamapi-common/entities/iam/site/site.entity"; import { SiteSetting } from "@tria-plc/iamapi-common/entities/iam/site/site-setting.entity"; +import { AuditLog, AuditLogCommand } from "@tria-plc/auditlog"; + +// @tria-plc/auditlog's entities live in node_modules, same as the iam ones — +// the glob below only matches this app's own src/**/*.entity.ts. +const auditEntities = [AuditLog, AuditLogCommand]; const iamEntities = [ UnitSetting, @@ -118,30 +123,95 @@ const iamMigrationsGlob = join( ); const freightMigrationsGlob = join(__dirname, "../migrations/*.js"); -export function buildDataSourceOptions(): DataSourceOptions { +/** + * Migration history is split per owner instead of sharing one `public.migrations` + * table: + * + * - IAM migrations ship with `@tria-plc/iamapi-common`, target the `iam` schema + * and are recorded in `iam.typeorm_migrations` — the same table the package's + * own CLI (`pnpm iam:migration:run|show|revert`) uses, so both paths agree on + * what has been applied. + * - Freight migrations are recorded in `freight.migrations`. + * + * `public.migrations` is the pre-split table; `src/scripts/migrate.ts` adopts its + * rows into the two tables above on first run and then leaves it untouched. + */ +export const IAM_MIGRATIONS = { + schema: "iam", + table: "typeorm_migrations", +} as const; + +export const FREIGHT_MIGRATIONS = { + schema: "freight", + table: "migrations", +} as const; + +export const LEGACY_MIGRATIONS = { + schema: "public", + table: "migrations", +} as const; + +function buildConnectionOptions() { return { - type: "postgres", + type: "postgres" as const, host: process.env.DB_HOST ?? "localhost", port: parseInt(process.env.DB_PORT ?? "5433", 10), username: process.env.DB_USER ?? "postgres", password: process.env.DB_PASSWORD ?? "", database: process.env.DB_NAME ?? "edr_freight", - schema: "public", // NOTE: do NOT pass `extra.options: '-c search_path=...'`. That sends the // Postgres startup `options` parameter, which connection poolers (PgBouncer / // proxies fronting the remote edr_dev DB) reject with // `08P01 unsupported startup parameter in options: search_path`. // The search_path is instead applied per-connection via a pool `connect` - // handler in app.module.ts (see setPoolSearchPath). - entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities], - migrations: [ - iamMigrationsGlob, - freightMigrationsGlob, - ], - migrationsTransactionMode: "each", + // handler (app.module.ts `setPoolSearchPath`, migrate.ts `applySearchPath`). synchronize: false, logging: - process.env.TYPEORM_LOGGING === "true" ? true : ["error", "warn"], + process.env.TYPEORM_LOGGING === "true" + ? true + : (["error", "warn"] as DataSourceOptions["logging"]), + }; +} + +/** + * Runtime options for the API (and the seed scripts using `AppDataSource`). + * Carries no migrations: migrations run only through `src/scripts/migrate.ts`, + * which uses the two dedicated DataSources below. + */ +export function buildDataSourceOptions(): DataSourceOptions { + return { + ...buildConnectionOptions(), + schema: "public", + entities: [ + __dirname + "/../**/*.entity.{ts,js}", + ...iamEntities, + ...auditEntities, + ], + migrations: [], + }; +} + +/** IAM migrations only, recorded in `iam.typeorm_migrations`. */ +export function buildIamMigrationDataSourceOptions(): DataSourceOptions { + return { + ...buildConnectionOptions(), + schema: IAM_MIGRATIONS.schema, + entities: [], + migrations: [iamMigrationsGlob], + migrationsTableName: IAM_MIGRATIONS.table, + migrationsTransactionMode: "each", + }; +} + +/** Freight migrations only, recorded in `freight.migrations`. */ +export function buildFreightMigrationDataSourceOptions(): DataSourceOptions { + return { + ...buildConnectionOptions(), + schema: FREIGHT_MIGRATIONS.schema, + entities: [], + migrations: [freightMigrationsGlob], + migrationsTableName: FREIGHT_MIGRATIONS.table, + migrationsTransactionMode: "each", }; } diff --git a/apps/edr-freight-api/src/config/ensure-postgres-schemas.ts b/apps/edr-freight-api/src/config/ensure-postgres-schemas.ts index 99fbcab61..0f359a942 100644 --- a/apps/edr-freight-api/src/config/ensure-postgres-schemas.ts +++ b/apps/edr-freight-api/src/config/ensure-postgres-schemas.ts @@ -10,6 +10,13 @@ export const APPLICATION_SCHEMAS = [ export const APPLICATION_SEARCH_PATH = APPLICATION_SCHEMAS.join(","); +/** + * Extensions the migrations call into but never create themselves — both the IAM + * package's migrations and the freight baseline default columns to + * `uuid_generate_v4()` / `gen_random_uuid()`. + */ +export const APPLICATION_EXTENSIONS = ["uuid-ossp", "pgcrypto"] as const; + /** * TypeORM creates the migrations table before any migration runs. If `public` was * dropped, current_schema() is null and CREATE TABLE migrations fails. @@ -42,6 +49,20 @@ export async function ensurePostgresSchemas( } } + // Best effort: creating an extension needs elevated rights the app user may not + // have. On an established database they are already installed and this is a + // no-op, so a failure here is only fatal for a brand-new database — where the + // first migration will fail loudly on the missing function anyway. + for (const extension of APPLICATION_EXTENSIONS) { + try { + await bootstrap.query(`CREATE EXTENSION IF NOT EXISTS "${extension}"`); + } catch (err) { + console.warn( + `could not ensure extension "${extension}": ${(err as Error).message}`, + ); + } + } + await bootstrap.query( `SET search_path TO ${APPLICATION_SEARCH_PATH}`, ); diff --git a/apps/edr-freight-api/src/contracts/contract-article.util.ts b/apps/edr-freight-api/src/contracts/contract-article.util.ts index 92bef9dd3..ee7978e08 100644 --- a/apps/edr-freight-api/src/contracts/contract-article.util.ts +++ b/apps/edr-freight-api/src/contracts/contract-article.util.ts @@ -3,7 +3,10 @@ import Handlebars from 'handlebars'; /** One numbered clause of a dynamic article, with optional nested bullets. */ export interface RenderedClause { text: string; - /** Computed outline number, e.g. "3" or "2.1.4". */ + /** + * Computed outline marker for this clause at its own level: "3" at depth 1, + * "b" at depth 2, "iv" at depth 3, cycling back to arabic at depth 4. + */ number: string; /** Nesting level: 1 = clause, 2 = sub-clause (x.y), 3 = x.y.z, … */ depth: number; @@ -35,6 +38,51 @@ const CLAUSE_NUMBER_RE = /^(?:(\d+(?:\.\d+)+)[.)]?|(\d+)[.)])(?:\s+|$)/; /** Deepest supported sub-clause level (1.1.1.1.1.1). */ const MAX_CLAUSE_DEPTH = 6; +/** 1 → "a", 2 → "b", … 27 → "aa". */ +function toAlpha(n: number): string { + let out = ''; + let value = n; + while (value > 0) { + const rem = (value - 1) % 26; + out = String.fromCharCode(97 + rem) + out; + value = Math.floor((value - 1) / 26); + } + return out || 'a'; +} + +const ROMAN: Array<[number, string]> = [ + [1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'], + [100, 'c'], [90, 'xc'], [50, 'l'], [40, 'xl'], + [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i'], +]; + +/** 1 → "i", 4 → "iv", 9 → "ix". */ +function toRoman(n: number): string { + let value = n; + let out = ''; + for (const [amount, numeral] of ROMAN) { + while (value >= amount) { + out += numeral; + value -= amount; + } + } + return out || 'i'; +} + +/** + * Word-processor outline markers, cycling by depth the way Quill's own list + * rendering does: 1. → a. → i. → 1. … Depth 1 keeps plain arabic numerals so + * top-level clauses read as "1.", "2." in the contract; the marker is the + * clause's own counter at its level, NOT a dotted path — "a" under clause 2 is + * "a", not "2.a". + */ +export function clauseMarker(counter: number, depth: number): string { + const style = (depth - 1) % 3; + if (style === 1) return toAlpha(counter); + if (style === 2) return toRoman(counter); + return String(counter); +} + /** * Parse a template article body into clauses. Format: one clause per line. * A leading outline number ("2. ", "2.1 ", "2.1.3 ") nests the line as a @@ -82,7 +130,7 @@ export function parseArticleBody(body: string): Pick s.containerSize ?? '').filter(Boolean)), + ].join(', '); + const cargoTypeName = [ + ...new Set( + scope + .map((s) => s.cargoType?.cargoTypeName ?? s.cargoFreeText ?? '') + .filter(Boolean), + ), + ].join(', '); + const cargoSummary = scope + .map((s) => { + const name = s.cargoType?.cargoTypeName ?? s.cargoFreeText ?? null; + const size = s.containerSize ? `(${s.containerSize})` : null; + const cap = s.quantityCap ? `× ${Number(s.quantityCap)}` : null; + return [name, size, cap].filter(Boolean).join(' '); + }) + .filter(Boolean) + .join('; '); + return { originLabel: this.yardLabel(firstRoute?.originYard), destinationLabel: this.yardLabel(firstRoute?.destinationYard), @@ -301,6 +327,9 @@ export class ContractDocumentViewModelBuilder { scheduledDate: this.formatDate(null), contractType: this.valueOrDash(contract.contractType), cargoDescription: this.valueOrDash(cargoName), + cargoTypeName: this.valueOrDash(cargoTypeName), + containerType: this.valueOrDash(containerType), + cargoSummary: this.valueOrDash(cargoSummary), totalWeightVgm: '—', equipmentReturn: this.valueOrDash(contract.equipmentReturn), // A hazardous contract names the declared class + UN number on the diff --git a/apps/edr-freight-api/src/contracts/contract-dynamic-template.spec.ts b/apps/edr-freight-api/src/contracts/contract-dynamic-template.spec.ts index 765c41142..6cefe6423 100644 --- a/apps/edr-freight-api/src/contracts/contract-dynamic-template.spec.ts +++ b/apps/edr-freight-api/src/contracts/contract-dynamic-template.spec.ts @@ -27,18 +27,32 @@ describe('parseArticleBody', () => { expect(parsed.clauses).toEqual([]); }); - it('nests numbered sub-clauses by their outline token and renumbers sequentially', () => { + it('nests sub-clauses by outline token and marks each level 1. → a. → i.', () => { const parsed = parseArticleBody( '1. Scope\n5.1 Rail transport\n1.1.1 Wagon supply\n2. Payment', ); expect(parsed.clauses.map((c) => [c.number, c.depth, c.text])).toEqual([ ['1', 1, 'Scope'], - ['1.1', 2, 'Rail transport'], - ['1.1.1', 3, 'Wagon supply'], + ['a', 2, 'Rail transport'], + ['i', 3, 'Wagon supply'], ['2', 1, 'Payment'], ]); }); + it('cycles markers back to arabic at depth 4 and counts each level on its own', () => { + const parsed = parseArticleBody( + '1. One\n1.1 Alpha\n1.2 Beta\n1.2.1 Roman one\n1.2.2 Roman two\n1.2.2.1 Deep', + ); + expect(parsed.clauses.map((c) => [c.number, c.depth])).toEqual([ + ['1', 1], + ['a', 2], + ['b', 2], + ['i', 3], + ['ii', 3], + ['1', 4], + ]); + }); + it('clamps a sub-clause with no open parent to the next available level', () => { const parsed = parseArticleBody('1.1.1 Orphan sub-clause\nSecond clause.'); expect(parsed.clauses.map((c) => [c.number, c.depth])).toEqual([ @@ -90,6 +104,8 @@ describe('dynamic template rendering (edr-dynamic.hbs)', () => { template: { ...meta, title: 'Bulk Import Contract', templateFile: 'edr-dynamic.hbs' }, contractDate: '1 January 2026', contractYear: 2026, + contractStartDate: '1 January 2026', + contractEndDate: '31 December 2026', client: { companyName: 'Abyssinia Trading PLC', companyAddress: 'Bole Sub-city, Addis Ababa', @@ -117,6 +133,9 @@ describe('dynamic template rendering (edr-dynamic.hbs)', () => { scheduledDate: '—', contractType: 'GENERAL', cargoDescription: 'Steel billets', + cargoTypeName: 'Steel billets', + containerType: '—', + cargoSummary: 'Steel billets × 2,800', totalWeightVgm: '—', equipmentReturn: '—', hazardousLabel: 'No', @@ -192,6 +211,42 @@ describe('dynamic template rendering (edr-dynamic.hbs)', () => { expect(html).toContain('#1b9e7a'); }); + it('shows the contract validity window in the commercial schedule annex', () => { + const html = renderer.render(dynamicView()); + expect(html).toContain('Valid from'); + expect(html).toContain('Valid until'); + expect(html).toContain('1 January 2026'); + expect(html).toContain('31 December 2026'); + }); + + it('interpolates the start/end date placeholders inside article text', () => { + const view = dynamicView(); + expect( + interpolateTemplateText( + 'In force {{contractStartDate}} to {{contractEndDate}}.', + view, + ), + ).toBe('In force 1 January 2026 to 31 December 2026.'); + }); + + it('shows cargo type and container type in the commercial schedule annex', () => { + const html = renderer.render(dynamicView()); + expect(html).toContain('Cargo type'); + expect(html).toContain('Container type'); + expect(html).toContain('Cargo scope'); + expect(html).toContain('Steel billets × 2,800'); + }); + + it('interpolates the cargo/container placeholders inside article text', () => { + const view = dynamicView(); + const body = + 'Cargo: {{schedule.cargoTypeName}} in {{schedule.containerType}} ' + + '({{schedule.freightType}}). Scope: {{schedule.cargoSummary}}.'; + expect(interpolateTemplateText(body, view)).toBe( + 'Cargo: Steel billets in — (BULK). Scope: Steel billets × 2,800.', + ); + }); + it('renders the live rate schedule lane under the pricing article', () => { const html = renderer.render(dynamicView()); expect(html).toContain('Rate Schedule'); diff --git a/apps/edr-freight-api/src/contracts/contract-rate-schedule.builder.ts b/apps/edr-freight-api/src/contracts/contract-rate-schedule.builder.ts index 48eadb6c7..c62a875bf 100644 --- a/apps/edr-freight-api/src/contracts/contract-rate-schedule.builder.ts +++ b/apps/edr-freight-api/src/contracts/contract-rate-schedule.builder.ts @@ -43,6 +43,7 @@ const UNIT_LABELS: Record = { PER_TON: 'per ton', PER_CONTAINER: 'per container', PER_KM: 'per km', + PER_TON_KM: 'per ton per km', PER_INVOICE: 'per invoice', FLAT: 'flat', }; diff --git a/apps/edr-freight-api/src/contracts/contract-renderer.service.spec.ts b/apps/edr-freight-api/src/contracts/contract-renderer.service.spec.ts index b0fc4c8ef..fc7e66e5b 100644 --- a/apps/edr-freight-api/src/contracts/contract-renderer.service.spec.ts +++ b/apps/edr-freight-api/src/contracts/contract-renderer.service.spec.ts @@ -16,6 +16,8 @@ describe('ContractRendererService', () => { template, contractDate: '1 January 2026', contractYear: 2026, + contractStartDate: '1 January 2026', + contractEndDate: '31 December 2026', client: { companyName: 'Test Co', companyAddress: 'Addis Ababa', @@ -43,6 +45,9 @@ describe('ContractRendererService', () => { scheduledDate: '1 January 2026', contractType: 'NEW', cargoDescription: 'Container cargo', + cargoTypeName: 'Coffee', + containerType: '40ft', + cargoSummary: 'Coffee (40ft) × 12', totalWeightVgm: '24 tons', equipmentReturn: 'RETURN', hazardousLabel: 'No', diff --git a/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts b/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts index 07f4d25d3..d1158d035 100644 --- a/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts +++ b/apps/edr-freight-api/src/contracts/contract-view-model.builder.ts @@ -41,6 +41,13 @@ export interface ContractViewModel { template: ContractTemplateMeta; contractDate: string; contractYear: number; + /** + * The contract's validity window (`contract_valid_from` / `_until`). Distinct + * from `contractDate`, which is the day the document is generated — these are + * the dates the contract is actually in force between. "—" when unset. + */ + contractStartDate: string; + contractEndDate: string; client: { companyName: string; companyAddress: string; @@ -68,6 +75,16 @@ export interface ContractViewModel { scheduledDate: string; contractType: string; cargoDescription: string; + /** + * The named cargo type on its own (e.g. "Coffee"), separate from + * `cargoDescription` which folds in free text and a container fallback. + * Lets a clause name the commodity without the surrounding prose. + */ + cargoTypeName: string; + /** Container size alone, e.g. "20ft" / "40ft"; "—" for bulk. */ + containerType: string; + /** Every cargo line on the contract, e.g. "Coffee (40ft) × 12". */ + cargoSummary: string; totalWeightVgm: string; equipmentReturn: string; hazardousLabel: string; @@ -133,6 +150,8 @@ export class ContractViewModelBuilder { year: 'numeric', }), contractYear: new Date().getFullYear(), + contractStartDate: this.formatDate(booking.contractValidFrom), + contractEndDate: this.formatDate(booking.contractValidUntil), client: { companyName: booking.company?.name ?? 'Client', companyAddress: this.valueOrDash(booking.company?.address), @@ -195,6 +214,21 @@ export class ContractViewModelBuilder { 'Bulk commodity' : booking.cargoType?.cargoTypeName || 'Container cargo'; const totalWeight = Number(booking.cargoTotalWeightVgm || 0); + // A booking may carry both sizes; name each one once, in the order booked. + const containerType = [ + ...new Set( + (booking.bookingContainers ?? []) + .map( + (line) => + line.containerType?.label ?? + (line.containerType?.sizeFt + ? `${line.containerType.sizeFt}ft` + : line.containerSize) ?? + '', + ) + .filter(Boolean), + ), + ].join(', '); return { originLabel: this.yardLabel(booking.originYard), @@ -207,6 +241,13 @@ export class ContractViewModelBuilder { scheduledDate: this.formatDate(booking.scheduledDate), contractType: this.valueOrDash(booking.contractType), cargoDescription: this.valueOrDash(cargoName), + cargoTypeName: this.valueOrDash(booking.cargoType?.cargoTypeName), + containerType: this.valueOrDash(containerType), + cargoSummary: this.valueOrDash( + [cargoName, containerType ? `(${containerType})` : null] + .filter(Boolean) + .join(' '), + ), totalWeightVgm: totalWeight > 0 ? `${totalWeight.toLocaleString()} tons` : '—', equipmentReturn: this.valueOrDash(booking.equipmentReturn), diff --git a/apps/edr-freight-api/src/contracts/templates/edr-dynamic.hbs b/apps/edr-freight-api/src/contracts/templates/edr-dynamic.hbs index 9c4957b2b..6ba7c1610 100644 --- a/apps/edr-freight-api/src/contracts/templates/edr-dynamic.hbs +++ b/apps/edr-freight-api/src/contracts/templates/edr-dynamic.hbs @@ -125,12 +125,30 @@ + + + + + + + + + + + + + + + + + +
Hazardous cargo {{schedule.hazardousLabel}}
Cargo type{{schedule.cargoTypeName}}Container type{{schedule.containerType}}
Cargo scope{{schedule.cargoSummary}}Freight type{{schedule.freightType}}
Equipment return {{schedule.equipmentReturn}} Payment currency {{paymentArticle}}
Valid from{{contractStartDate}}Valid until{{contractEndDate}}
diff --git a/apps/edr-freight-api/src/contracts/templates/last-mile.hbs b/apps/edr-freight-api/src/contracts/templates/last-mile.hbs new file mode 100644 index 000000000..9437bb18b --- /dev/null +++ b/apps/edr-freight-api/src/contracts/templates/last-mile.hbs @@ -0,0 +1,114 @@ + + + + + Last-Mile Delivery Contract — {{bookingReference}} + + + +
+
+
EDR
+
+

Ethio-Djibouti Standard Gauge Railway Share Company

+

Last-Mile Delivery Contract

+
+
+ +

Last-Mile Delivery Contract

+

Booking {{bookingReference}} — {{companyName}}

+ +

Shipment Details

+ + + + {{#if containerCount}} + + + {{/if}} + {{#if cargoDescription}} + + {{/if}} + {{#if deliveryAddress}} + + {{/if}} + {{#if trainDepartureDate}} + + {{/if}} + + + +
Client{{companyName}}
Booking Reference{{bookingReference}}
Number of Containers{{containerCount}}
Containers{{containerList}}
Cargo Description{{cargoDescription}}
Delivery Address{{deliveryAddress}}
Train Departure from Djibouti{{trainDepartureDate}}
Last-Mile Delivery Date{{deliveryDate}}
Request Date{{requestDate}}
Approval Date{{approvalDate}}
+ +

Rates

+ + + + + + {{#each rateLines}} + + {{/each}} + {{#if estimatedKm}} + + {{/if}} + + + + +
DescriptionAmount{{#if currency}} ({{currency}}){{/if}}
{{description}}{{amount}}
Estimated distance{{estimatedKm}} km
Advance payable on signing{{advanceAmount}}{{#if currency}} {{currency}}{{/if}}
+ +

Terms

+
+

1. The Service Provider shall deliver the goods identified above from the arrival yard to the Client's delivery address on or about the last-mile delivery date stated above.

+

2. The Client shall pay the advance stated above upon signing this contract. The final delivery fee is computed on completion per the Service Provider's published last-mile rates and actual distance.

+

3. The Client shall ensure access and receipt of the goods at the delivery address. Waiting time and truck detention beyond free time may incur additional charges per the applicable tariff.

+

4. This contract is governed by the laws applicable to the Ethio-Djibouti Standard Gauge Railway Share Company's freight services.

+
+ +

Signatures

+
+
+

Client

+ {{#if signature}} + Customer signature +
{{signature.signerDisplayName}} — signed {{signature.signedAt}}
+ {{#if signature.consentText}}{{/if}} + {{else}} +

Awaiting customer signature.

+
Name, signature & date
+ {{/if}} +
+
+

Service Provider

+

Ethio-Djibouti Standard Gauge Railway Share Company

+
Authorized representative
+
+
+
+ + diff --git a/apps/edr-freight-api/src/main.ts b/apps/edr-freight-api/src/main.ts index f13c596d2..c9a718f5d 100644 --- a/apps/edr-freight-api/src/main.ts +++ b/apps/edr-freight-api/src/main.ts @@ -10,6 +10,7 @@ import { ResponseTransformInterceptor, createValidationPipe, } from "@edr/api-common"; +import { getAuditLoggerConfig } from "@tria-plc/auditlog"; import { AppModule } from "./app.module"; @@ -19,7 +20,7 @@ import { AppModule } from "./app.module"; * ~13.4MB on the wire. Express defaults to 100kb, which rejected any real stamp * image with a 413 "request entity too large". */ -const JSON_BODY_LIMIT = '20mb'; +const JSON_BODY_LIMIT = "20mb"; /** * Static /etc/hosts-style overrides from `DNS_HOST_OVERRIDES`, formatted as @@ -44,7 +45,9 @@ function applyDnsHostOverrides(): void { } if (overrides.size === 0) return; - const dns = createRequire(__filename)("node:dns") as typeof import("node:dns"); + const dns = createRequire(__filename)( + "node:dns", + ) as typeof import("node:dns"); const originalLookup = dns.lookup.bind(dns); // `dns.lookup` is overloaded (options optional, all/family variants); the // cast keeps that surface intact while we intercept only mapped hostnames. @@ -63,7 +66,9 @@ function applyDnsHostOverrides(): void { ) => void; const family = ip.includes(":") ? 6 : 4; const wantsAll = - typeof options === "object" && options !== null && (options as { all?: boolean }).all; + typeof options === "object" && + options !== null && + (options as { all?: boolean }).all; process.nextTick(() => wantsAll ? done(null, [{ address: ip, family }]) : done(null, ip, family), @@ -77,7 +82,15 @@ function applyDnsHostOverrides(): void { applyDnsHostOverrides(); -async function bootstrap() { +/** + * Build the app with every global the production process applies, but do NOT + * listen. Exported so a test harness can boot the REAL app in its own process + * (integration/src/app.ts) and get the same prefix, pipe, filter, interceptor + * and body-parser configuration — replaying this list by hand is how an e2e + * harness silently drifts from production (routes 404 without the "api" + * prefix, responses lose the transform envelope). + */ +export async function createFreightApp(): Promise { const app = await NestFactory.create(AppModule); // Nest's own body-parser API, NOT `app.use(json(...))` from express: express @@ -86,14 +99,14 @@ async function bootstrap() { // pnpm's hoisted dev store and died as MODULE_NOT_FOUND in the production // image, where `pnpm deploy --prod` installs declared dependencies only. // This also RECONFIGURES the default parsers rather than racing them. - app.useBodyParser('json', { limit: JSON_BODY_LIMIT }); - app.useBodyParser('urlencoded', { limit: JSON_BODY_LIMIT, extended: true }); + app.useBodyParser("json", { limit: JSON_BODY_LIMIT }); + app.useBodyParser("urlencoded", { limit: JSON_BODY_LIMIT, extended: true }); // Dev CORS: reflect any localhost origin and allow credentials so the // freight portal (5173), passenger portal (5174), backoffices (5183/5184) // and any other dev port can call the API with cookies + Authorization. // For production, restrict `origin` to known FQDNs. - + app.enableCors({ origin: true, // reflect request origin credentials: true, @@ -130,8 +143,11 @@ async function bootstrap() { maxAge: 86400, // cache preflight for 24h to cut chatter in dev }); - // /callback stays un-prefixed: it's the Fayda OAuth redirect_uri ack endpoint. - app.setGlobalPrefix("api", { exclude: ["callback"] }); + // /fayda/callback stays un-prefixed: it's the Fayda OAuth redirect_uri ack + // endpoint. Exact path, not "fayda" — exclusion is an exact route match, so + // "fayda" would leave /fayda/callback prefixed (404 at the registered + // redirect_uri) while still reading as if it covered the whole subtree. + app.setGlobalPrefix("api", { exclude: ["fayda/callback"] }); // enableImplicitConversion is OFF: class-transformer's implicit boolean // coercion turns any non-empty multipart/form-data string (including the // literal "false") into `true`, silently corrupting flags like isHazardous @@ -145,6 +161,13 @@ async function bootstrap() { app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalInterceptors(new ResponseTransformInterceptor()); + // Audit listener: consumes the RMQ events MezgebModule's client interceptor + // (app.module.ts) emits and persists them via the AuditLogController / + // AuditLogCommandController @EventPattern handlers. Same queue config the + // client side uses, reused from the package so the two never drift apart. + app.connectMicroservice(getAuditLoggerConfig()); + await app.startAllMicroservices(); + const config = new DocumentBuilder() .setTitle("EDR Freight API") .setDescription("API for the EDR Freight Management application") @@ -154,13 +177,21 @@ async function bootstrap() { const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup("api/docs", app, document); + return app; +} + +async function bootstrap() { + const app = await createFreightApp(); const port = parseInt(process.env.PORT ?? "3001", 10); // await app.listen(port, "0.0.0.0"); - await app.listen( - - port) + await app.listen(port); // eslint-disable-next-line no-console console.log(`[freight-api] listening on port ${port}`); } -bootstrap(); +// Only self-start when this file IS the entrypoint. The Dockerfile's +// `CMD ["node", "dist/main.js"]` still boots; importers get `createFreightApp` +// without the process binding a port behind their back. +if (require.main === module) { + bootstrap(); +} diff --git a/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts b/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts deleted file mode 100644 index 08ce634eb..000000000 --- a/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from "typeorm"; - -export class AddServiceTypesAndCargoTypes1748427600000 implements MigrationInterface { - name = "AddServiceTypesAndCargoTypes1748427600000"; - - public async up(queryRunner: QueryRunner): Promise { - // Create service_types table - if (!(await queryRunner.hasTable("freight.service_types"))) await queryRunner.createTable( - new Table({ - name: "service_types", - schema: "freight", - columns: [ - { - name: "id", - type: "uuid", - isPrimary: true, - generationStrategy: "uuid", - default: "uuid_generate_v4()", - }, - { - name: "service_name", - type: "varchar", - length: "255", - isNullable: false, - }, - { - name: "description", - type: "text", - isNullable: true, - }, - { - name: "can_be_booked_alone", - type: "boolean", - default: true, - isNullable: false, - }, - { - name: "includes_first_mile", - type: "boolean", - default: false, - isNullable: false, - }, - { - name: "includes_last_mile", - type: "boolean", - default: false, - isNullable: false, - }, - { - name: "includes_customs", - type: "boolean", - default: false, - isNullable: false, - }, - { - name: "priority_bonus_points", - type: "int", - default: 0, - isNullable: false, - }, - { - name: "is_active", - type: "boolean", - default: true, - isNullable: false, - }, - { - name: "display_order", - type: "int", - default: 1, - isNullable: false, - }, - { - name: "created_at", - type: "timestamptz", - default: "now()", - isNullable: false, - }, - { - name: "updated_at", - type: "timestamptz", - default: "now()", - isNullable: false, - }, - { - name: "deleted_at", - type: "timestamptz", - isNullable: true, - }, - ], - }), - true, - ); - - // Create indexes for service_types - const table = await queryRunner.getTable("freight.service_types"); - if (table && !table.indices.some((idx) => idx.name === "IDX_SERVICE_TYPES_IS_ACTIVE")) { - await queryRunner.createIndex( - "freight.service_types", - new TableIndex({ - name: "IDX_SERVICE_TYPES_IS_ACTIVE", - columnNames: ["is_active"], - }), - ); - } - if (table && !table.indices.some((idx) => idx.name === "IDX_SERVICE_TYPES_DISPLAY_ORDER")) { - await queryRunner.createIndex( - "freight.service_types", - new TableIndex({ - name: "IDX_SERVICE_TYPES_DISPLAY_ORDER", - columnNames: ["display_order"], - }), - ); - } - - // Create cargo_types table - if (!(await queryRunner.hasTable("freight.cargo_types"))) await queryRunner.createTable( - new Table({ - name: "cargo_types", - schema: "freight", - columns: [ - { - name: "id", - type: "uuid", - isPrimary: true, - generationStrategy: "uuid", - default: "uuid_generate_v4()", - }, - { - name: "cargo_type_name", - type: "varchar", - length: "255", - isNullable: false, - }, - { - name: "parent_group_id", - type: "uuid", - isNullable: true, - }, - { - name: "show_free_text_box", - type: "boolean", - default: false, - isNullable: false, - }, - { - name: "requires_director_approval", - type: "boolean", - default: false, - isNullable: false, - }, - { - name: "is_active", - type: "boolean", - default: true, - isNullable: false, - }, - { - name: "display_order", - type: "int", - default: 1, - isNullable: false, - }, - { - name: "created_at", - type: "timestamptz", - default: "now()", - isNullable: false, - }, - { - name: "updated_at", - type: "timestamptz", - default: "now()", - isNullable: false, - }, - { - name: "deleted_at", - type: "timestamptz", - isNullable: true, - }, - ], - }), - true, - ); - - // Create indexes for cargo_types - await queryRunner.createIndex( - "freight.cargo_types", - new TableIndex({ - name: "IDX_CARGO_TYPES_IS_ACTIVE", - columnNames: ["is_active"], - }), - ); - await queryRunner.createIndex( - "freight.cargo_types", - new TableIndex({ - name: "IDX_CARGO_TYPES_DISPLAY_ORDER", - columnNames: ["display_order"], - }), - ); - await queryRunner.createIndex( - "freight.cargo_types", - new TableIndex({ - name: "IDX_CARGO_TYPES_PARENT_GROUP_ID", - columnNames: ["parent_group_id"], - }), - ); - - // Create self-referencing foreign key for cargo_types - await queryRunner.createForeignKey( - "freight.cargo_types", - new TableForeignKey({ - name: "FK_CARGO_TYPES_PARENT_GROUP", - columnNames: ["parent_group_id"], - referencedSchema: "freight", - referencedTableName: "cargo_types", - referencedColumnNames: ["id"], - onDelete: "SET NULL", - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - // Drop foreign key first - await queryRunner.dropForeignKey("freight.cargo_types", "FK_CARGO_TYPES_PARENT_GROUP"); - - // Drop cargo_types table - await queryRunner.dropTable("freight.cargo_types", true); - - // Drop service_types table - await queryRunner.dropTable("freight.service_types", true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1748514000000-AddRuleEngineTablesAndCodes.ts b/apps/edr-freight-api/src/migrations/1748514000000-AddRuleEngineTablesAndCodes.ts deleted file mode 100644 index e1a9f6aeb..000000000 --- a/apps/edr-freight-api/src/migrations/1748514000000-AddRuleEngineTablesAndCodes.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { - MigrationInterface, - QueryRunner, - Table, - TableIndex, - TableForeignKey, - TableColumn, -} from 'typeorm'; - -export class AddRuleEngineTablesAndCodes1748514000000 implements MigrationInterface { - name = 'AddRuleEngineTablesAndCodes1748514000000'; - - public async up(queryRunner: QueryRunner): Promise { - // ── 1. Add `code` column to existing tables ─────────────────────────── - - if (!(await queryRunner.hasColumn('freight.service_types', 'code'))) { - await queryRunner.addColumn( - 'freight.service_types', - new TableColumn({ - name: 'code', - type: 'varchar', - length: '50', - isNullable: true, - }), - ); - } - await queryRunner.query( - `UPDATE freight.service_types SET code = upper(replace(service_name, ' ', '_')) WHERE code IS NULL OR code = ''`, - ); - await queryRunner.query( - `UPDATE freight.service_types SET code = 'SERVICE_' || substring(id::text, 1, 8) WHERE code IS NULL`, - ); - await queryRunner.query( - `ALTER TABLE freight.service_types ALTER COLUMN code SET NOT NULL`, - ); - const serviceTypesCodeIdx = await queryRunner.query( - `SELECT 1 FROM pg_indexes WHERE schemaname = 'freight' AND indexname = 'idx_service_types_code' LIMIT 1`, - ); - if (serviceTypesCodeIdx.length === 0) { - await queryRunner.createIndex( - 'freight.service_types', - new TableIndex({ name: 'IDX_service_types_code', columnNames: ['code'], isUnique: true }), - ); - } - - if (!(await queryRunner.hasColumn('freight.cargo_types', 'code'))) { - await queryRunner.addColumn( - 'freight.cargo_types', - new TableColumn({ - name: 'code', - type: 'varchar', - length: '50', - isNullable: true, - }), - ); - } - await queryRunner.query( - `UPDATE freight.cargo_types SET code = upper(replace(cargo_type_name, ' ', '_')) WHERE code IS NULL OR code = ''`, - ); - await queryRunner.query( - `UPDATE freight.cargo_types SET code = 'CARGO_' || substring(id::text, 1, 8) WHERE code IS NULL`, - ); - await queryRunner.query( - `ALTER TABLE freight.cargo_types ALTER COLUMN code SET NOT NULL`, - ); - const cargoTypesCodeIdx = await queryRunner.query( - `SELECT 1 FROM pg_indexes WHERE schemaname = 'freight' AND indexname = 'idx_cargo_types_code' LIMIT 1`, - ); - if (cargoTypesCodeIdx.length === 0) { - await queryRunner.createIndex( - 'freight.cargo_types', - new TableIndex({ name: 'IDX_cargo_types_code', columnNames: ['code'], isUnique: true }), - ); - } - - // ── 2. surcharge_types ──────────────────────────────────────────────── - - if (!(await queryRunner.hasTable('freight.surcharge_types'))) await queryRunner.createTable( - new Table({ - name: 'surcharge_types', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'code', type: 'varchar', length: '50', isNullable: false }, - { name: 'name', type: 'varchar', length: '100', isNullable: false }, - { name: 'description', type: 'text', isNullable: true }, - { name: 'is_active', type: 'boolean', default: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createIndex( - 'freight.surcharge_types', - new TableIndex({ name: 'IDX_surcharge_types_code', columnNames: ['code'], isUnique: true }), - ); - await queryRunner.createIndex( - 'freight.surcharge_types', - new TableIndex({ name: 'IDX_surcharge_types_is_active', columnNames: ['is_active'] }), - ); - - // ── 3. surcharges ───────────────────────────────────────────────────── - - if (!(await queryRunner.hasTable('freight.surcharges'))) await queryRunner.createTable( - new Table({ - name: 'surcharges', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'surcharge_type_id', type: 'uuid', isNullable: false }, - { name: 'fee_name', type: 'varchar', length: '255', isNullable: false }, - { name: 'trigger_description', type: 'text', isNullable: true }, - { - name: 'calculation_method', - type: 'enum', - enum: ['PER_TON', 'FLAT_FEE', 'PERCENTAGE'], - default: `'PER_TON'`, - }, - { name: 'rate', type: 'numeric', precision: 10, scale: 2, isNullable: false }, - { name: 'currency', type: 'char', length: '3', default: `'USD'` }, - { name: 'apply_to_rail', type: 'boolean', default: false }, - { name: 'apply_to_first_mile', type: 'boolean', default: false }, - { name: 'apply_to_last_mile', type: 'boolean', default: false }, - { name: 'is_active', type: 'boolean', default: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createForeignKey( - 'freight.surcharges', - new TableForeignKey({ - name: 'FK_surcharges_surcharge_type', - columnNames: ['surcharge_type_id'], - referencedTableName: 'freight.surcharge_types', - referencedColumnNames: ['id'], - onDelete: 'RESTRICT', - }), - ); - await queryRunner.createIndex( - 'freight.surcharges', - new TableIndex({ name: 'IDX_surcharges_surcharge_type_id', columnNames: ['surcharge_type_id'] }), - ); - await queryRunner.createIndex( - 'freight.surcharges', - new TableIndex({ name: 'IDX_surcharges_is_active', columnNames: ['is_active'] }), - ); - - // ── 4. container_types ──────────────────────────────────────────────── - - if (!(await queryRunner.hasTable('freight.container_types'))) await queryRunner.createTable( - new Table({ - name: 'container_types', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'size_code', type: 'varchar', length: '20', isNullable: false }, - { name: 'description', type: 'varchar', length: '100', isNullable: true }, - { name: 'containers_per_wagon', type: 'int', isNullable: false }, - { name: 'is_active', type: 'boolean', default: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createIndex( - 'freight.container_types', - new TableIndex({ name: 'IDX_container_types_size_code', columnNames: ['size_code'], isUnique: true }), - ); - await queryRunner.createIndex( - 'freight.container_types', - new TableIndex({ name: 'IDX_container_types_is_active', columnNames: ['is_active'] }), - ); - - // ── 5. weight_limit_rules ───────────────────────────────────────────── - - if (!(await queryRunner.hasTable('freight.weight_limit_rules'))) await queryRunner.createTable( - new Table({ - name: 'weight_limit_rules', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'container_type_id', type: 'uuid', isNullable: false }, - { - name: 'trade_direction', - type: 'enum', - enum: ['IMPORT', 'EXPORT', 'BOTH'], - isNullable: false, - }, - { name: 'max_weight_tons', type: 'numeric', precision: 10, scale: 2, isNullable: false }, - { name: 'warning_threshold_tons', type: 'numeric', precision: 10, scale: 2, isNullable: false }, - { - name: 'exceeded_action', - type: 'enum', - enum: ['WARNING_ONLY', 'HARD_BLOCK'], - default: `'WARNING_ONLY'`, - }, - { name: 'surcharge_id', type: 'uuid', isNullable: true }, - { name: 'is_active', type: 'boolean', default: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createForeignKey( - 'freight.weight_limit_rules', - new TableForeignKey({ - name: 'FK_weight_limit_rules_container_type', - columnNames: ['container_type_id'], - referencedTableName: 'freight.container_types', - referencedColumnNames: ['id'], - onDelete: 'RESTRICT', - }), - ); - await queryRunner.createForeignKey( - 'freight.weight_limit_rules', - new TableForeignKey({ - name: 'FK_weight_limit_rules_surcharge', - columnNames: ['surcharge_id'], - referencedTableName: 'freight.surcharges', - referencedColumnNames: ['id'], - onDelete: 'SET NULL', - }), - ); - await queryRunner.createIndex( - 'freight.weight_limit_rules', - new TableIndex({ name: 'IDX_weight_limit_rules_container_type_id', columnNames: ['container_type_id'] }), - ); - await queryRunner.createIndex( - 'freight.weight_limit_rules', - new TableIndex({ name: 'IDX_weight_limit_rules_surcharge_id', columnNames: ['surcharge_id'] }), - ); - await queryRunner.createIndex( - 'freight.weight_limit_rules', - new TableIndex({ name: 'IDX_weight_limit_rules_is_active', columnNames: ['is_active'] }), - ); - - // ── 6. priority_rules ───────────────────────────────────────────────── - - if (!(await queryRunner.hasTable('freight.priority_rules'))) await queryRunner.createTable( - new Table({ - name: 'priority_rules', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { - name: 'priority_type', - type: 'enum', - enum: ['USD_PAYER', 'RAIL_AND_FORWARDING', 'GOVERNMENT_ACCOUNT', 'HIGH_VOLUME_SHIPMENT'], - isNullable: false, - }, - { name: 'rule_name', type: 'varchar', length: '255', isNullable: false }, - { name: 'description', type: 'text', isNullable: true }, - { name: 'activation_condition', type: 'text', isNullable: true }, - { name: 'bonus_points', type: 'int', default: 0 }, - { name: 'is_active', type: 'boolean', default: false }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createIndex( - 'freight.priority_rules', - new TableIndex({ name: 'IDX_priority_rules_priority_type', columnNames: ['priority_type'], isUnique: true }), - ); - await queryRunner.createIndex( - 'freight.priority_rules', - new TableIndex({ name: 'IDX_priority_rules_is_active', columnNames: ['is_active'] }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.priority_rules', true); - await queryRunner.dropTable('freight.weight_limit_rules', true); - await queryRunner.dropTable('freight.container_types', true); - await queryRunner.dropTable('freight.surcharges', true); - await queryRunner.dropTable('freight.surcharge_types', true); - await queryRunner.dropIndex('freight.cargo_types', 'IDX_cargo_types_code'); - await queryRunner.dropColumn('freight.cargo_types', 'code'); - await queryRunner.dropIndex('freight.service_types', 'IDX_service_types_code'); - await queryRunner.dropColumn('freight.service_types', 'code'); - } -} diff --git a/apps/edr-freight-api/src/migrations/1748550000000-CreateFreightLegacyBaseline.ts b/apps/edr-freight-api/src/migrations/1748550000000-CreateFreightLegacyBaseline.ts deleted file mode 100644 index 2455727bf..000000000 --- a/apps/edr-freight-api/src/migrations/1748550000000-CreateFreightLegacyBaseline.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Pre-ITMLS baseline for fresh databases. Older environments created `freight.bookings` - * via synchronize or manual SQL; ItmlsFullSchemaRewrite only ALTERs that table. - */ -export class CreateFreightLegacyBaseline1748550000000 implements MigrationInterface { - name = 'CreateFreightLegacyBaseline1748550000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS freight`); - - await queryRunner.query(` - DO $$ BEGIN - CREATE TYPE freight.train_status AS ENUM ( - 'AVAILABLE', 'SCHEDULED', 'IN_SERVICE', 'UNDER_MAINTENANCE', 'OUT_OF_SERVICE' - ); - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.trains ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - code VARCHAR(32) NOT NULL UNIQUE, - capacity_tons NUMERIC(10, 2) NOT NULL DEFAULT 0, - status freight.train_status NOT NULL DEFAULT 'AVAILABLE', - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.bookings ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - reference VARCHAR(64) NOT NULL UNIQUE, - customer_id UUID NOT NULL, - train_id UUID, - status VARCHAR(40) NOT NULL DEFAULT 'DRAFT', - scheduled_date TIMESTAMPTZ NOT NULL DEFAULT now(), - total_amount NUMERIC(14, 2) NOT NULL DEFAULT 0, - payment_status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - contract_type VARCHAR(20) NOT NULL DEFAULT 'SPOT', - previous_contract_id UUID, - trade_direction VARCHAR(10) NOT NULL DEFAULT 'IMPORT', - equipment_return VARCHAR(20) NOT NULL DEFAULT 'RETURN', - first_mile_pickup_address TEXT, - last_mile_delivery_address TEXT, - cargo_total_weight_vgm NUMERIC(12, 3) NOT NULL DEFAULT 0, - is_hazardous BOOLEAN NOT NULL DEFAULT false, - payment_currency VARCHAR(5) NOT NULL DEFAULT 'USD', - start_date DATE, - end_date DATE, - financial_terms TEXT, - version_number INT NOT NULL DEFAULT 1, - approved_by_staff_id UUID, - approved_by_staff_at TIMESTAMPTZ, - signed_by_director_id UUID, - signed_by_director_at TIMESTAMPTZ, - signed_by_ceo_id UUID, - signed_by_ceo_at TIMESTAMPTZ, - priority_score INT NOT NULL DEFAULT 0, - allow_consolidation BOOLEAN NOT NULL DEFAULT false, - consolidation_partner_id UUID, - origin_station VARCHAR(255), - destination_station VARCHAR(255), - service_type VARCHAR(100), - freight_type VARCHAR(100), - freight_subtype VARCHAR(255), - containers JSONB, - first_mile_enabled BOOLEAN DEFAULT false, - last_mile_enabled BOOLEAN DEFAULT false, - is_refrigerated BOOLEAN DEFAULT false, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.bookings CASCADE`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.trains CASCADE`); - await queryRunner.query(`DROP TYPE IF EXISTS freight.train_status`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1748600000000-ItmlsFullSchemaRewrite.ts b/apps/edr-freight-api/src/migrations/1748600000000-ItmlsFullSchemaRewrite.ts deleted file mode 100644 index ce5b38c15..000000000 --- a/apps/edr-freight-api/src/migrations/1748600000000-ItmlsFullSchemaRewrite.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { - MigrationInterface, - QueryRunner, - Table, - TableForeignKey, - TableIndex, - TableUnique, -} from 'typeorm'; - -export class ItmlsFullSchemaRewrite1748600000000 implements MigrationInterface { - name = 'ItmlsFullSchemaRewrite1748600000000'; - - public async up(queryRunner: QueryRunner): Promise { - // ── container_types ─────────────────────────────────────────────────── - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.container_types RENAME COLUMN size_code TO code; - EXCEPTION WHEN undefined_column THEN NULL; - END $$; - `); - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.container_types RENAME COLUMN description TO label; - EXCEPTION WHEN undefined_column THEN NULL; - END $$; - `); - await queryRunner.query(` - ALTER TABLE freight.container_types - ADD COLUMN IF NOT EXISTS size_ft SMALLINT, - ADD COLUMN IF NOT EXISTS is_reefer BOOLEAN NOT NULL DEFAULT false, - ADD COLUMN IF NOT EXISTS is_open_top BOOLEAN NOT NULL DEFAULT false, - ADD COLUMN IF NOT EXISTS display_order INT NOT NULL DEFAULT 1; - `); - await queryRunner.query(` - ALTER TABLE freight.container_types - ADD COLUMN IF NOT EXISTS wagons_per_unit NUMERIC(4,2); - `); - await queryRunner.query(` - UPDATE freight.container_types - SET wagons_per_unit = CASE - WHEN containers_per_wagon > 0 THEN ROUND(1.0 / containers_per_wagon, 2) - ELSE 1.00 - END - WHERE wagons_per_unit IS NULL; - `); - await queryRunner.query(` - UPDATE freight.container_types - SET size_ft = CASE WHEN code LIKE '40%' OR code LIKE '%40%' THEN 40 ELSE 20 END - WHERE size_ft IS NULL; - `); - await queryRunner.query(` - ALTER TABLE freight.container_types - ALTER COLUMN wagons_per_unit SET NOT NULL, - DROP COLUMN IF EXISTS containers_per_wagon; - `); - - // ── weight_limit_rules ────────────────────────────────────────────────── - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.weight_limit_rules RENAME COLUMN max_weight_tons TO max_vgm_tons; - EXCEPTION WHEN undefined_column THEN NULL; - END $$; - `); - await queryRunner.query(` - ALTER TABLE freight.weight_limit_rules - ALTER COLUMN max_vgm_tons TYPE NUMERIC(8,3); - `); - await queryRunner.query(` - ALTER TABLE freight.weight_limit_rules - ADD COLUMN IF NOT EXISTS effective_from DATE NOT NULL DEFAULT CURRENT_DATE, - ADD COLUMN IF NOT EXISTS effective_to DATE; - `); - await queryRunner.query(` - ALTER TABLE freight.weight_limit_rules - DROP COLUMN IF EXISTS warning_threshold_tons, - DROP COLUMN IF EXISTS exceeded_action, - DROP COLUMN IF EXISTS surcharge_id, - DROP COLUMN IF EXISTS is_active; - `); - - // ── priority_rules ──────────────────────────────────────────────────── - await queryRunner.query(` - ALTER TABLE freight.priority_rules - ADD COLUMN IF NOT EXISTS code VARCHAR(40), - ADD COLUMN IF NOT EXISTS label VARCHAR(100), - ADD COLUMN IF NOT EXISTS score INT NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS condition_currency VARCHAR(5); - `); - await queryRunner.query(` - UPDATE freight.priority_rules - SET code = COALESCE(code, upper(priority_type::text)), - label = COALESCE(label, rule_name), - score = COALESCE(score, bonus_points) - WHERE code IS NULL OR label IS NULL; - `); - await queryRunner.query(` - ALTER TABLE freight.priority_rules - DROP COLUMN IF EXISTS priority_type, - DROP COLUMN IF EXISTS rule_name, - DROP COLUMN IF EXISTS bonus_points, - DROP COLUMN IF EXISTS activation_condition, - DROP COLUMN IF EXISTS description; - `); - await queryRunner.query(` - ALTER TABLE freight.priority_rules - ALTER COLUMN code SET NOT NULL, - ALTER COLUMN label SET NOT NULL; - `); - await queryRunner.createIndex( - 'freight.priority_rules', - new TableIndex({ name: 'UQ_priority_rules_code', columnNames: ['code'], isUnique: true }), - ); - - // ── rates (before surcharge_types.rate_id) ──────────────────────────── - await queryRunner.createTable( - new Table({ - name: 'rates', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'rate_type', type: 'varchar', length: '50' }, - { name: 'container_type_id', type: 'uuid', isNullable: true }, - { name: 'trade_direction', type: 'varchar', length: '10', isNullable: true }, - { name: 'currency', type: 'varchar', length: '5' }, - { name: 'rate_value', type: 'numeric', precision: 14, scale: 4 }, - { name: 'rate_unit', type: 'varchar', length: '30' }, - { name: 'status', type: 'varchar', length: '20', default: "'DRAFT'" }, - { name: 'proposed_by_staff_id', type: 'uuid' }, - { name: 'approved_by_ceo_id', type: 'uuid', isNullable: true }, - { name: 'approved_at', type: 'timestamptz', isNullable: true }, - { name: 'effective_from', type: 'date' }, - { name: 'effective_to', type: 'date', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - // ── surcharge_types ─────────────────────────────────────────────────── - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.surcharge_types RENAME COLUMN name TO label; - EXCEPTION WHEN undefined_column THEN NULL; - END $$; - `); - await queryRunner.query(` - ALTER TABLE freight.surcharge_types - ADD COLUMN IF NOT EXISTS trigger_condition VARCHAR(50), - ADD COLUMN IF NOT EXISTS rate_id UUID; - `); - await queryRunner.query(`ALTER TABLE freight.surcharge_types DROP COLUMN IF EXISTS description`); - - await queryRunner.query(`DROP TABLE IF EXISTS freight.surcharges CASCADE`); - - // ── yards ───────────────────────────────────────────────────────────── - await queryRunner.createTable( - new Table({ - name: 'yards', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'code', type: 'varchar', length: '20' }, - { name: 'label', type: 'varchar', length: '100' }, - { name: 'country', type: 'varchar', length: '50' }, - { name: 'is_active', type: 'boolean', default: true }, - { name: 'display_order', type: 'int', default: 1 }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createIndex( - 'freight.yards', - new TableIndex({ name: 'UQ_yards_code', columnNames: ['code'], isUnique: true }), - ); - - // ── shipping_lines ──────────────────────────────────────────────────── - await queryRunner.createTable( - new Table({ - name: 'shipping_lines', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'code', type: 'varchar', length: '20' }, - { name: 'label', type: 'varchar', length: '100' }, - { name: 'mapped_to_code', type: 'varchar', length: '20', isNullable: true }, - { name: 'show_extra_fee_notice', type: 'boolean', default: false }, - { name: 'is_active', type: 'boolean', default: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - // ── approval_rules ──────────────────────────────────────────────────── - await queryRunner.createTable( - new Table({ - name: 'approval_rules', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'requires_director_approval', type: 'boolean' }, - { name: 'step_order', type: 'smallint' }, - { name: 'required_role', type: 'varchar', length: '30' }, - { name: 'action_label', type: 'varchar', length: '50' }, - { name: 'blocks_role', type: 'varchar', length: '30', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createUniqueConstraint( - 'freight.approval_rules', - new TableUnique({ - name: 'UQ_approval_rules_chain_step', - columnNames: ['requires_director_approval', 'step_order'], - }), - ); - - // ── bookings ──────────────────────────────────────────────────────── - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS origin_yard_id UUID, - ADD COLUMN IF NOT EXISTS destination_yard_id UUID, - ADD COLUMN IF NOT EXISTS service_type_id UUID, - ADD COLUMN IF NOT EXISTS cargo_type_id UUID, - ADD COLUMN IF NOT EXISTS cargo_free_text VARCHAR(200), - ADD COLUMN IF NOT EXISTS shipping_line_id UUID, - ADD COLUMN IF NOT EXISTS pnr_code VARCHAR(50), - ADD COLUMN IF NOT EXISTS customer_signed_at TIMESTAMPTZ, - ADD COLUMN IF NOT EXISTS fully_executed_at TIMESTAMPTZ; - `); - - await queryRunner.query(` - INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at) - VALUES - (uuid_generate_v4(), 'KALITY', 'Kality Rail Terminal', 'Ethiopia', true, 1, now(), now()), - (uuid_generate_v4(), 'MOJO', 'Mojo Dry Port', 'Ethiopia', true, 2, now(), now()), - (uuid_generate_v4(), 'DIRE_DAWA', 'Dire Dawa Yard', 'Ethiopia', true, 3, now(), now()), - (uuid_generate_v4(), 'DJIB_PORT', 'Djibouti Port Terminal', 'Djibouti', true, 4, now(), now()), - (uuid_generate_v4(), 'NAGAD', 'Nagad Terminal, Djibouti', 'Djibouti', true, 5, now(), now()), - (uuid_generate_v4(), 'LEGACY_ORIGIN', 'Legacy Origin', 'Ethiopia', true, 99, now(), now()), - (uuid_generate_v4(), 'LEGACY_DEST', 'Legacy Destination', 'Ethiopia', true, 100, now(), now()) - ON CONFLICT (code) DO NOTHING; - `); - - await queryRunner.query(` - INSERT INTO freight.service_types (id, code, service_name, can_be_booked_alone, includes_first_mile, includes_last_mile, includes_customs, priority_bonus_points, is_active, display_order, created_at, updated_at) - SELECT uuid_generate_v4(), 'RAIL', 'Rail Transport Only', true, false, false, false, 0, true, 1, now(), now() - WHERE NOT EXISTS (SELECT 1 FROM freight.service_types LIMIT 1); - `); - await queryRunner.query(` - INSERT INTO freight.cargo_types (id, code, cargo_type_name, requires_director_approval, show_free_text_box, is_active, display_order, created_at, updated_at) - SELECT uuid_generate_v4(), 'GENERAL', 'General Cargo', false, false, true, 1, now(), now() - WHERE NOT EXISTS (SELECT 1 FROM freight.cargo_types LIMIT 1); - `); - - const hasServiceTypeCol = await queryRunner.query(` - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'freight' AND table_name = 'bookings' AND column_name = 'service_type' - LIMIT 1; - `); - - if (hasServiceTypeCol.length > 0) { - await queryRunner.query(` - UPDATE freight.bookings b - SET service_type_id = st.id - FROM freight.service_types st - WHERE b.service_type_id IS NULL - AND ( - st.code = b.service_type - OR upper(replace(st.service_name, ' ', '_')) = upper(b.service_type) - OR st.code = upper(replace(b.service_type, ' ', '_')) - ); - `); - await queryRunner.query(` - UPDATE freight.bookings b - SET cargo_type_id = ct.id - FROM freight.cargo_types ct - WHERE b.cargo_type_id IS NULL - AND ( - ct.code = upper(b.freight_type) - OR ct.code = upper(concat(b.freight_type, '_', coalesce(b.freight_subtype, ''))) - ); - `); - await queryRunner.query(` - UPDATE freight.bookings b - SET cargo_free_text = b.freight_subtype - WHERE b.cargo_free_text IS NULL AND b.freight_subtype IS NOT NULL; - `); - await queryRunner.query(` - UPDATE freight.bookings b - SET origin_yard_id = y.id - FROM freight.yards y - WHERE b.origin_yard_id IS NULL - AND (y.label ILIKE b.origin_station OR y.code = upper(replace(b.origin_station, ' ', '_'))); - `); - await queryRunner.query(` - UPDATE freight.bookings b - SET destination_yard_id = y.id - FROM freight.yards y - WHERE b.destination_yard_id IS NULL - AND (y.label ILIKE b.destination_station OR y.code = upper(replace(b.destination_station, ' ', '_'))); - `); - } - - const defaultServiceTypeId = await queryRunner.query( - `SELECT id FROM freight.service_types ORDER BY display_order LIMIT 1`, - ); - const defaultCargoTypeId = await queryRunner.query( - `SELECT id FROM freight.cargo_types ORDER BY display_order LIMIT 1`, - ); - const legacyOriginId = await queryRunner.query( - `SELECT id FROM freight.yards WHERE code = 'LEGACY_ORIGIN' LIMIT 1`, - ); - const legacyDestId = await queryRunner.query( - `SELECT id FROM freight.yards WHERE code = 'LEGACY_DEST' LIMIT 1`, - ); - - if (defaultServiceTypeId[0]?.id) { - await queryRunner.query( - `UPDATE freight.bookings SET service_type_id = $1 WHERE service_type_id IS NULL`, - [defaultServiceTypeId[0].id], - ); - } - if (defaultCargoTypeId[0]?.id) { - await queryRunner.query( - `UPDATE freight.bookings SET cargo_type_id = $1 WHERE cargo_type_id IS NULL`, - [defaultCargoTypeId[0].id], - ); - } - if (legacyOriginId[0]?.id) { - await queryRunner.query( - `UPDATE freight.bookings SET origin_yard_id = $1 WHERE origin_yard_id IS NULL`, - [legacyOriginId[0].id], - ); - } - if (legacyDestId[0]?.id) { - await queryRunner.query( - `UPDATE freight.bookings SET destination_yard_id = $1 WHERE destination_yard_id IS NULL`, - [legacyDestId[0].id], - ); - } - - const nullBookings = await queryRunner.query( - `SELECT COUNT(*)::int AS cnt FROM freight.bookings WHERE service_type_id IS NULL OR cargo_type_id IS NULL OR origin_yard_id IS NULL OR destination_yard_id IS NULL`, - ); - if (nullBookings[0]?.cnt > 0) { - await queryRunner.query(`DELETE FROM freight.bookings WHERE service_type_id IS NULL OR cargo_type_id IS NULL OR origin_yard_id IS NULL OR destination_yard_id IS NULL`); - } - - await queryRunner.query(` - ALTER TABLE freight.bookings - ALTER COLUMN service_type_id SET NOT NULL, - ALTER COLUMN cargo_type_id SET NOT NULL, - ALTER COLUMN origin_yard_id SET NOT NULL, - ALTER COLUMN destination_yard_id SET NOT NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS origin_station, - DROP COLUMN IF EXISTS destination_station, - DROP COLUMN IF EXISTS service_type, - DROP COLUMN IF EXISTS freight_type, - DROP COLUMN IF EXISTS freight_subtype, - DROP COLUMN IF EXISTS containers, - DROP COLUMN IF EXISTS first_mile_enabled, - DROP COLUMN IF EXISTS last_mile_enabled, - DROP COLUMN IF EXISTS is_refrigerated; - `); - - // ── booking_container ───────────────────────────────────────────────── - await queryRunner.createTable( - new Table({ - name: 'booking_container', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'booking_id', type: 'uuid' }, - { name: 'container_type_id', type: 'uuid' }, - { name: 'quantity', type: 'smallint' }, - { name: 'vgm_per_unit_tons', type: 'numeric', precision: 10, scale: 3 }, - { name: 'total_vgm_tons', type: 'numeric', precision: 12, scale: 3 }, - { name: 'wagons_required', type: 'numeric', precision: 6, scale: 2 }, - { name: 'weight_limit_rule_id', type: 'uuid', isNullable: true }, - { name: 'is_overweight', type: 'boolean', default: false }, - { name: 'overweight_excess_tons', type: 'numeric', precision: 10, scale: 3, isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createTable( - new Table({ - name: 'booking_rate_snapshot', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'booking_id', type: 'uuid' }, - { name: 'rate_id', type: 'uuid' }, - { name: 'rate_type', type: 'varchar', length: '50' }, - { name: 'rate_value', type: 'numeric', precision: 14, scale: 4 }, - { name: 'rate_unit', type: 'varchar', length: '30' }, - { name: 'currency', type: 'varchar', length: '5' }, - { name: 'snapshotted_at', type: 'timestamptz' }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createTable( - new Table({ - name: 'booking_cargo_modifier', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'booking_id', type: 'uuid' }, - { name: 'surcharge_type_id', type: 'uuid' }, - { name: 'trigger_value', type: 'numeric', precision: 14, scale: 4, isNullable: true }, - { name: 'calculated_amount', type: 'numeric', precision: 14, scale: 2 }, - { name: 'rate_snapshot_id', type: 'uuid' }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createTable( - new Table({ - name: 'booking_approval_step', - schema: 'freight', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'booking_id', type: 'uuid' }, - { name: 'approval_rule_id', type: 'uuid' }, - { name: 'step_order', type: 'smallint' }, - { name: 'required_role', type: 'varchar', length: '30' }, - { name: 'status', type: 'varchar', length: '20', default: "'PENDING'" }, - { name: 'actioned_by_staff_id', type: 'uuid', isNullable: true }, - { name: 'actioned_at', type: 'timestamptz', isNullable: true }, - { name: 'remarks', type: 'text', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - // Foreign keys - await queryRunner.createForeignKey( - 'freight.surcharge_types', - new TableForeignKey({ - name: 'FK_surcharge_types_rate_id', - columnNames: ['rate_id'], - referencedTableName: 'rates', - referencedSchema: 'freight', - referencedColumnNames: ['id'], - }), - ); - await queryRunner.createForeignKey( - 'freight.booking_container', - new TableForeignKey({ - columnNames: ['booking_id'], - referencedTableName: 'bookings', - referencedSchema: 'freight', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }), - ); - await queryRunner.createForeignKey( - 'freight.bookings', - new TableForeignKey({ - columnNames: ['origin_yard_id'], - referencedTableName: 'yards', - referencedSchema: 'freight', - referencedColumnNames: ['id'], - }), - ); - await queryRunner.createForeignKey( - 'freight.bookings', - new TableForeignKey({ - columnNames: ['destination_yard_id'], - referencedTableName: 'yards', - referencedSchema: 'freight', - referencedColumnNames: ['id'], - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.booking_approval_step', true); - await queryRunner.dropTable('freight.booking_cargo_modifier', true); - await queryRunner.dropTable('freight.booking_rate_snapshot', true); - await queryRunner.dropTable('freight.booking_container', true); - - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS origin_station VARCHAR(255), - ADD COLUMN IF NOT EXISTS destination_station VARCHAR(255), - ADD COLUMN IF NOT EXISTS service_type VARCHAR(30), - ADD COLUMN IF NOT EXISTS freight_type VARCHAR(20), - ADD COLUMN IF NOT EXISTS freight_subtype VARCHAR(100), - ADD COLUMN IF NOT EXISTS containers JSONB, - ADD COLUMN IF NOT EXISTS first_mile_enabled BOOLEAN DEFAULT false, - ADD COLUMN IF NOT EXISTS last_mile_enabled BOOLEAN DEFAULT false, - ADD COLUMN IF NOT EXISTS is_refrigerated BOOLEAN DEFAULT false; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS origin_yard_id, - DROP COLUMN IF EXISTS destination_yard_id, - DROP COLUMN IF EXISTS service_type_id, - DROP COLUMN IF EXISTS cargo_type_id, - DROP COLUMN IF EXISTS cargo_free_text, - DROP COLUMN IF EXISTS shipping_line_id, - DROP COLUMN IF EXISTS pnr_code, - DROP COLUMN IF EXISTS customer_signed_at, - DROP COLUMN IF EXISTS fully_executed_at; - `); - - await queryRunner.dropTable('freight.approval_rules', true); - await queryRunner.dropTable('freight.shipping_lines', true); - await queryRunner.dropTable('freight.yards', true); - await queryRunner.dropTable('freight.rates', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1748700000000-AddBookingsConfigForeignKeys.ts b/apps/edr-freight-api/src/migrations/1748700000000-AddBookingsConfigForeignKeys.ts deleted file mode 100644 index 79f3cfb94..000000000 --- a/apps/edr-freight-api/src/migrations/1748700000000-AddBookingsConfigForeignKeys.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddBookingsConfigForeignKeys1748700000000 implements MigrationInterface { - name = 'AddBookingsConfigForeignKeys1748700000000'; - - public async up(queryRunner: QueryRunner): Promise { - // Ensure parent config rows exist for backfill - await queryRunner.query(` - INSERT INTO freight.service_types (id, code, service_name, can_be_booked_alone, includes_first_mile, includes_last_mile, includes_customs, priority_bonus_points, is_active, display_order, created_at, updated_at) - SELECT uuid_generate_v4(), 'RAIL', 'Rail Transport Only', true, false, false, false, 0, true, 1, now(), now() - WHERE NOT EXISTS (SELECT 1 FROM freight.service_types LIMIT 1); - `); - await queryRunner.query(` - INSERT INTO freight.cargo_types (id, code, cargo_type_name, requires_director_approval, show_free_text_box, is_active, display_order, created_at, updated_at) - SELECT uuid_generate_v4(), 'GENERAL', 'General Cargo', false, false, true, 1, now(), now() - WHERE NOT EXISTS (SELECT 1 FROM freight.cargo_types LIMIT 1); - `); - - // Clear orphan shipping_line references (nullable FK) - await queryRunner.query(` - UPDATE freight.bookings b - SET shipping_line_id = NULL - WHERE b.shipping_line_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM freight.shipping_lines sl WHERE sl.id = b.shipping_line_id - ); - `); - - // Backfill required FK columns - await queryRunner.query(` - UPDATE freight.bookings - SET service_type_id = (SELECT id FROM freight.service_types ORDER BY display_order LIMIT 1) - WHERE service_type_id IS NULL - OR NOT EXISTS (SELECT 1 FROM freight.service_types st WHERE st.id = service_type_id); - `); - await queryRunner.query(` - UPDATE freight.bookings - SET cargo_type_id = (SELECT id FROM freight.cargo_types ORDER BY display_order LIMIT 1) - WHERE cargo_type_id IS NULL - OR NOT EXISTS (SELECT 1 FROM freight.cargo_types ct WHERE ct.id = cargo_type_id); - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_service_type_id" - FOREIGN KEY (service_type_id) - REFERENCES freight.service_types(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_cargo_type_id" - FOREIGN KEY (cargo_type_id) - REFERENCES freight.cargo_types(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_shipping_line_id" - FOREIGN KEY (shipping_line_id) - REFERENCES freight.shipping_lines(id) - ON DELETE SET NULL; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_shipping_line_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_cargo_type_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_service_type_id"; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1748800000000-AddBookingsRemainingForeignKeys.ts b/apps/edr-freight-api/src/migrations/1748800000000-AddBookingsRemainingForeignKeys.ts deleted file mode 100644 index 50d052a24..000000000 --- a/apps/edr-freight-api/src/migrations/1748800000000-AddBookingsRemainingForeignKeys.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddBookingsRemainingForeignKeys1748800000000 implements MigrationInterface { - name = 'AddBookingsRemainingForeignKeys1748800000000'; - - public async up(queryRunner: QueryRunner): Promise { - const publicCustomersExists = await queryRunner.query(` - SELECT EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'customers' - ) AS exists - `); - const hasPublicCustomers = Boolean(publicCustomersExists[0]?.exists); - - // ── freight.bookings: nullable FK cleanup ───────────────────────────── - await queryRunner.query(` - UPDATE freight.bookings b - SET train_id = NULL - WHERE b.train_id IS NOT NULL - AND NOT EXISTS (SELECT 1 FROM freight.trains t WHERE t.id = b.train_id); - `); - await queryRunner.query(` - UPDATE freight.bookings b - SET previous_contract_id = NULL - WHERE b.previous_contract_id IS NOT NULL - AND NOT EXISTS (SELECT 1 FROM freight.bookings pb WHERE pb.id = b.previous_contract_id); - `); - await queryRunner.query(` - UPDATE freight.bookings b - SET consolidation_partner_id = NULL - WHERE b.consolidation_partner_id IS NOT NULL - AND NOT EXISTS (SELECT 1 FROM freight.bookings pb WHERE pb.id = b.consolidation_partner_id); - `); - - // Legacy DBs only: public.customers is created/moved in MoveCustomersToFreightSchema (174890). - if (hasPublicCustomers) { - await queryRunner.query(` - DELETE FROM freight.booking_cargo_modifier bcm - USING freight.bookings b - WHERE bcm.booking_id = b.id - AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id); - `); - await queryRunner.query(` - DELETE FROM freight.booking_approval_step bas - USING freight.bookings b - WHERE bas.booking_id = b.id - AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id); - `); - await queryRunner.query(` - DELETE FROM freight.booking_rate_snapshot brs - USING freight.bookings b - WHERE brs.booking_id = b.id - AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id); - `); - await queryRunner.query(` - DELETE FROM freight.booking_container bc - USING freight.bookings b - WHERE bc.booking_id = b.id - AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id); - `); - await queryRunner.query(` - DELETE FROM freight.bookings b - WHERE NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id); - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_customer_id" - FOREIGN KEY (customer_id) - REFERENCES public.customers(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - } - - // ── freight.bookings FKs ──────────────────────────────────────────── - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_train_id" - FOREIGN KEY (train_id) - REFERENCES freight.trains(id) - ON DELETE SET NULL; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_previous_contract_id" - FOREIGN KEY (previous_contract_id) - REFERENCES freight.bookings(id) - ON DELETE SET NULL; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_consolidation_partner_id" - FOREIGN KEY (consolidation_partner_id) - REFERENCES freight.bookings(id) - ON DELETE SET NULL; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - // ── freight.booking_container ───────────────────────────────────────── - await queryRunner.query(` - UPDATE freight.booking_container bc - SET weight_limit_rule_id = NULL - WHERE bc.weight_limit_rule_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM freight.weight_limit_rules wlr WHERE wlr.id = bc.weight_limit_rule_id - ); - `); - - await queryRunner.query(` - DELETE FROM freight.booking_container bc - WHERE NOT EXISTS (SELECT 1 FROM freight.container_types ct WHERE ct.id = bc.container_type_id); - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.booking_container - ADD CONSTRAINT "FK_booking_container_container_type_id" - FOREIGN KEY (container_type_id) - REFERENCES freight.container_types(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.booking_container - ADD CONSTRAINT "FK_booking_container_weight_limit_rule_id" - FOREIGN KEY (weight_limit_rule_id) - REFERENCES freight.weight_limit_rules(id) - ON DELETE SET NULL; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - // ── freight.booking_rate_snapshot ───────────────────────────────────── - await queryRunner.query(` - DELETE FROM freight.booking_cargo_modifier bcm - USING freight.booking_rate_snapshot brs - WHERE bcm.rate_snapshot_id = brs.id - AND ( - NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = brs.booking_id) - OR NOT EXISTS (SELECT 1 FROM freight.rates r WHERE r.id = brs.rate_id) - ); - `); - await queryRunner.query(` - DELETE FROM freight.booking_rate_snapshot brs - WHERE NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = brs.booking_id) - OR NOT EXISTS (SELECT 1 FROM freight.rates r WHERE r.id = brs.rate_id); - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.booking_rate_snapshot - ADD CONSTRAINT "FK_booking_rate_snapshot_booking_id" - FOREIGN KEY (booking_id) - REFERENCES freight.bookings(id) - ON DELETE CASCADE; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.booking_rate_snapshot - ADD CONSTRAINT "FK_booking_rate_snapshot_rate_id" - FOREIGN KEY (rate_id) - REFERENCES freight.rates(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - // ── freight.booking_approval_step ───────────────────────────────────── - await queryRunner.query(` - DELETE FROM freight.booking_approval_step bas - WHERE NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = bas.booking_id) - OR NOT EXISTS (SELECT 1 FROM freight.approval_rules ar WHERE ar.id = bas.approval_rule_id); - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.booking_approval_step - ADD CONSTRAINT "FK_booking_approval_step_booking_id" - FOREIGN KEY (booking_id) - REFERENCES freight.bookings(id) - ON DELETE CASCADE; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.booking_approval_step - ADD CONSTRAINT "FK_booking_approval_step_approval_rule_id" - FOREIGN KEY (approval_rule_id) - REFERENCES freight.approval_rules(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - // ── freight.booking_cargo_modifier ──────────────────────────────────── - await queryRunner.query(` - DELETE FROM freight.booking_cargo_modifier bcm - WHERE NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = bcm.booking_id) - OR NOT EXISTS (SELECT 1 FROM freight.surcharge_types st WHERE st.id = bcm.surcharge_type_id) - OR NOT EXISTS ( - SELECT 1 FROM freight.booking_rate_snapshot brs WHERE brs.id = bcm.rate_snapshot_id - ); - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.booking_cargo_modifier - ADD CONSTRAINT "FK_booking_cargo_modifier_booking_id" - FOREIGN KEY (booking_id) - REFERENCES freight.bookings(id) - ON DELETE CASCADE; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.booking_cargo_modifier - ADD CONSTRAINT "FK_booking_cargo_modifier_surcharge_type_id" - FOREIGN KEY (surcharge_type_id) - REFERENCES freight.surcharge_types(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.booking_cargo_modifier - ADD CONSTRAINT "FK_booking_cargo_modifier_rate_snapshot_id" - FOREIGN KEY (rate_snapshot_id) - REFERENCES freight.booking_rate_snapshot(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_rate_snapshot_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_surcharge_type_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_booking_id"; - `); - - await queryRunner.query(` - ALTER TABLE freight.booking_approval_step - DROP CONSTRAINT IF EXISTS "FK_booking_approval_step_approval_rule_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.booking_approval_step - DROP CONSTRAINT IF EXISTS "FK_booking_approval_step_booking_id"; - `); - - await queryRunner.query(` - ALTER TABLE freight.booking_rate_snapshot - DROP CONSTRAINT IF EXISTS "FK_booking_rate_snapshot_rate_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.booking_rate_snapshot - DROP CONSTRAINT IF EXISTS "FK_booking_rate_snapshot_booking_id"; - `); - - await queryRunner.query(` - ALTER TABLE freight.booking_container - DROP CONSTRAINT IF EXISTS "FK_booking_container_weight_limit_rule_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.booking_container - DROP CONSTRAINT IF EXISTS "FK_booking_container_container_type_id"; - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_consolidation_partner_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_previous_contract_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_train_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_customer_id"; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts b/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts deleted file mode 100644 index 36e848e64..000000000 --- a/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class MoveCustomersToFreightSchema1748900000000 implements MigrationInterface { - name = 'MoveCustomersToFreightSchema1748900000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.customers ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID NOT NULL, - first_name VARCHAR(100) NOT NULL, - last_name VARCHAR(100) NOT NULL, - email VARCHAR(150) NOT NULL UNIQUE, - phone VARCHAR(20) NOT NULL, - company_name VARCHAR(200) NOT NULL, - company_email VARCHAR(150) NOT NULL, - company_phone VARCHAR(20) NOT NULL, - company_location VARCHAR(100) NOT NULL, - company_address TEXT NOT NULL, - customer_type VARCHAR(32), - status VARCHAR(32), - contact_person_name VARCHAR(100) NOT NULL, - contact_person_phone VARCHAR(20) NOT NULL, - tin_number VARCHAR(10) NOT NULL UNIQUE, - vat_number VARCHAR(50), - fan_number VARCHAR(16) NOT NULL UNIQUE, - general_manager_name VARCHAR(100) NOT NULL, - general_manager_email VARCHAR(150) NOT NULL, - general_manager_phone VARCHAR(20) NOT NULL, - poa_name VARCHAR(100), - poa_phone VARCHAR(20), - poa_address TEXT, - poa_email VARCHAR(150), - poa_location VARCHAR(100), - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_freight_customers_email" - ON freight.customers (email); - `); - // await queryRunner.query(` - // CREATE INDEX IF NOT EXISTS "IDX_freight_customers_user_id" - // ON freight.customers (user_id); - //`); - - // Copy rows from public.customers when that legacy table exists - await queryRunner.query(` - DO $$ - DECLARE - has_public boolean; - has_user_id boolean; - has_userid boolean; - BEGIN - SELECT EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = 'customers' - ) INTO has_public; - - IF NOT has_public THEN - RETURN; - END IF; - - SELECT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'public' AND table_name = 'customers' AND column_name = 'user_id' - ) INTO has_user_id; - - SELECT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'public' AND table_name = 'customers' AND column_name = 'userid' - ) INTO has_userid; - - IF has_user_id THEN - INSERT INTO freight.customers ( - id, user_id, first_name, last_name, email, phone, - company_name, company_email, company_phone, company_location, company_address, - contact_person_name, contact_person_phone, tin_number, vat_number, fan_number, - general_manager_name, general_manager_email, general_manager_phone, - poa_name, poa_phone, poa_address, poa_email, poa_location, notes, - created_at, updated_at - ) - SELECT - id, user_id, first_name, last_name, email, phone, - company_name, company_email, company_phone, company_location, company_address, - contact_person_name, contact_person_phone, tin_number, vat_number, fan_number, - general_manager_name, general_manager_email, general_manager_phone, - poa_name, poa_phone, poa_address, poa_email, poa_location, notes, - COALESCE(created_at, now()), COALESCE(updated_at, now()) - FROM public.customers - ON CONFLICT (id) DO NOTHING; - ELSIF has_userid THEN - INSERT INTO freight.customers ( - id, user_id, first_name, last_name, email, phone, - company_name, company_email, company_phone, company_location, company_address, - contact_person_name, contact_person_phone, tin_number, vat_number, fan_number, - general_manager_name, general_manager_email, general_manager_phone, - poa_name, poa_phone, poa_address, poa_email, poa_location, notes, - created_at, updated_at - ) - SELECT - id, userid, firstname, lastname, email, phone, - companyname, companyemail, companyphone, companylocation, companyaddress, - contactpersonname, contactpersonphone, tinnumber, vatnumber, fannumber, - generalmanagername, generalmanageremail, generalmanagerphone, - poaname, poaphone, poaaddress, poaemail, poalocation, notes, - COALESCE("createdAt", now()), COALESCE("updatedAt", now()) - FROM public.customers - ON CONFLICT (id) DO NOTHING; - END IF; - END $$; - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_customer_id"; - `); - - await queryRunner.query(` - DELETE FROM freight.booking_cargo_modifier bcm - USING freight.bookings b - WHERE bcm.booking_id = b.id - AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id); - `); - await queryRunner.query(` - DELETE FROM freight.booking_approval_step bas - USING freight.bookings b - WHERE bas.booking_id = b.id - AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id); - `); - await queryRunner.query(` - DELETE FROM freight.booking_rate_snapshot brs - USING freight.bookings b - WHERE brs.booking_id = b.id - AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id); - `); - await queryRunner.query(` - DELETE FROM freight.booking_container bc - USING freight.bookings b - WHERE bc.booking_id = b.id - AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id); - `); - await queryRunner.query(` - DELETE FROM freight.bookings b - WHERE NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id); - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_customer_id" - FOREIGN KEY (customer_id) - REFERENCES freight.customers(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_customer_id"; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_customer_id" - FOREIGN KEY (customer_id) - REFERENCES public.customers(id) - ON DELETE RESTRICT; - EXCEPTION - WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(`DROP TABLE IF EXISTS freight.customers CASCADE;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749000000000-NormalizeWeightLimitTradeDirectionBoth.ts b/apps/edr-freight-api/src/migrations/1749000000000-NormalizeWeightLimitTradeDirectionBoth.ts deleted file mode 100644 index 5dc1d6315..000000000 --- a/apps/edr-freight-api/src/migrations/1749000000000-NormalizeWeightLimitTradeDirectionBoth.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Align weight_limit_rules.trade_direction with app code: IMPORT, EXPORT, BOTH (not ANY). - */ -export class NormalizeWeightLimitTradeDirectionBoth1749000000000 - implements MigrationInterface -{ - name = 'NormalizeWeightLimitTradeDirectionBoth1749000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DO $$ BEGIN - UPDATE freight.weight_limit_rules - SET trade_direction = 'BOTH' - WHERE trade_direction::text = 'ANY'; - - UPDATE freight.weight_limit_rules - SET trade_direction = 'IMPORT' - WHERE trade_direction IS NULL; - EXCEPTION WHEN undefined_table OR undefined_column THEN NULL; - END $$; - `); - } - - public async down(_queryRunner: QueryRunner): Promise { - // No-op: ANY is not a valid enum value in PostgreSQL. - } -} diff --git a/apps/edr-freight-api/src/migrations/1749100000000-CreateFreightFilesTable.ts b/apps/edr-freight-api/src/migrations/1749100000000-CreateFreightFilesTable.ts deleted file mode 100644 index ebb194833..000000000 --- a/apps/edr-freight-api/src/migrations/1749100000000-CreateFreightFilesTable.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class CreateFreightFilesTable1749100000000 implements MigrationInterface { - name = 'CreateFreightFilesTable1749100000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.files ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - resource_id UUID NOT NULL, - resource VARCHAR(100) NOT NULL, - code VARCHAR(100) NOT NULL, - name VARCHAR(500) NOT NULL, - url TEXT NOT NULL, - size INTEGER NOT NULL, - mime_type VARCHAR(255) NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_freight_files_resource" - ON freight.files (resource_id, resource); - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_freight_files_resource_code" - ON freight.files (resource_id, resource, code); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.files CASCADE`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749200000000-BookingFlowRefactor.ts b/apps/edr-freight-api/src/migrations/1749200000000-BookingFlowRefactor.ts deleted file mode 100644 index 162672727..000000000 --- a/apps/edr-freight-api/src/migrations/1749200000000-BookingFlowRefactor.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class BookingFlowRefactor1749200000000 implements MigrationInterface { - name = 'BookingFlowRefactor1749200000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.booking_review_note ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - booking_id UUID NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE, - author_id UUID, - note TEXT NOT NULL, - type VARCHAR(30) NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - deleted_at TIMESTAMPTZ - ); - CREATE INDEX IF NOT EXISTS idx_booking_review_note_booking_id - ON freight.booking_review_note(booking_id); - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS marketing_approved_by_id UUID, - ADD COLUMN IF NOT EXISTS marketing_approved_at TIMESTAMPTZ, - ADD COLUMN IF NOT EXISTS contract_summary TEXT, - ADD COLUMN IF NOT EXISTS locked_at TIMESTAMPTZ; - `); - - await queryRunner.query(` - UPDATE freight.bookings SET status = 'SUBMITTED' - WHERE status IN ('RFQ_SUBMITTED', 'QUOTATION_SENT', 'QUOTATION_APPROVED'); - UPDATE freight.bookings SET status = 'REJECTED' - WHERE status = 'QUOTATION_REJECTED'; - UPDATE freight.bookings SET status = 'CANCELLED' - WHERE status = 'CANCELLED'; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS locked_at, - DROP COLUMN IF EXISTS contract_summary, - DROP COLUMN IF EXISTS marketing_approved_at, - DROP COLUMN IF EXISTS marketing_approved_by_id; - `); - await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_review_note;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749200000000-CreateCompaniesModule.ts b/apps/edr-freight-api/src/migrations/1749200000000-CreateCompaniesModule.ts deleted file mode 100644 index c02c9fb5b..000000000 --- a/apps/edr-freight-api/src/migrations/1749200000000-CreateCompaniesModule.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex, TableUnique } from 'typeorm'; - -export class CreateCompaniesModule1749200000000 implements MigrationInterface { - name = 'CreateCompaniesModule1749200000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'companies', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'name', type: 'varchar', length: '200' }, - { name: 'type', type: 'varchar', length: '32' }, - { name: 'status', type: 'varchar', length: '32', default: "'pending'" }, - { name: 'tin', type: 'varchar', length: '10', isUnique: true }, - { name: 'vat_number', type: 'varchar', length: '50', isNullable: true }, - { name: 'business_license', type: 'varchar', length: '100', isNullable: true }, - { name: 'fan_number', type: 'varchar', length: '16', isNullable: true }, - { name: 'country', type: 'varchar', length: '32', default: "'Ethiopia'" }, - { name: 'address', type: 'text', isNullable: true }, - { name: 'phone', type: 'varchar', length: '20', isNullable: true }, - { name: 'email', type: 'varchar', length: '150', isNullable: true }, - { name: 'website', type: 'varchar', length: '200', isNullable: true }, - { name: 'attributes', type: 'jsonb', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'external_profiles', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'user_id', type: 'uuid' }, - { name: 'company_id', type: 'uuid' }, - { name: 'first_name', type: 'varchar', length: '100' }, - { name: 'last_name', type: 'varchar', length: '100' }, - { name: 'email', type: 'varchar', length: '150', isUnique: true }, - { name: 'phone', type: 'varchar', length: '20', isNullable: true }, - { name: 'national_id', type: 'varchar', length: '50', isNullable: true }, - { name: 'job_title', type: 'varchar', length: '100', isNullable: true }, - { name: 'is_primary_contact', type: 'boolean', default: false }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['company_id'], - referencedTableName: 'companies', - referencedSchema: 'freight', - referencedColumnNames: ['id'], - }, - ], - }), - true, - ); - - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'ff_clients', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'forwarder_company_id', type: 'uuid' }, - { name: 'client_company_id', type: 'uuid' }, - { name: 'relationship_type', type: 'varchar', length: '32', default: "'managed_account'" }, - { name: 'can_book_on_behalf', type: 'boolean', default: true }, - { name: 'can_view_documents', type: 'boolean', default: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['forwarder_company_id'], - referencedTableName: 'companies', - referencedSchema: 'freight', - referencedColumnNames: ['id'], - }, - { - columnNames: ['client_company_id'], - referencedTableName: 'companies', - referencedSchema: 'freight', - referencedColumnNames: ['id'], - }, - ], - }), - true, - ); - - await queryRunner.createIndex('freight.companies', new TableIndex({ columnNames: ['tin'] })); - await queryRunner.createIndex('freight.companies', new TableIndex({ columnNames: ['type'] })); - await queryRunner.createIndex('freight.external_profiles', new TableIndex({ columnNames: ['user_id'] })); - await queryRunner.createIndex('freight.external_profiles', new TableIndex({ columnNames: ['company_id'] })); - await queryRunner.createIndex('freight.ff_clients', new TableIndex({ columnNames: ['forwarder_company_id'] })); - await queryRunner.createIndex('freight.ff_clients', new TableIndex({ columnNames: ['client_company_id'] })); - - await queryRunner.createUniqueConstraint('freight.ff_clients', new TableUnique({ - columnNames: ['forwarder_company_id', 'client_company_id'], - })); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.ff_clients'); - await queryRunner.dropTable('freight.external_profiles'); - await queryRunner.dropTable('freight.companies'); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749300000000-AddBookingFreightType.ts b/apps/edr-freight-api/src/migrations/1749300000000-AddBookingFreightType.ts deleted file mode 100644 index 795d93fc3..000000000 --- a/apps/edr-freight-api/src/migrations/1749300000000-AddBookingFreightType.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddBookingFreightType1749300000000 implements MigrationInterface { - name = 'AddBookingFreightType1749300000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS freight_type VARCHAR(20); - `); - - await queryRunner.query(` - UPDATE freight.bookings b - SET freight_type = 'CONTAINER' - WHERE EXISTS ( - SELECT 1 FROM freight.booking_container bc WHERE bc.booking_id = b.id - ); - `); - - await queryRunner.query(` - UPDATE freight.bookings b - SET freight_type = 'BULK' - WHERE freight_type IS NULL - AND b.cargo_type_id IS NOT NULL - AND EXISTS ( - SELECT 1 FROM freight.cargo_types ct - WHERE ct.id = b.cargo_type_id AND ct.requires_director_approval = true - ); - `); - - await queryRunner.query(` - UPDATE freight.bookings - SET freight_type = 'CONTAINER' - WHERE freight_type IS NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - ALTER COLUMN cargo_type_id DROP NOT NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - ALTER COLUMN freight_type SET NOT NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD CONSTRAINT chk_bookings_freight_type - CHECK (freight_type IN ('CONTAINER', 'BULK')); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings DROP CONSTRAINT IF EXISTS chk_bookings_freight_type; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings DROP COLUMN IF EXISTS freight_type; - `); - await queryRunner.query(` - UPDATE freight.bookings SET cargo_type_id = ( - SELECT id FROM freight.cargo_types LIMIT 1 - ) WHERE cargo_type_id IS NULL; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - ALTER COLUMN cargo_type_id SET NOT NULL; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts b/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts deleted file mode 100644 index 4df7ea4ce..000000000 --- a/apps/edr-freight-api/src/migrations/1749300000000-AddFanNumberToCompanies.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddFanNumberToCompanies1749300000000 implements MigrationInterface { - name = 'AddFanNumberToCompanies1749300000000'; - - public async up(queryRunner: QueryRunner): Promise { - // fan_number may already exist when CreateCompaniesModule ran with the full schema - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS fan_number varchar(16) NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS fan_number; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749400000000-AddContractSignatures.ts b/apps/edr-freight-api/src/migrations/1749400000000-AddContractSignatures.ts deleted file mode 100644 index 8126b91ca..000000000 --- a/apps/edr-freight-api/src/migrations/1749400000000-AddContractSignatures.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddContractSignatures1749400000000 implements MigrationInterface { - name = 'AddContractSignatures1749400000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS contract_template_key VARCHAR(80), - ADD COLUMN IF NOT EXISTS contract_generated_at TIMESTAMPTZ, - ADD COLUMN IF NOT EXISTS pricing_breakdown JSONB; - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.booking_contract_signatures ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - booking_id UUID NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE, - signer_role VARCHAR(20) NOT NULL, - signer_user_id UUID, - signer_display_name VARCHAR(200) NOT NULL, - signed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - signature_file_id UUID REFERENCES freight.files(id) ON DELETE SET NULL, - consent_text TEXT, - ip_address VARCHAR(64), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - deleted_at TIMESTAMPTZ, - CONSTRAINT uq_booking_contract_signatures_role - UNIQUE (booking_id, signer_role) - ); - CREATE INDEX IF NOT EXISTS idx_booking_contract_signatures_booking_id - ON freight.booking_contract_signatures(booking_id); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_contract_signatures;`); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS pricing_breakdown, - DROP COLUMN IF EXISTS contract_generated_at, - DROP COLUMN IF EXISTS contract_template_key; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749400000000-AddTrainScheduling.ts b/apps/edr-freight-api/src/migrations/1749400000000-AddTrainScheduling.ts deleted file mode 100644 index 5e61797da..000000000 --- a/apps/edr-freight-api/src/migrations/1749400000000-AddTrainScheduling.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddTrainScheduling1749400000000 implements MigrationInterface { - name = 'AddTrainScheduling1749400000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.wagon_types ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - code VARCHAR(32) NOT NULL UNIQUE, - name VARCHAR(100) NOT NULL, - capacity_tons NUMERIC(10,3) NOT NULL, - length_meters NUMERIC(10,3) NOT NULL, - max_wagons_per_train INT NULL, - supported_load_types TEXT[] NOT NULL DEFAULT '{}', - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.locomotives ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - code VARCHAR(32) NOT NULL UNIQUE, - name VARCHAR(100) NULL, - max_pull_weight_tons NUMERIC(10,3) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'AVAILABLE', - available_from TIMESTAMPTZ NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.train_sets ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - locomotive_id UUID NOT NULL, - total_weight_tons NUMERIC(10,3) NOT NULL, - total_length_meters NUMERIC(10,3) NOT NULL, - wagon_count INT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT fk_train_sets_locomotive FOREIGN KEY (locomotive_id) - REFERENCES freight.locomotives(id) - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.train_set_wagons ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - train_set_id UUID NOT NULL, - wagon_type_id UUID NOT NULL, - sequence_no INT NOT NULL, - capacity_tons NUMERIC(10,3) NOT NULL, - length_meters NUMERIC(10,3) NOT NULL, - assigned_weight_tons NUMERIC(10,3) NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT uq_train_set_wagons_sequence UNIQUE (train_set_id, sequence_no), - CONSTRAINT fk_train_set_wagons_train_set FOREIGN KEY (train_set_id) - REFERENCES freight.train_sets(id) ON DELETE CASCADE, - CONSTRAINT fk_train_set_wagons_wagon_type FOREIGN KEY (wagon_type_id) - REFERENCES freight.wagon_types(id) - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.train_schedules ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - train_set_id UUID NOT NULL UNIQUE, - origin_station_id UUID NOT NULL, - destination_station_id UUID NOT NULL, - scheduled_departure_date TIMESTAMPTZ NOT NULL, - scheduled_arrival_date TIMESTAMPTZ NULL, - status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT fk_train_schedules_train_set FOREIGN KEY (train_set_id) - REFERENCES freight.train_sets(id), - CONSTRAINT fk_train_schedules_origin FOREIGN KEY (origin_station_id) - REFERENCES freight.yards(id), - CONSTRAINT fk_train_schedules_destination FOREIGN KEY (destination_station_id) - REFERENCES freight.yards(id) - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.train_schedule_bookings ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - train_schedule_id UUID NOT NULL, - booking_id UUID NOT NULL UNIQUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT uq_train_schedule_booking UNIQUE (train_schedule_id, booking_id), - CONSTRAINT fk_train_schedule_bookings_schedule FOREIGN KEY (train_schedule_id) - REFERENCES freight.train_schedules(id) ON DELETE CASCADE, - CONSTRAINT fk_train_schedule_bookings_booking FOREIGN KEY (booking_id) - REFERENCES freight.bookings(id) - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.wagon_booking_allocations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - train_set_wagon_id UUID NOT NULL, - booking_id UUID NOT NULL, - allocated_weight_tons NUMERIC(10,3) NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT fk_wagon_booking_allocations_wagon FOREIGN KEY (train_set_wagon_id) - REFERENCES freight.train_set_wagons(id) ON DELETE CASCADE, - CONSTRAINT fk_wagon_booking_allocations_booking FOREIGN KEY (booking_id) - REFERENCES freight.bookings(id) - ); - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_locomotives_status - ON freight.locomotives(status); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_train_sets_status - ON freight.train_sets(status); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_train_schedules_departure_status - ON freight.train_schedules(scheduled_departure_date, status); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_wagon_booking_allocations_booking - ON freight.wagon_booking_allocations(booking_id); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_booking_allocations;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.train_schedule_bookings;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.train_schedules;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.train_set_wagons;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.train_sets;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.locomotives;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_types;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749500000000-AddCompanyIdToBookings.ts b/apps/edr-freight-api/src/migrations/1749500000000-AddCompanyIdToBookings.ts deleted file mode 100644 index 25fbe1806..000000000 --- a/apps/edr-freight-api/src/migrations/1749500000000-AddCompanyIdToBookings.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddCompanyIdToBookings1749500000000 implements MigrationInterface { - name = 'AddCompanyIdToBookings1749500000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ALTER COLUMN customer_id DROP NOT NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS company_id UUID; - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_bookings_company_id - ON freight.bookings(company_id); - `); - - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'FK_bookings_company_id' - ) THEN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_company_id" - FOREIGN KEY (company_id) - REFERENCES freight.companies(id); - END IF; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_company_id"; - `); - - await queryRunner.query(` - UPDATE freight.bookings SET customer_id = company_id WHERE customer_id IS NULL AND company_id IS NOT NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - ALTER COLUMN customer_id SET NOT NULL; - `); - await queryRunner.query(` - DROP INDEX IF EXISTS freight.idx_bookings_company_id; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS company_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749600000000-AddBlocksRoleToApprovalStep.ts b/apps/edr-freight-api/src/migrations/1749600000000-AddBlocksRoleToApprovalStep.ts deleted file mode 100644 index a1830c370..000000000 --- a/apps/edr-freight-api/src/migrations/1749600000000-AddBlocksRoleToApprovalStep.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddBlocksRoleToApprovalStep1749600000000 implements MigrationInterface { - name = 'AddBlocksRoleToApprovalStep1749600000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_approval_step - ADD COLUMN IF NOT EXISTS blocks_role VARCHAR(30) NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_approval_step - DROP COLUMN IF EXISTS blocks_role; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749700000000-SeedDefaultApprovalRules.ts b/apps/edr-freight-api/src/migrations/1749700000000-SeedDefaultApprovalRules.ts deleted file mode 100644 index f972f50c2..000000000 --- a/apps/edr-freight-api/src/migrations/1749700000000-SeedDefaultApprovalRules.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Seed ITMLS US-06 approval chains if missing (standard + bulk). - */ -export class SeedDefaultApprovalRules1749700000000 implements MigrationInterface { - name = 'SeedDefaultApprovalRules1749700000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - INSERT INTO freight.approval_rules - (id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at) - SELECT uuid_generate_v4(), false, 1, 'LINE_STAFF', 'Review & Approve', NULL, now(), now() - WHERE NOT EXISTS ( - SELECT 1 FROM freight.approval_rules - WHERE requires_director_approval = false AND step_order = 1 AND deleted_at IS NULL - ); - - INSERT INTO freight.approval_rules - (id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at) - SELECT uuid_generate_v4(), false, 2, 'DIRECTOR', 'Final Signature', 'LINE_STAFF', now(), now() - WHERE NOT EXISTS ( - SELECT 1 FROM freight.approval_rules - WHERE requires_director_approval = false AND step_order = 2 AND deleted_at IS NULL - ); - - INSERT INTO freight.approval_rules - (id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at) - SELECT uuid_generate_v4(), true, 1, 'DIRECTOR', 'Review & Approve', 'LINE_STAFF', now(), now() - WHERE NOT EXISTS ( - SELECT 1 FROM freight.approval_rules - WHERE requires_director_approval = true AND step_order = 1 AND deleted_at IS NULL - ); - - INSERT INTO freight.approval_rules - (id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at) - SELECT uuid_generate_v4(), true, 2, 'CEO', 'Final Signature', NULL, now(), now() - WHERE NOT EXISTS ( - SELECT 1 FROM freight.approval_rules - WHERE requires_director_approval = true AND step_order = 2 AND deleted_at IS NULL - ); - `); - } - - public async down(_queryRunner: QueryRunner): Promise { - // Keep seeded rules on rollback to avoid breaking in-flight bookings. - } -} diff --git a/apps/edr-freight-api/src/migrations/1749800000000-AddShippingLinesCodeUniqueIndex.ts b/apps/edr-freight-api/src/migrations/1749800000000-AddShippingLinesCodeUniqueIndex.ts deleted file mode 100644 index 374fea724..000000000 --- a/apps/edr-freight-api/src/migrations/1749800000000-AddShippingLinesCodeUniqueIndex.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner, TableIndex } from 'typeorm'; - -/** - * shipping_lines was created without a unique index on code; seeder upserts require it. - */ -export class AddShippingLinesCodeUniqueIndex1749800000000 implements MigrationInterface { - name = 'AddShippingLinesCodeUniqueIndex1749800000000'; - - public async up(queryRunner: QueryRunner): Promise { - const existing = await queryRunner.query( - `SELECT 1 FROM pg_indexes WHERE schemaname = 'freight' AND tablename = 'shipping_lines' AND indexdef ILIKE '%UNIQUE%code%' LIMIT 1`, - ); - if (existing.length === 0) { - await queryRunner.createIndex( - 'freight.shipping_lines', - new TableIndex({ - name: 'UQ_shipping_lines_code', - columnNames: ['code'], - isUnique: true, - }), - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropIndex('freight.shipping_lines', 'UQ_shipping_lines_code'); - } -} diff --git a/apps/edr-freight-api/src/migrations/1749900000000-CreateFileUploadSettingsTables.ts b/apps/edr-freight-api/src/migrations/1749900000000-CreateFileUploadSettingsTables.ts deleted file mode 100644 index cbbf914d4..000000000 --- a/apps/edr-freight-api/src/migrations/1749900000000-CreateFileUploadSettingsTables.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * file_upload_settings / file_upload_fields entities had no migration; seeder requires both tables. - */ -export class CreateFileUploadSettingsTables1749900000000 implements MigrationInterface { - name = 'CreateFileUploadSettingsTables1749900000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.file_upload_settings ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - code VARCHAR(128) NOT NULL, - label VARCHAR(256) NOT NULL, - description TEXT, - entity VARCHAR(32) NOT NULL DEFAULT 'other', - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_file_upload_settings_code" - ON freight.file_upload_settings (code); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.file_upload_fields ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - setting_id UUID NOT NULL, - file_key VARCHAR(128) NOT NULL, - file_label VARCHAR(256) NOT NULL, - help_text TEXT, - is_required BOOLEAN NOT NULL DEFAULT false, - is_multiple BOOLEAN NOT NULL DEFAULT false, - max_files INTEGER NOT NULL DEFAULT 1, - allowed_extensions TEXT[] NOT NULL DEFAULT '{}'::text[], - max_size_mb INTEGER NOT NULL DEFAULT 10, - display_order INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ, - CONSTRAINT "CHK_file_upload_fields_max_files" CHECK (max_files > 0), - CONSTRAINT "CHK_file_upload_fields_max_size_mb" CHECK (max_size_mb > 0), - CONSTRAINT "FK_file_upload_fields_setting" - FOREIGN KEY (setting_id) - REFERENCES freight.file_upload_settings(id) - ON DELETE CASCADE - ); - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_file_upload_fields_setting_file_key" - ON freight.file_upload_fields (setting_id, file_key); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.file_upload_fields CASCADE`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.file_upload_settings CASCADE`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750000000000-AddCompanyContactColumns.ts b/apps/edr-freight-api/src/migrations/1750000000000-AddCompanyContactColumns.ts deleted file mode 100644 index 56d41edf9..000000000 --- a/apps/edr-freight-api/src/migrations/1750000000000-AddCompanyContactColumns.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddCompanyContactColumns1750000000000 implements MigrationInterface { - name = 'AddCompanyContactColumns1750000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS contact_person_name VARCHAR(100);`); - await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS contact_person_phone VARCHAR(20);`); - await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS general_manager_name VARCHAR(100);`); - await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS general_manager_email VARCHAR(150);`); - await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS general_manager_phone VARCHAR(20);`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS general_manager_phone;`); - await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS general_manager_email;`); - await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS general_manager_name;`); - await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS contact_person_phone;`); - await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS contact_person_name;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750000000000-AddTrainExtendedColumns.ts b/apps/edr-freight-api/src/migrations/1750000000000-AddTrainExtendedColumns.ts deleted file mode 100644 index b249bd198..000000000 --- a/apps/edr-freight-api/src/migrations/1750000000000-AddTrainExtendedColumns.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Train entity gained extended fields; baseline trains table only had code/capacity/status/notes. - */ -export class AddTrainExtendedColumns1750000000000 implements MigrationInterface { - name = 'AddTrainExtendedColumns1750000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.trains - ADD COLUMN IF NOT EXISTS train_number VARCHAR(20), - ADD COLUMN IF NOT EXISTS train_name VARCHAR(100), - ADD COLUMN IF NOT EXISTS route_id UUID, - ADD COLUMN IF NOT EXISTS origin_station_id UUID, - ADD COLUMN IF NOT EXISTS destination_station_id UUID, - ADD COLUMN IF NOT EXISTS departure_time TIMESTAMPTZ, - ADD COLUMN IF NOT EXISTS arrival_time TIMESTAMPTZ, - ADD COLUMN IF NOT EXISTS locomotive_number VARCHAR(50), - ADD COLUMN IF NOT EXISTS remarks TEXT; - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_trains_train_number" - ON freight.trains (train_number) - WHERE train_number IS NOT NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_trains_train_number"`); - await queryRunner.query(` - ALTER TABLE freight.trains - DROP COLUMN IF EXISTS remarks, - DROP COLUMN IF EXISTS locomotive_number, - DROP COLUMN IF EXISTS arrival_time, - DROP COLUMN IF EXISTS departure_time, - DROP COLUMN IF EXISTS destination_station_id, - DROP COLUMN IF EXISTS origin_station_id, - DROP COLUMN IF EXISTS route_id, - DROP COLUMN IF EXISTS train_name, - DROP COLUMN IF EXISTS train_number; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750000000000-CreateFacilitiesTable.ts b/apps/edr-freight-api/src/migrations/1750000000000-CreateFacilitiesTable.ts deleted file mode 100644 index 32e1ff653..000000000 --- a/apps/edr-freight-api/src/migrations/1750000000000-CreateFacilitiesTable.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -export class CreateFacilitiesTable1750000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'facilities', - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - generationStrategy: 'uuid', - default: 'gen_random_uuid()', - }, - { - name: 'code', - type: 'varchar', - length: '40', - isUnique: true, - }, - { - name: 'name', - type: 'varchar', - length: '160', - }, - { - name: 'description', - type: 'text', - isNullable: true, - }, - { - name: 'facility_type', - type: 'varchar', - length: '32', - }, - { - name: 'facility_status', - type: 'varchar', - length: '32', - default: "'ACTIVE'", - }, - { - name: 'location_name', - type: 'varchar', - length: '200', - isNullable: true, - }, - { - name: 'country', - type: 'varchar', - length: '100', - isNullable: true, - }, - { - name: 'city', - type: 'varchar', - length: '100', - isNullable: true, - }, - { - name: 'address', - type: 'text', - isNullable: true, - }, - { - name: 'latitude', - type: 'numeric', - precision: 10, - scale: 8, - isNullable: true, - }, - { - name: 'longitude', - type: 'numeric', - precision: 11, - scale: 8, - isNullable: true, - }, - { - name: 'capacity', - type: 'numeric', - precision: 14, - scale: 3, - isNullable: true, - }, - { - name: 'is_active', - type: 'boolean', - default: true, - }, - { - name: 'notes', - type: 'text', - isNullable: true, - }, - { - name: 'created_at', - type: 'timestamp', - default: 'CURRENT_TIMESTAMP', - }, - { - name: 'updated_at', - type: 'timestamp', - default: 'CURRENT_TIMESTAMP', - }, - { - name: 'deleted_at', - type: 'timestamp', - isNullable: true, - }, - ], - }), - ); - - await queryRunner.createIndex( - 'freight.facilities', - new TableIndex({ - name: 'idx_facilities_code', - columnNames: ['code'], - isUnique: true, - }), - ); - - await queryRunner.createIndex( - 'freight.facilities', - new TableIndex({ - name: 'idx_facilities_status', - columnNames: ['facility_status'], - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.facilities'); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750000000001-AddFacilityIdToWarehouses.ts b/apps/edr-freight-api/src/migrations/1750000000001-AddFacilityIdToWarehouses.ts deleted file mode 100644 index 82f2d92fd..000000000 --- a/apps/edr-freight-api/src/migrations/1750000000001-AddFacilityIdToWarehouses.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from 'typeorm'; - -export class AddFacilityIdToWarehouses1750000000001 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable('freight.warehouses'); - if (!table) { - // warehouses table doesn't exist yet, skip this migration - return; - } - - const hasColumn = table.columns.some((col) => col.name === 'facility_id'); - if (hasColumn) { - // Column already exists, skip - return; - } - - await queryRunner.addColumn( - 'freight.warehouses', - new TableColumn({ - name: 'facility_id', - type: 'uuid', - isNullable: true, - }), - ); - - await queryRunner.createForeignKey( - 'freight.warehouses', - new TableForeignKey({ - columnNames: ['facility_id'], - referencedColumnNames: ['id'], - referencedTableName: 'facilities', - referencedSchema: 'freight', - onDelete: 'SET NULL', - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable('freight.warehouses'); - if (!table) { - return; - } - const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.includes('facility_id')); - if (foreignKey) { - await queryRunner.dropForeignKey('freight.warehouses', foreignKey); - } - const hasColumn = table.columns.some((col) => col.name === 'facility_id'); - if (hasColumn) { - await queryRunner.dropColumn('freight.warehouses', 'facility_id'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1750000000002-AddProofOfDeliveryToCargoes.ts b/apps/edr-freight-api/src/migrations/1750000000002-AddProofOfDeliveryToCargoes.ts deleted file mode 100644 index 67957f437..000000000 --- a/apps/edr-freight-api/src/migrations/1750000000002-AddProofOfDeliveryToCargoes.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -/** - * Proof of Delivery (customer pickup) capture on cargoes: - * receiver name, delivered/picked-up timestamp, and delivery remarks. - */ -export class AddProofOfDeliveryToCargoes1750000000002 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable('freight.cargoes'); - if (!table) { - // cargoes table doesn't exist yet, skip this migration - return; - } - - const columnsToAdd = [ - { name: 'receiver_name', type: 'varchar', isNullable: true }, - { name: 'delivered_at', type: 'timestamp', isNullable: true }, - { name: 'delivery_remarks', type: 'text', isNullable: true }, - ]; - - const columnsToCreate = columnsToAdd.filter( - (col) => !table.columns.some((c) => c.name === col.name), - ); - - if (columnsToCreate.length > 0) { - await queryRunner.addColumns( - 'freight.cargoes', - columnsToCreate.map((col) => new TableColumn(col)), - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable('freight.cargoes'); - if (!table) { - return; - } - - const columnNames = ['receiver_name', 'delivered_at', 'delivery_remarks']; - const columnsToRemove = columnNames.filter((name) => - table.columns.some((c) => c.name === name), - ); - - if (columnsToRemove.length > 0) { - await queryRunner.dropColumns('freight.cargoes', columnsToRemove); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1750000000003-AddWarehouseInspection.ts b/apps/edr-freight-api/src/migrations/1750000000003-AddWarehouseInspection.ts deleted file mode 100644 index 88ef8d3a5..000000000 --- a/apps/edr-freight-api/src/migrations/1750000000003-AddWarehouseInspection.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm'; - -/** - * Batch 4.5 — warehouse inspection reports + inventory inspection status. - */ -export class AddWarehouseInspection1750000000003 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - // inventory.inspection_status - const inventoryTable = await queryRunner.getTable('freight.warehouse_inventory'); - if (inventoryTable) { - const hasColumn = inventoryTable.columns.some((col) => col.name === 'inspection_status'); - if (!hasColumn) { - await queryRunner.addColumn( - 'freight.warehouse_inventory', - new TableColumn({ name: 'inspection_status', type: 'varchar', length: '20', isNullable: true }), - ); - } - } - - // warehouse_inspection_reports table - const inspectionTable = await queryRunner.getTable('freight.warehouse_inspection_reports'); - if (!inspectionTable) { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'warehouse_inspection_reports', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' }, - { name: 'inventory_id', type: 'uuid' }, - { name: 'booking_id', type: 'uuid', isNullable: true }, - { name: 'customer_id', type: 'uuid', isNullable: true }, - { name: 'report_type', type: 'varchar', length: '32', default: "'INSPECTION'" }, - { name: 'inspection_status', type: 'varchar', length: '20', default: "'NEEDS_REVIEW'" }, - { name: 'has_damage', type: 'boolean', default: false }, - { name: 'damage_description', type: 'text', isNullable: true }, - { name: 'has_weight_loss', type: 'boolean', default: false }, - { name: 'expected_weight', type: 'numeric', precision: 14, scale: 3, isNullable: true }, - { name: 'actual_weight', type: 'numeric', precision: 14, scale: 3, isNullable: true }, - { name: 'weight_loss', type: 'numeric', precision: 14, scale: 3, isNullable: true }, - { name: 'weight_loss_unit', type: 'varchar', length: '12', isNullable: true }, - { name: 'has_missing_items', type: 'boolean', default: false }, - { name: 'missing_items_description', type: 'text', isNullable: true }, - { name: 'remarks', type: 'text', isNullable: true }, - { name: 'inspected_by_id', type: 'uuid', isNullable: true }, - { name: 'inspected_at', type: 'timestamptz', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - indices: [ - { name: 'idx_wir_inventory', columnNames: ['inventory_id'] }, - { name: 'idx_wir_booking', columnNames: ['booking_id'] }, - { name: 'idx_wir_status', columnNames: ['inspection_status'] }, - ], - }), - true, - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - const inspectionTable = await queryRunner.getTable('freight.warehouse_inspection_reports'); - if (inspectionTable) { - await queryRunner.dropTable('freight.warehouse_inspection_reports', true); - } - - const inventoryTable = await queryRunner.getTable('freight.warehouse_inventory'); - if (inventoryTable) { - const hasColumn = inventoryTable.columns.some((col) => col.name === 'inspection_status'); - if (hasColumn) { - await queryRunner.dropColumn('freight.warehouse_inventory', 'inspection_status'); - } - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1750100000000-AddRoutesAndExtendLocomotives.ts b/apps/edr-freight-api/src/migrations/1750100000000-AddRoutesAndExtendLocomotives.ts deleted file mode 100644 index 421f66ef9..000000000 --- a/apps/edr-freight-api/src/migrations/1750100000000-AddRoutesAndExtendLocomotives.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddRoutesAndExtendLocomotives1750100000000 implements MigrationInterface { - name = 'AddRoutesAndExtendLocomotives1750100000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.locomotives - ADD COLUMN IF NOT EXISTS locomotive_type VARCHAR(20) NOT NULL DEFAULT 'DIESEL', - ADD COLUMN IF NOT EXISTS max_train_length_meters NUMERIC(10,3) NOT NULL DEFAULT 760, - ADD COLUMN IF NOT EXISTS power_kw NUMERIC(10,3) NULL, - ADD COLUMN IF NOT EXISTS traction_force_kn NUMERIC(10,3) NULL, - ADD COLUMN IF NOT EXISTS max_speed_kmh NUMERIC(10,3) NULL; - `); - - await queryRunner.query(` - UPDATE freight.locomotives - SET status = 'OUT_OF_SERVICE' - WHERE status = 'INACTIVE'; - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.routes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(120) NOT NULL UNIQUE, - origin_yard_id UUID NOT NULL REFERENCES freight.yards(id), - destination_yard_id UUID NOT NULL REFERENCES freight.yards(id), - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.route_milestones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - route_id UUID NOT NULL REFERENCES freight.routes(id) ON DELETE CASCADE, - yard_id UUID NOT NULL REFERENCES freight.yards(id), - sequence_no INT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT uq_route_milestones_route_sequence UNIQUE (route_id, sequence_no) - ); - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_routes_origin_yard_id - ON freight.routes(origin_yard_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_routes_destination_yard_id - ON freight.routes(destination_yard_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_routes_is_active - ON freight.routes(is_active); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_route_milestones_route_id - ON freight.route_milestones(route_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_route_milestones_yard_id - ON freight.route_milestones(yard_id); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.route_milestones;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.routes;`); - await queryRunner.query(` - ALTER TABLE freight.locomotives - DROP COLUMN IF EXISTS max_speed_kmh, - DROP COLUMN IF EXISTS traction_force_kn, - DROP COLUMN IF EXISTS power_kw, - DROP COLUMN IF EXISTS max_train_length_meters, - DROP COLUMN IF EXISTS locomotive_type; - `); - await queryRunner.query(` - UPDATE freight.locomotives - SET status = 'INACTIVE' - WHERE status = 'OUT_OF_SERVICE'; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750100000000-CreateFleetCrudTables.ts b/apps/edr-freight-api/src/migrations/1750100000000-CreateFleetCrudTables.ts deleted file mode 100644 index 1763a9db0..000000000 --- a/apps/edr-freight-api/src/migrations/1750100000000-CreateFleetCrudTables.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class CreateFleetCrudTables1750100000000 implements MigrationInterface { - name = 'CreateFleetCrudTables1750100000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS freight`); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.wagons ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - wagon_number VARCHAR NOT NULL UNIQUE, - wagon_type_id UUID NOT NULL, - train_id UUID, - sequence_number INT, - tare_weight NUMERIC(10, 2) NOT NULL, - max_payload_weight NUMERIC(10, 2) NOT NULL, - status VARCHAR NOT NULL DEFAULT 'AVAILABLE', - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.containers ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - container_number VARCHAR NOT NULL UNIQUE, - container_type_id UUID NOT NULL, - wagon_id UUID, - position INT, - tare_weight NUMERIC(10, 2) NOT NULL, - max_gross_weight NUMERIC(10, 2) NOT NULL, - seal_number VARCHAR, - status VARCHAR NOT NULL DEFAULT 'AVAILABLE', - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.cargoes ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - cargo_reference VARCHAR NOT NULL UNIQUE, - shipment_id UUID NOT NULL, - container_id UUID NOT NULL, - cargo_type_id UUID, - description TEXT, - quantity NUMERIC(12, 3) NOT NULL, - weight NUMERIC(10, 2) NOT NULL, - volume NUMERIC(10, 2), - status VARCHAR NOT NULL DEFAULT 'PENDING', - loaded_at TIMESTAMP, - unloaded_at TIMESTAMP, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_wagons_train_id" ON freight.wagons (train_id)`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_containers_wagon_id" ON freight.containers (wagon_id)`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_cargoes_container_id" ON freight.cargoes (container_id)`); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.wagons - ADD CONSTRAINT "FK_wagons_train_id" - FOREIGN KEY (train_id) REFERENCES freight.trains(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.wagons - ADD CONSTRAINT "FK_wagons_wagon_type_id" - FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types(id); - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.containers - ADD CONSTRAINT "FK_containers_wagon_id" - FOREIGN KEY (wagon_id) REFERENCES freight.wagons(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.containers - ADD CONSTRAINT "FK_containers_container_type_id" - FOREIGN KEY (container_type_id) REFERENCES freight.container_types(id); - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.cargoes - ADD CONSTRAINT "FK_cargoes_container_id" - FOREIGN KEY (container_id) REFERENCES freight.containers(id) ON DELETE RESTRICT; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.cargoes - ADD CONSTRAINT "FK_cargoes_cargo_type_id" - FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id); - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.cargoes CASCADE`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.containers CASCADE`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.wagons CASCADE`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750200000000-AddPhysicalWagonToTrainSetWagons.ts b/apps/edr-freight-api/src/migrations/1750200000000-AddPhysicalWagonToTrainSetWagons.ts deleted file mode 100644 index 9ce1db6c1..000000000 --- a/apps/edr-freight-api/src/migrations/1750200000000-AddPhysicalWagonToTrainSetWagons.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddPhysicalWagonToTrainSetWagons1750200000000 implements MigrationInterface { - name = 'AddPhysicalWagonToTrainSetWagons1750200000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_set_wagons - ADD COLUMN IF NOT EXISTS physical_wagon_id UUID NULL; - `); - - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM information_schema.table_constraints - WHERE constraint_schema = 'freight' - AND table_name = 'train_set_wagons' - AND constraint_name = 'fk_train_set_wagons_physical_wagon' - ) THEN - ALTER TABLE freight.train_set_wagons - ADD CONSTRAINT fk_train_set_wagons_physical_wagon - FOREIGN KEY (physical_wagon_id) - REFERENCES freight.wagons(id) - ON DELETE SET NULL; - END IF; - END $$; - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_train_set_wagons_physical_wagon - ON freight.train_set_wagons(physical_wagon_id); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_train_set_wagons_physical_wagon;`); - await queryRunner.query(` - ALTER TABLE freight.train_set_wagons - DROP CONSTRAINT IF EXISTS fk_train_set_wagons_physical_wagon; - `); - await queryRunner.query(` - ALTER TABLE freight.train_set_wagons - DROP COLUMN IF EXISTS physical_wagon_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750200000000-SeedDefaultWagonTypes.ts b/apps/edr-freight-api/src/migrations/1750200000000-SeedDefaultWagonTypes.ts deleted file mode 100644 index 06dc0b917..000000000 --- a/apps/edr-freight-api/src/migrations/1750200000000-SeedDefaultWagonTypes.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class SeedDefaultWagonTypes1750200000000 implements MigrationInterface { - name = 'SeedDefaultWagonTypes1750200000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - INSERT INTO freight.wagon_types ( - code, - name, - capacity_tons, - length_meters, - max_wagons_per_train, - supported_load_types, - is_active - ) - VALUES - ('NW7', 'Double deck sedan wagon', 22, 26.066, NULL, ARRAY['vehicles', 'sedan'], true), - ('NW5', 'Flat wagon (container)', 70, 14.000, 53, ARRAY['container', 'steel', 'machinery'], true), - ('PW2', 'Box wagon', 70, 17.066, 18, ARRAY['general cargo', 'break bulk'], true), - ('GW2', 'Tank wagon', 70, 12.228, 37, ARRAY['liquid', 'fuel'], true), - ('CW4', 'Gondola covered wagon', 70, 13.976, 37, ARRAY['covered bulk cargo'], true), - ('CW3', 'Gondola open wagon', 70, 13.976, NULL, ARRAY['open bulk cargo'], true), - ('KW2', 'Hopper covered wagon', 69, 16.466, NULL, ARRAY['bulk grains'], true), - ('KW3', 'Hopper open wagon', 70, 14.400, NULL, ARRAY['coal', 'bulk cargo'], true), - ('NW6', 'Flat wagon (long cargo)', 70, 18.560, NULL, ARRAY['long cargo'], true), - ('BW1', 'Refrigerated wagon', 38, 21.996, NULL, ARRAY['refrigerated cargo'], true) - ON CONFLICT (code) DO UPDATE SET - name = EXCLUDED.name, - capacity_tons = EXCLUDED.capacity_tons, - length_meters = EXCLUDED.length_meters, - max_wagons_per_train = EXCLUDED.max_wagons_per_train, - supported_load_types = EXCLUDED.supported_load_types, - is_active = true, - deleted_at = NULL, - updated_at = now(); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DELETE FROM freight.wagon_types - WHERE code IN ('NW7', 'NW5', 'PW2', 'GW2', 'CW4', 'CW3', 'KW2', 'KW3', 'NW6', 'BW1'); - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750300000000-AddCurrentLocationToWagons.ts b/apps/edr-freight-api/src/migrations/1750300000000-AddCurrentLocationToWagons.ts deleted file mode 100644 index 454218fd7..000000000 --- a/apps/edr-freight-api/src/migrations/1750300000000-AddCurrentLocationToWagons.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddCurrentLocationToWagons1750300000000 implements MigrationInterface { - name = 'AddCurrentLocationToWagons1750300000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagons - ADD COLUMN IF NOT EXISTS current_location_yard_id UUID NULL; - `); - - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM information_schema.table_constraints - WHERE constraint_schema = 'freight' - AND table_name = 'wagons' - AND constraint_name = 'FK_wagons_current_location_yard_id' - ) THEN - ALTER TABLE freight.wagons - ADD CONSTRAINT "FK_wagons_current_location_yard_id" - FOREIGN KEY (current_location_yard_id) - REFERENCES freight.yards(id) - ON DELETE SET NULL; - END IF; - END $$; - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_wagons_current_location_yard_id" - ON freight.wagons(current_location_yard_id); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_wagons_current_location_yard_id";`); - await queryRunner.query(` - ALTER TABLE freight.wagons - DROP CONSTRAINT IF EXISTS "FK_wagons_current_location_yard_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.wagons - DROP COLUMN IF EXISTS current_location_yard_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750300000000-AddRouteToTrainSchedules.ts b/apps/edr-freight-api/src/migrations/1750300000000-AddRouteToTrainSchedules.ts deleted file mode 100644 index 027ebfe98..000000000 --- a/apps/edr-freight-api/src/migrations/1750300000000-AddRouteToTrainSchedules.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddRouteToTrainSchedules1750300000000 implements MigrationInterface { - name = 'AddRouteToTrainSchedules1750300000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS route_id UUID NULL; - `); - - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_constraint - WHERE conname = 'fk_train_schedules_route' - ) THEN - ALTER TABLE freight.train_schedules - ADD CONSTRAINT fk_train_schedules_route - FOREIGN KEY (route_id) REFERENCES freight.routes(id); - END IF; - END $$; - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_train_schedules_route_id - ON freight.train_schedules(route_id); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_train_schedules_route_id;`); - await queryRunner.query(` - ALTER TABLE freight.train_schedules - DROP CONSTRAINT IF EXISTS fk_train_schedules_route; - `); - await queryRunner.query(` - ALTER TABLE freight.train_schedules - DROP COLUMN IF EXISTS route_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750400000000-AddSchedulingAllocationEnhancements.ts b/apps/edr-freight-api/src/migrations/1750400000000-AddSchedulingAllocationEnhancements.ts deleted file mode 100644 index a0ca64303..000000000 --- a/apps/edr-freight-api/src/migrations/1750400000000-AddSchedulingAllocationEnhancements.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddSchedulingAllocationEnhancements1750400000000 - implements MigrationInterface -{ - name = 'AddSchedulingAllocationEnhancements1750400000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS wagons_required NUMERIC(6,2) NULL, - ADD COLUMN IF NOT EXISTS scheduling_status VARCHAR(30) NOT NULL DEFAULT 'NOT_SCHEDULED', - ADD COLUMN IF NOT EXISTS hold_started_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS hold_expires_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS train_number VARCHAR(20) NULL, - ADD COLUMN IF NOT EXISTS direction VARCHAR(10) NULL, - ADD COLUMN IF NOT EXISTS actual_departure_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS actual_arrival_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS prepared_by_user_id UUID NULL, - ADD COLUMN IF NOT EXISTS checked_by_user_id UUID NULL, - ADD COLUMN IF NOT EXISTS max_wagons INT NOT NULL DEFAULT 53; - `); - - await queryRunner.query(` - ALTER TABLE freight.train_set_wagons - ADD COLUMN IF NOT EXISTS physical_wagon_id UUID NULL, - ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'PLANNED'; - `); - - await queryRunner.query(` - ALTER TABLE freight.wagon_booking_allocations - ADD COLUMN IF NOT EXISTS load_type VARCHAR(20) NULL, - ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'PLANNED', - ADD COLUMN IF NOT EXISTS confirmed_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS confirmed_by_user_id UUID NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.wagon_types - ADD COLUMN IF NOT EXISTS equated_length_m NUMERIC(10,3) NULL, - ADD COLUMN IF NOT EXISTS tare_weight_tons NUMERIC(10,3) NULL, - ADD COLUMN IF NOT EXISTS supports_container BOOLEAN NOT NULL DEFAULT false, - ADD COLUMN IF NOT EXISTS max_container_gross_t NUMERIC(10,3) NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.wagons - ADD COLUMN IF NOT EXISTS train_set_wagon_id UUID NULL, - ADD COLUMN IF NOT EXISTS current_train_schedule_id UUID NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.containers - ADD COLUMN IF NOT EXISTS booking_id UUID NULL, - ADD COLUMN IF NOT EXISTS wagon_booking_allocation_id UUID NULL, - ADD COLUMN IF NOT EXISTS booking_container_id UUID NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.cargoes - ADD COLUMN IF NOT EXISTS wagon_booking_allocation_id UUID NULL, - ADD COLUMN IF NOT EXISTS booking_id UUID NULL, - ADD COLUMN IF NOT EXISTS load_type VARCHAR(20) NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.cargoes - ALTER COLUMN container_id DROP NOT NULL; - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.wagon_allocation_container_items ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - wagon_booking_allocation_id UUID NOT NULL, - booking_container_id UUID NULL, - container_id UUID NULL, - container_number VARCHAR(64) NULL, - container_type_id UUID NOT NULL, - position_on_wagon SMALLINT NULL, - seal_number VARCHAR(64) NULL, - chassis_number VARCHAR(64) NULL, - gross_weight_tons NUMERIC(10,3) NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT fk_waci_allocation FOREIGN KEY (wagon_booking_allocation_id) - REFERENCES freight.wagon_booking_allocations(id) ON DELETE CASCADE, - CONSTRAINT fk_waci_booking_container FOREIGN KEY (booking_container_id) - REFERENCES freight.booking_container(id) ON DELETE SET NULL, - CONSTRAINT fk_waci_container FOREIGN KEY (container_id) - REFERENCES freight.containers(id) ON DELETE SET NULL, - CONSTRAINT fk_waci_container_type FOREIGN KEY (container_type_id) - REFERENCES freight.container_types(id) - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.wagon_allocation_bulk_loads ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - wagon_booking_allocation_id UUID NOT NULL UNIQUE, - booking_id UUID NOT NULL, - cargo_type_id UUID NULL, - cargo_description TEXT NULL, - pricing_unit VARCHAR(20) NOT NULL DEFAULT 'PER_TON', - quantity NUMERIC(12,3) NOT NULL DEFAULT 0, - weight_tons NUMERIC(10,3) NOT NULL DEFAULT 0, - truck_plate_number VARCHAR(32) NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT fk_wabl_allocation FOREIGN KEY (wagon_booking_allocation_id) - REFERENCES freight.wagon_booking_allocations(id) ON DELETE CASCADE, - CONSTRAINT fk_wabl_booking FOREIGN KEY (booking_id) - REFERENCES freight.bookings(id), - CONSTRAINT fk_wabl_cargo_type FOREIGN KEY (cargo_type_id) - REFERENCES freight.cargo_types(id) ON DELETE SET NULL - ); - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_bookings_scheduling_status - ON freight.bookings(scheduling_status); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_train_schedules_train_number - ON freight.train_schedules(train_number); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_train_set_wagons_physical_wagon - ON freight.train_set_wagons(physical_wagon_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_wagons_train_set_wagon_id - ON freight.wagons(train_set_wagon_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_wagons_current_train_schedule_id - ON freight.wagons(current_train_schedule_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_waci_allocation - ON freight.wagon_allocation_container_items(wagon_booking_allocation_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_wabl_booking - ON freight.wagon_allocation_bulk_loads(booking_id); - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.train_set_wagons - ADD CONSTRAINT fk_train_set_wagons_physical_wagon - FOREIGN KEY (physical_wagon_id) REFERENCES freight.wagons(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.wagons - ADD CONSTRAINT fk_wagons_train_set_wagon - FOREIGN KEY (train_set_wagon_id) REFERENCES freight.train_set_wagons(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.wagons - ADD CONSTRAINT fk_wagons_current_train_schedule - FOREIGN KEY (current_train_schedule_id) REFERENCES freight.train_schedules(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.containers - ADD CONSTRAINT fk_containers_booking - FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.containers - ADD CONSTRAINT fk_containers_wagon_allocation - FOREIGN KEY (wagon_booking_allocation_id) REFERENCES freight.wagon_booking_allocations(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.containers - ADD CONSTRAINT fk_containers_booking_container - FOREIGN KEY (booking_container_id) REFERENCES freight.booking_container(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.cargoes - ADD CONSTRAINT fk_cargoes_wagon_allocation - FOREIGN KEY (wagon_booking_allocation_id) REFERENCES freight.wagon_booking_allocations(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.cargoes - ADD CONSTRAINT fk_cargoes_booking - FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE SET NULL; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - UPDATE freight.wagon_types SET - equated_length_m = 1.3, - tare_weight_tons = 22.4, - supports_container = true, - max_container_gross_t = 30.48 - WHERE code = 'NW5'; - `); - await queryRunner.query(` - UPDATE freight.wagon_types SET - equated_length_m = 1.6, - tare_weight_tons = 25.2, - supports_container = false - WHERE code = 'PW2'; - `); - await queryRunner.query(` - UPDATE freight.wagon_types SET - equated_length_m = 1.5, - tare_weight_tons = 25.2, - supports_container = false - WHERE code = 'KW2'; - `); - await queryRunner.query(` - UPDATE freight.wagon_types SET - equated_length_m = 1.3, - tare_weight_tons = 23.4, - supports_container = false - WHERE code = 'CW3'; - `); - await queryRunner.query(` - UPDATE freight.wagon_types SET - equated_length_m = 1.3, - tare_weight_tons = 24.8, - supports_container = false - WHERE code = 'CW4'; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_allocation_bulk_loads;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_allocation_container_items;`); - - await queryRunner.query(` - ALTER TABLE freight.cargoes - ALTER COLUMN container_id SET NOT NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS wagons_required, - DROP COLUMN IF EXISTS scheduling_status, - DROP COLUMN IF EXISTS hold_started_at, - DROP COLUMN IF EXISTS hold_expires_at, - DROP COLUMN IF EXISTS scheduled_at; - `); - await queryRunner.query(` - ALTER TABLE freight.train_schedules - DROP COLUMN IF EXISTS train_number, - DROP COLUMN IF EXISTS direction, - DROP COLUMN IF EXISTS actual_departure_at, - DROP COLUMN IF EXISTS actual_arrival_at, - DROP COLUMN IF EXISTS prepared_by_user_id, - DROP COLUMN IF EXISTS checked_by_user_id, - DROP COLUMN IF EXISTS max_wagons; - `); - await queryRunner.query(` - ALTER TABLE freight.train_set_wagons - DROP COLUMN IF EXISTS physical_wagon_id, - DROP COLUMN IF EXISTS status; - `); - await queryRunner.query(` - ALTER TABLE freight.wagon_booking_allocations - DROP COLUMN IF EXISTS load_type, - DROP COLUMN IF EXISTS status, - DROP COLUMN IF EXISTS confirmed_at, - DROP COLUMN IF EXISTS confirmed_by_user_id; - `); - await queryRunner.query(` - ALTER TABLE freight.wagon_types - DROP COLUMN IF EXISTS equated_length_m, - DROP COLUMN IF EXISTS tare_weight_tons, - DROP COLUMN IF EXISTS supports_container, - DROP COLUMN IF EXISTS max_container_gross_t; - `); - await queryRunner.query(` - ALTER TABLE freight.wagons - DROP COLUMN IF EXISTS train_set_wagon_id, - DROP COLUMN IF EXISTS current_train_schedule_id; - `); - await queryRunner.query(` - ALTER TABLE freight.containers - DROP COLUMN IF EXISTS booking_id, - DROP COLUMN IF EXISTS wagon_booking_allocation_id, - DROP COLUMN IF EXISTS booking_container_id; - `); - await queryRunner.query(` - ALTER TABLE freight.cargoes - DROP COLUMN IF EXISTS wagon_booking_allocation_id, - DROP COLUMN IF EXISTS booking_id, - DROP COLUMN IF EXISTS load_type; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750400000000-SeedEdRWagonFleet.ts b/apps/edr-freight-api/src/migrations/1750400000000-SeedEdRWagonFleet.ts deleted file mode 100644 index 0a4e96276..000000000 --- a/apps/edr-freight-api/src/migrations/1750400000000-SeedEdRWagonFleet.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -type FleetRow = { - code: string; - name: string; - count: number; - start: number; - end: number; - capacityTons: number; - tareWeight: number; - lengthMeters: number; - supportedLoadTypes: string[]; -}; - -const FLEET: FleetRow[] = [ - { - code: 'PW2', - name: 'Box wagon', - count: 220, - start: 1, - end: 220, - capacityTons: 70, - tareWeight: 25.2, - lengthMeters: 17.066, - supportedLoadTypes: ['BULK', 'GENERAL_CARGO', 'BAGGED_CARGO', 'BOXED_CARGO'], - }, - { - code: 'CW4', - name: 'Gondola wagon covered', - count: 110, - start: 221, - end: 330, - capacityTons: 70, - tareWeight: 24.8, - lengthMeters: 13.976, - supportedLoadTypes: ['CONTAINER'], - }, - { - code: 'CW3', - name: 'Gondola wagon', - count: 20, - start: 331, - end: 350, - capacityTons: 70, - tareWeight: 23.4, - lengthMeters: 13.976, - supportedLoadTypes: ['BULK', 'COAL', 'ORE'], - }, - { - code: 'KW2', - name: 'Hopper wagon covered', - count: 20, - start: 351, - end: 370, - capacityTons: 69, - tareWeight: 25.2, - lengthMeters: 16.466, - supportedLoadTypes: ['BULK', 'GRAIN'], - }, - { - code: 'KW3', - name: 'Hopper wagon', - count: 20, - start: 371, - end: 390, - capacityTons: 70, - tareWeight: 24, - lengthMeters: 14.4, - supportedLoadTypes: ['BULK', 'COAL'], - }, - { - code: 'NW5', - name: 'Flat wagon container', - count: 550, - start: 391, - end: 940, - capacityTons: 70, - tareWeight: 0, - lengthMeters: 14, - supportedLoadTypes: ['CONTAINER'], - }, -]; - -const wagonNumber = (sequence: number) => `ER${String(sequence).padStart(4, '0')}`; - -export class SeedEdRWagonFleet1750400000000 implements MigrationInterface { - name = 'SeedEdRWagonFleet1750400000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.wagon_types - SET name = 'Flat wagon container', - capacity_tons = 70, - length_meters = 14.000, - supported_load_types = ARRAY['CONTAINER'], - max_wagons_per_train = 53, - is_active = true, - deleted_at = NULL, - updated_at = now() - WHERE code = 'NW5'; - `); - - const [defaultLocation] = await queryRunner.query(` - SELECT id - FROM freight.yards - WHERE code IN ('DJIBOUTI', 'DJIB_PORT', 'NAGAD') - OR lower(label) LIKE '%djibouti%' - ORDER BY - CASE code - WHEN 'DJIBOUTI' THEN 1 - WHEN 'DJIB_PORT' THEN 2 - WHEN 'NAGAD' THEN 3 - ELSE 4 - END, - display_order ASC - LIMIT 1; - `); - const defaultLocationYardId = defaultLocation?.id ?? null; - - for (const row of FLEET) { - await queryRunner.query( - ` - INSERT INTO freight.wagon_types ( - code, - name, - capacity_tons, - length_meters, - max_wagons_per_train, - supported_load_types, - is_active - ) - VALUES ($1, $2, $3, $4, $5, $6::text[], true) - ON CONFLICT (code) DO UPDATE SET - name = EXCLUDED.name, - capacity_tons = EXCLUDED.capacity_tons, - length_meters = EXCLUDED.length_meters, - max_wagons_per_train = EXCLUDED.max_wagons_per_train, - supported_load_types = EXCLUDED.supported_load_types, - is_active = true, - deleted_at = NULL, - updated_at = now(); - `, - [ - row.code, - row.name, - row.capacityTons, - row.lengthMeters, - row.supportedLoadTypes.includes('CONTAINER') ? 53 : 37, - row.supportedLoadTypes, - ], - ); - - const [typeRecord] = await queryRunner.query( - `SELECT id FROM freight.wagon_types WHERE code = $1 LIMIT 1;`, - [row.code], - ); - - if (!typeRecord?.id) { - throw new Error(`wagon_type_seed_failed:${row.code}`); - } - - if (row.end - row.start + 1 !== row.count) { - throw new Error(`wagon_range_mismatch:${row.code}`); - } - - for (let sequence = row.start; sequence <= row.end; sequence += 1) { - await queryRunner.query( - ` - INSERT INTO freight.wagons ( - wagon_number, - wagon_type_id, - tare_weight, - max_payload_weight, - current_location_yard_id, - status, - notes - ) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (wagon_number) DO UPDATE SET - wagon_type_id = EXCLUDED.wagon_type_id, - tare_weight = EXCLUDED.tare_weight, - max_payload_weight = EXCLUDED.max_payload_weight, - current_location_yard_id = CASE - WHEN freight.wagons.train_id IS NULL THEN EXCLUDED.current_location_yard_id - ELSE freight.wagons.current_location_yard_id - END, - status = CASE - WHEN freight.wagons.train_id IS NULL THEN EXCLUDED.status - ELSE freight.wagons.status - END, - notes = EXCLUDED.notes, - updated_at = now(); - `, - [ - wagonNumber(sequence), - typeRecord.id, - row.tareWeight, - row.capacityTons, - defaultLocationYardId, - defaultLocationYardId ? 'IMPORT_READY' : 'AVAILABLE', - `Seeded Ethio-Djibouti Railway ${row.code} fleet record.`, - ], - ); - } - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DELETE FROM freight.wagons - WHERE wagon_number BETWEEN 'ER0001' AND 'ER0940'; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750500000000-AddWagonReadiness.ts b/apps/edr-freight-api/src/migrations/1750500000000-AddWagonReadiness.ts deleted file mode 100644 index 17cc23f9a..000000000 --- a/apps/edr-freight-api/src/migrations/1750500000000-AddWagonReadiness.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddWagonReadiness1750500000000 implements MigrationInterface { - name = 'AddWagonReadiness1750500000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagons - ADD COLUMN IF NOT EXISTS readiness VARCHAR(20) NOT NULL DEFAULT 'IMPORT_READY' - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_wagons_readiness - ON freight.wagons (readiness) - WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_wagons_readiness`); - await queryRunner.query(` - ALTER TABLE freight.wagons - DROP COLUMN IF EXISTS readiness - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750600000000-AddGovernmentBookingFields.ts b/apps/edr-freight-api/src/migrations/1750600000000-AddGovernmentBookingFields.ts deleted file mode 100644 index ce833e4ac..000000000 --- a/apps/edr-freight-api/src/migrations/1750600000000-AddGovernmentBookingFields.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddGovernmentBookingFields1750600000000 implements MigrationInterface { - name = 'AddGovernmentBookingFields1750600000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS is_government BOOLEAN NOT NULL DEFAULT false - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS government_institution VARCHAR(255) NULL - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - ALTER COLUMN company_id DROP NOT NULL - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_bookings_is_government - ON freight.bookings (is_government) - WHERE is_government = true AND deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_is_government`); - await queryRunner.query(` - UPDATE freight.bookings - SET company_id = '00000000-0000-0000-0000-000000000000' - WHERE company_id IS NULL - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - ALTER COLUMN company_id SET NOT NULL - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS government_institution - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS is_government - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750700000000-CreateSchedulingEvents.ts b/apps/edr-freight-api/src/migrations/1750700000000-CreateSchedulingEvents.ts deleted file mode 100644 index 9f92f70b4..000000000 --- a/apps/edr-freight-api/src/migrations/1750700000000-CreateSchedulingEvents.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class CreateSchedulingEvents1750700000000 implements MigrationInterface { - name = 'CreateSchedulingEvents1750700000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.scheduling_events ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - train_schedule_id UUID NOT NULL, - trigger VARCHAR(40) NOT NULL, - actor_user_id UUID NULL, - reason TEXT NULL, - plan_snapshot JSONB NOT NULL DEFAULT '{}', - displaced_booking_ids JSONB NOT NULL DEFAULT '[]', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - deleted_at TIMESTAMPTZ NULL - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_scheduling_events_train_schedule_id - ON freight.scheduling_events (train_schedule_id) - WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_scheduling_events_train_schedule_id`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.scheduling_events`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750800000000-FixContainerWagonsPerUnit.ts b/apps/edr-freight-api/src/migrations/1750800000000-FixContainerWagonsPerUnit.ts deleted file mode 100644 index 1bb1a9518..000000000 --- a/apps/edr-freight-api/src/migrations/1750800000000-FixContainerWagonsPerUnit.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** 20ft = 0.5 wagon slots (2 per wagon); 40ft = 1.0 wagon slot (1 per wagon). */ -export class FixContainerWagonsPerUnit1750800000000 implements MigrationInterface { - name = 'FixContainerWagonsPerUnit1750800000000'; - - public async up(queryRunner: QueryRunner): Promise { - const hasContainerTypes = await queryRunner.hasTable('freight.container_types'); - if (!hasContainerTypes) { - return; - } - - await queryRunner.query(` - UPDATE freight.container_types - SET wagons_per_unit = 0.50 - WHERE size_ft = 20 OR code LIKE '20%'; - `); - await queryRunner.query(` - UPDATE freight.container_types - SET wagons_per_unit = 1.00 - WHERE size_ft = 40 OR code LIKE '40%'; - `); - - const hasBookingContainer = await queryRunner.hasTable('freight.booking_container'); - if (!hasBookingContainer) { - return; - } - - await queryRunner.query(` - UPDATE freight.booking_container bc - SET wagons_required = CEILING(bc.quantity * ct.wagons_per_unit) - FROM freight.container_types ct - WHERE ct.id = bc.container_type_id; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - const hasContainerTypes = await queryRunner.hasTable('freight.container_types'); - if (!hasContainerTypes) { - return; - } - - await queryRunner.query(` - UPDATE freight.container_types SET wagons_per_unit = 1.00; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1750900000000-AddContainerNumberToBookingContainer.ts b/apps/edr-freight-api/src/migrations/1750900000000-AddContainerNumberToBookingContainer.ts deleted file mode 100644 index eb28da8f3..000000000 --- a/apps/edr-freight-api/src/migrations/1750900000000-AddContainerNumberToBookingContainer.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddContainerNumberToBookingContainer1750900000000 implements MigrationInterface { - name = "AddContainerNumberToBookingContainer1750900000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_container - ALTER COLUMN container_type_id DROP NOT NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.booking_container - ADD COLUMN container_number varchar(64); - `); - - await queryRunner.query(` - ALTER TABLE freight.wagon_allocation_container_items - ALTER COLUMN container_type_id DROP NOT NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagon_allocation_container_items - ALTER COLUMN container_type_id SET NOT NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.booking_container - DROP COLUMN container_number; - `); - - await queryRunner.query(` - ALTER TABLE freight.booking_container - ALTER COLUMN container_type_id SET NOT NULL; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1751000000000-CreateTrainSchedulingGlobalRules.ts b/apps/edr-freight-api/src/migrations/1751000000000-CreateTrainSchedulingGlobalRules.ts deleted file mode 100644 index ac9741c5a..000000000 --- a/apps/edr-freight-api/src/migrations/1751000000000-CreateTrainSchedulingGlobalRules.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class CreateTrainSchedulingGlobalRules1751000000000 implements MigrationInterface { - name = "CreateTrainSchedulingGlobalRules1751000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE freight.train_scheduling_global_rules ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - max_train_length_meters numeric(10, 2) NOT NULL DEFAULT 760, - max_train_weight_tons numeric(10, 3) NOT NULL DEFAULT 3500, - max_wagons_per_train integer NOT NULL DEFAULT 53, - max_20ft_container_weight_tons numeric(8, 3) NOT NULL DEFAULT 30, - max_20ft_pair_weight_diff_tons numeric(8, 3) NOT NULL DEFAULT 10, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL - ); - `); - - await queryRunner.query(` - INSERT INTO freight.train_scheduling_global_rules ( - max_train_length_meters, - max_train_weight_tons, - max_wagons_per_train, - max_20ft_container_weight_tons, - max_20ft_pair_weight_diff_tons - ) VALUES (760, 3500, 53, 30, 10); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.train_scheduling_global_rules;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1751000000001-AddDeletedAtToTrainSchedulingGlobalRules.ts b/apps/edr-freight-api/src/migrations/1751000000001-AddDeletedAtToTrainSchedulingGlobalRules.ts deleted file mode 100644 index fd72f6c3b..000000000 --- a/apps/edr-freight-api/src/migrations/1751000000001-AddDeletedAtToTrainSchedulingGlobalRules.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddDeletedAtToTrainSchedulingGlobalRules1751000000001 - implements MigrationInterface -{ - name = "AddDeletedAtToTrainSchedulingGlobalRules1751000000001"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - ADD COLUMN IF NOT EXISTS deleted_at timestamptz NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - DROP COLUMN IF EXISTS deleted_at; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1752000000000-CreateCompanyProfiles.ts b/apps/edr-freight-api/src/migrations/1752000000000-CreateCompanyProfiles.ts deleted file mode 100644 index a27cef3ad..000000000 --- a/apps/edr-freight-api/src/migrations/1752000000000-CreateCompanyProfiles.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - MigrationInterface, - QueryRunner, - Table, - TableIndex, - TableForeignKey, -} from "typeorm"; - -export class CreateCompanyProfiles1752000000000 implements MigrationInterface { - name = "CreateCompanyProfiles1752000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: "freight", - name: "company_profiles", - columns: [ - { - name: "id", - type: "uuid", - isPrimary: true, - generationStrategy: "uuid", - default: "gen_random_uuid()", - }, - { name: "company_id", type: "uuid" }, - { name: "type", type: "varchar", length: "32" }, - { name: "reference", type: "varchar", length: "20", isUnique: true }, - { - name: "status", - type: "varchar", - length: "32", - default: "'active'", - }, - { - name: "business_license", - type: "varchar", - length: "100", - isNullable: true, - }, - { name: "attributes", type: "jsonb", isNullable: true }, - { name: "created_at", type: "timestamptz", default: "now()" }, - { name: "updated_at", type: "timestamptz", default: "now()" }, - { name: "deleted_at", type: "timestamptz", isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createForeignKey( - "freight.company_profiles", - new TableForeignKey({ - columnNames: ["company_id"], - referencedTableName: "companies", - referencedSchema: "freight", - referencedColumnNames: ["id"], - }), - ); - - await queryRunner.createIndex( - "freight.company_profiles", - new TableIndex({ columnNames: ["company_id"] }), - ); - await queryRunner.createIndex( - "freight.company_profiles", - new TableIndex({ columnNames: ["type"] }), - ); - - await queryRunner.query( - `CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_ex START WITH 1`, - ); - await queryRunner.query( - `CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_im START WITH 1`, - ); - await queryRunner.query( - `CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_ffe START WITH 1`, - ); - await queryRunner.query( - `CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_fwj START WITH 1`, - ); - await queryRunner.query( - `CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_tr START WITH 1`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable("freight.company_profiles"); - await queryRunner.query( - `DROP SEQUENCE IF EXISTS freight.seq_company_profile_ex`, - ); - await queryRunner.query( - `DROP SEQUENCE IF EXISTS freight.seq_company_profile_im`, - ); - await queryRunner.query( - `DROP SEQUENCE IF EXISTS freight.seq_company_profile_ffe`, - ); - await queryRunner.query( - `DROP SEQUENCE IF EXISTS freight.seq_company_profile_fwj`, - ); - await queryRunner.query( - `DROP SEQUENCE IF EXISTS freight.seq_company_profile_tr`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1752000000001-MoveBusinessLicenseToProfile.ts b/apps/edr-freight-api/src/migrations/1752000000001-MoveBusinessLicenseToProfile.ts deleted file mode 100644 index f15996773..000000000 --- a/apps/edr-freight-api/src/migrations/1752000000001-MoveBusinessLicenseToProfile.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class MoveBusinessLicenseToProfile1752000000001 - implements MigrationInterface -{ - name = 'MoveBusinessLicenseToProfile1752000000001'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.company_profiles cp - SET business_license = c.business_license - FROM freight.companies c - WHERE cp.company_id = c.id AND c.business_license IS NOT NULL - `); - - await queryRunner.query( - `ALTER TABLE freight.companies DROP COLUMN IF EXISTS business_license`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.companies ADD COLUMN business_license varchar(100) NULL`, - ); - - await queryRunner.query(` - UPDATE freight.companies c - SET business_license = cp.business_license - FROM ( - SELECT DISTINCT ON (cp2.company_id) - cp2.company_id, cp2.business_license - FROM freight.company_profiles cp2 - WHERE cp2.business_license IS NOT NULL - ORDER BY cp2.company_id, cp2.created_at - ) cp - WHERE cp.company_id = c.id - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1770000000000-CreateVehiclesTable.ts b/apps/edr-freight-api/src/migrations/1770000000000-CreateVehiclesTable.ts deleted file mode 100644 index 2cf09c9f5..000000000 --- a/apps/edr-freight-api/src/migrations/1770000000000-CreateVehiclesTable.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class CreateVehiclesTable1770000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'vehicles' AND table_schema = 'freight') THEN - CREATE TABLE freight.vehicles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - plate_number VARCHAR NOT NULL UNIQUE, - registration_number VARCHAR NOT NULL UNIQUE, - vehicle_type VARCHAR NOT NULL, - manufacturer VARCHAR NOT NULL, - model VARCHAR NOT NULL, - year INTEGER NOT NULL, - fuel_type VARCHAR NOT NULL, - capacity NUMERIC NOT NULL, - status VARCHAR DEFAULT 'ACTIVE' NOT NULL, - description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - deleted_at TIMESTAMP NULL - ); - - CREATE INDEX idx_vehicles_plate_number ON freight.vehicles(plate_number); - CREATE INDEX idx_vehicles_registration_number ON freight.vehicles(registration_number); - CREATE INDEX idx_vehicles_status ON freight.vehicles(status); - CREATE INDEX idx_vehicles_vehicle_type ON freight.vehicles(vehicle_type); - CREATE INDEX idx_vehicles_manufacturer ON freight.vehicles(manufacturer); - END IF; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.vehicles CASCADE;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1775000000000-CreateDriversTable.ts b/apps/edr-freight-api/src/migrations/1775000000000-CreateDriversTable.ts deleted file mode 100644 index 8f41587cf..000000000 --- a/apps/edr-freight-api/src/migrations/1775000000000-CreateDriversTable.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class CreateDriversTable1775000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'drivers' AND table_schema = 'freight') THEN - CREATE TABLE freight.drivers ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - license_number VARCHAR NOT NULL UNIQUE, - first_name VARCHAR NOT NULL, - last_name VARCHAR NOT NULL, - email VARCHAR NOT NULL UNIQUE, - phone_number VARCHAR NOT NULL UNIQUE, - date_of_birth DATE NOT NULL, - license_expiry_date DATE NOT NULL, - status VARCHAR DEFAULT 'ACTIVE' NOT NULL, - vehicle_types_authorized VARCHAR[], - address TEXT, - emergency_contact VARCHAR, - notes TEXT, - total_trips INTEGER DEFAULT 0, - rating NUMERIC(3, 2), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - deleted_at TIMESTAMP NULL - ); - - CREATE INDEX idx_drivers_license_number ON freight.drivers(license_number); - CREATE INDEX idx_drivers_email ON freight.drivers(email); - CREATE INDEX idx_drivers_phone_number ON freight.drivers(phone_number); - CREATE INDEX idx_drivers_status ON freight.drivers(status); - END IF; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.drivers CASCADE;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts b/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts deleted file mode 100644 index 0382c834b..000000000 --- a/apps/edr-freight-api/src/migrations/1780639311366-CreatePaymentTable.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class CreatePaymentTable1780639311366 implements MigrationInterface { - name = "CreatePaymentTable1780639311366"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TYPE freight.payments_type_enum AS ENUM ('booking'); - `); - - await queryRunner.query(` - CREATE TYPE freight.payments_method_enum AS ENUM ('telebirr', 'cbe-birr', 'ebirr'); - `); - - await queryRunner.query(` - CREATE TYPE freight.payments_currency_enum AS ENUM ('ETB', 'USD'); - `); - - await queryRunner.query(` - CREATE TYPE freight.payments_status_enum AS ENUM ( - 'action-required', - 'processing', - 'success', - 'failed', - 'canceled', - 'refunded' - ); - `); - - await queryRunner.query(` - CREATE TABLE freight.payments ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - - ref_id varchar(255) NOT NULL, - - type freight.payments_type_enum NOT NULL, - - method freight.payments_method_enum NOT NULL, - - currency freight.payments_currency_enum NOT NULL, - - amount numeric NOT NULL, - - raw_initiation jsonb NOT NULL DEFAULT '{}'::jsonb, - - client_action json, - - merchant_order_id varchar(255) NOT NULL, - - transaction_id varchar(255), - - status freight.payments_status_enum NOT NULL DEFAULT 'action-required', - - paid_at date, - - refunded_at date, - - expires_at date, - - failer_code varchar(30), - - failer_message varchar(255), - - reason varchar(255), - - created_at TIMESTAMP NOT NULL DEFAULT now(), - - CONSTRAINT PK_payments PRIMARY KEY (id), - - CONSTRAINT UQ_payments_merchant_order_id UNIQUE (merchant_order_id), - - CONSTRAINT UQ_payments_transaction_id UNIQUE (transaction_id) - ); -`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DROP TABLE IF EXISTS freight.payments; - `); - - await queryRunner.query(` - DROP TYPE IF EXISTS freight.payments_status_enum; - `); - - await queryRunner.query(` - DROP TYPE IF EXISTS freight.payments_currency_enum; - `); - - await queryRunner.query(` - DROP TYPE IF EXISTS freight.payments_method_enum; - `); - - await queryRunner.query(` - DROP TYPE IF EXISTS freight.payments_type_enum; - `); - } -} \ No newline at end of file diff --git a/apps/edr-freight-api/src/migrations/1780639978834-AlterClientActionToJsonb.ts b/apps/edr-freight-api/src/migrations/1780639978834-AlterClientActionToJsonb.ts deleted file mode 100644 index 723331ab3..000000000 --- a/apps/edr-freight-api/src/migrations/1780639978834-AlterClientActionToJsonb.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AlterClientActionToJsonb1780639978834 implements MigrationInterface { - name = "AlterClientActionToJsonb1780639978834"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.payments - ALTER COLUMN client_action TYPE jsonb - USING client_action::jsonb; - `); - - await queryRunner.query(` - ALTER TABLE freight.payments - ALTER COLUMN client_action DROP DEFAULT; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.payments - ALTER COLUMN client_action TYPE json - USING client_action::json; - `); - } - - -} diff --git a/apps/edr-freight-api/src/migrations/1780644945086-UpdatePaymentTimestamp.ts b/apps/edr-freight-api/src/migrations/1780644945086-UpdatePaymentTimestamp.ts deleted file mode 100644 index 6cfc7fc8f..000000000 --- a/apps/edr-freight-api/src/migrations/1780644945086-UpdatePaymentTimestamp.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class UpdatePaymentTimestamp1780644945086 implements MigrationInterface { - name = "UpdatePaymentTimestamp1780644945086"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.payments - ALTER COLUMN refunded_at TYPE timestamp - USING refunded_at::timestamp; - `); - - await queryRunner.query(` - ALTER TABLE freight.payments - ALTER COLUMN expires_at TYPE timestamp - USING expires_at::timestamp; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.payments - ALTER COLUMN refunded_at TYPE timestamptz - USING refunded_at::timestamptz; - `); - - await queryRunner.query(` - ALTER TABLE freight.payments - ALTER COLUMN expires_at TYPE timestamptz - USING expires_at::timestamptz; - `); - } -} \ No newline at end of file diff --git a/apps/edr-freight-api/src/migrations/1781000000000-AddLocomotiveReadiness.ts b/apps/edr-freight-api/src/migrations/1781000000000-AddLocomotiveReadiness.ts deleted file mode 100644 index f6d87f41d..000000000 --- a/apps/edr-freight-api/src/migrations/1781000000000-AddLocomotiveReadiness.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddLocomotiveReadiness1781000000000 implements MigrationInterface { - name = 'AddLocomotiveReadiness1781000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.locomotives - ADD COLUMN IF NOT EXISTS readiness VARCHAR(20) NOT NULL DEFAULT 'IMPORT_READY' - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_locomotives_readiness - ON freight.locomotives (readiness) - WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_locomotives_readiness`); - await queryRunner.query(` - ALTER TABLE freight.locomotives - DROP COLUMN IF EXISTS readiness - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1781000000001-CreateTrainCheckpointEvents.ts b/apps/edr-freight-api/src/migrations/1781000000001-CreateTrainCheckpointEvents.ts deleted file mode 100644 index 7d9ce745a..000000000 --- a/apps/edr-freight-api/src/migrations/1781000000001-CreateTrainCheckpointEvents.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class CreateTrainCheckpointEvents1781000000001 implements MigrationInterface { - name = 'CreateTrainCheckpointEvents1781000000001'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.train_checkpoint_events ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - train_schedule_id UUID NOT NULL REFERENCES freight.train_schedules(id) ON DELETE CASCADE, - yard_id UUID NOT NULL, - sequence_no INT NOT NULL, - kind VARCHAR(20) NOT NULL, - occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - note TEXT NULL, - recorded_by_user_id UUID NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - deleted_at TIMESTAMPTZ NULL - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_train_checkpoint_events_schedule - ON freight.train_checkpoint_events (train_schedule_id, sequence_no) - WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight.idx_train_checkpoint_events_schedule`, - ); - await queryRunner.query(`DROP TABLE IF EXISTS freight.train_checkpoint_events`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1781000000002-AddBatchBookingFields.ts b/apps/edr-freight-api/src/migrations/1781000000002-AddBatchBookingFields.ts deleted file mode 100644 index 0287b55be..000000000 --- a/apps/edr-freight-api/src/migrations/1781000000002-AddBatchBookingFields.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddBatchBookingFields1781000000002 implements MigrationInterface { - name = 'AddBatchBookingFields1781000000002'; - - public async up(queryRunner: QueryRunner): Promise { - // Booking → target schedule (pool membership) + 1h pay-window deadline. - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS train_schedule_id UUID NULL, - ADD COLUMN IF NOT EXISTS payment_deadline TIMESTAMPTZ NULL - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_bookings_train_schedule_id - ON freight.bookings (train_schedule_id) - WHERE deleted_at IS NULL - `); - - // TrainSchedule → booking-window status (OPEN/FULL/CLOSED). - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS booking_window_status VARCHAR(10) NOT NULL DEFAULT 'OPEN' - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_train_schedules_booking_window_status - ON freight.train_schedules (booking_window_status) - WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight.idx_train_schedules_booking_window_status`, - ); - await queryRunner.query( - `ALTER TABLE freight.train_schedules DROP COLUMN IF EXISTS booking_window_status`, - ); - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_train_schedule_id`); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS train_schedule_id, - DROP COLUMN IF EXISTS payment_deadline - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1781000000003-AddSelectedForBatchStatus.ts b/apps/edr-freight-api/src/migrations/1781000000003-AddSelectedForBatchStatus.ts deleted file mode 100644 index 1ba9670b2..000000000 --- a/apps/edr-freight-api/src/migrations/1781000000003-AddSelectedForBatchStatus.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddSelectedForBatchStatus1781000000003 implements MigrationInterface { - name = 'AddSelectedForBatchStatus1781000000003'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS selected_for_batch_at TIMESTAMPTZ NULL - `); - - await queryRunner.query(` - UPDATE freight.bookings - SET - status = 'SELECTED_FOR_BATCH', - selected_for_batch_at = COALESCE( - payment_deadline - INTERVAL '5 minutes', - updated_at - ) - WHERE status = 'AWAITING_PAYMENT' - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.bookings - SET status = 'AWAITING_PAYMENT' - WHERE status = 'SELECTED_FOR_BATCH' - `); - - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS selected_for_batch_at - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1781000000004-AddDomesticWeightLimitTradeDirection.ts b/apps/edr-freight-api/src/migrations/1781000000004-AddDomesticWeightLimitTradeDirection.ts deleted file mode 100644 index 59b1de22e..000000000 --- a/apps/edr-freight-api/src/migrations/1781000000004-AddDomesticWeightLimitTradeDirection.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Allow DOMESTIC trade direction on weight_limit_rules (domestic corridor bookings). - */ -export class AddDomesticWeightLimitTradeDirection1781000000004 - implements MigrationInterface -{ - name = 'AddDomesticWeightLimitTradeDirection1781000000004'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DO $$ BEGIN - ALTER TYPE freight.weight_limit_rules_trade_direction_enum ADD VALUE 'DOMESTIC'; - EXCEPTION - WHEN duplicate_object THEN NULL; - WHEN undefined_object THEN - BEGIN - ALTER TYPE weight_limit_rules_trade_direction_enum ADD VALUE 'DOMESTIC'; - EXCEPTION - WHEN duplicate_object THEN NULL; - END; - END $$; - `); - } - - public async down(_queryRunner: QueryRunner): Promise { - // PostgreSQL does not support removing enum values safely. - } -} diff --git a/apps/edr-freight-api/src/migrations/1781000000005-CreateTrainCompositionRemovalLog.ts b/apps/edr-freight-api/src/migrations/1781000000005-CreateTrainCompositionRemovalLog.ts deleted file mode 100644 index 475a52574..000000000 --- a/apps/edr-freight-api/src/migrations/1781000000005-CreateTrainCompositionRemovalLog.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -export class CreateTrainCompositionRemovalLog1781000000005 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'train_composition_removal_logs', - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'uuid_generate_v4()', - }, - { - name: 'schedule_id', - type: 'uuid', - isNullable: false, - }, - { - name: 'booking_id', - type: 'uuid', - isNullable: false, - }, - { - name: 'booking_reference', - type: 'varchar', - length: '64', - isNullable: true, - }, - { - name: 'removed_by_user_id', - type: 'uuid', - isNullable: true, - }, - { - name: 'removed_at', - type: 'timestamptz', - default: 'NOW()', - isNullable: false, - }, - { - name: 'notes', - type: 'text', - isNullable: true, - }, - { - name: 'created_at', - type: 'timestamptz', - default: 'NOW()', - isNullable: false, - }, - { - name: 'updated_at', - type: 'timestamptz', - default: 'NOW()', - isNullable: false, - }, - { - name: 'deleted_at', - type: 'timestamptz', - isNullable: true, - }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.train_composition_removal_logs', - new TableIndex({ - columnNames: ['schedule_id'], - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.train_composition_removal_logs', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1782000000000-WagonLocomotiveYardLink.ts b/apps/edr-freight-api/src/migrations/1782000000000-WagonLocomotiveYardLink.ts deleted file mode 100644 index 68100f367..000000000 --- a/apps/edr-freight-api/src/migrations/1782000000000-WagonLocomotiveYardLink.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class WagonLocomotiveYardLink1782000000000 implements MigrationInterface { - name = 'WagonLocomotiveYardLink1782000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagons - ADD COLUMN IF NOT EXISTS "current_yard_id" UUID NULL; - `); - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'FK_wagon_current_yard' - ) THEN - ALTER TABLE freight.wagons - ADD CONSTRAINT "FK_wagon_current_yard" - FOREIGN KEY ("current_yard_id") REFERENCES freight.yards(id) ON DELETE SET NULL; - END IF; - END $$; - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_wagon_current_yard_id" - ON freight.wagons ("current_yard_id"); - `); - - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_wagons_readiness`); - await queryRunner.query(`ALTER TABLE freight.wagons DROP COLUMN IF EXISTS readiness;`); - - await queryRunner.query(` - ALTER TABLE freight.locomotives - ADD COLUMN IF NOT EXISTS "current_yard_id" UUID NULL; - `); - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'FK_locomotive_current_yard' - ) THEN - ALTER TABLE freight.locomotives - ADD CONSTRAINT "FK_locomotive_current_yard" - FOREIGN KEY ("current_yard_id") REFERENCES freight.yards(id) ON DELETE SET NULL; - END IF; - END $$; - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_locomotive_current_yard_id" - ON freight.locomotives ("current_yard_id"); - `); - - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_locomotives_readiness`); - await queryRunner.query(`ALTER TABLE freight.locomotives DROP COLUMN IF EXISTS readiness;`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagons - ADD COLUMN IF NOT EXISTS readiness VARCHAR(20) NOT NULL DEFAULT 'IMPORT_READY'; - `); - await queryRunner.query(` - ALTER TABLE freight.locomotives - ADD COLUMN IF NOT EXISTS readiness VARCHAR(20) NOT NULL DEFAULT 'IMPORT_READY'; - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_wagons_readiness - ON freight.wagons (readiness) - WHERE deleted_at IS NULL; - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_locomotives_readiness - ON freight.locomotives (readiness) - WHERE deleted_at IS NULL; - `); - - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_wagon_current_yard_id"`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_locomotive_current_yard_id"`); - await queryRunner.query(` - ALTER TABLE freight.wagons DROP CONSTRAINT IF EXISTS "FK_wagon_current_yard"; - `); - await queryRunner.query(` - ALTER TABLE freight.locomotives DROP CONSTRAINT IF EXISTS "FK_locomotive_current_yard"; - `); - await queryRunner.query(`ALTER TABLE freight.wagons DROP COLUMN IF EXISTS "current_yard_id";`); - await queryRunner.query(`ALTER TABLE freight.locomotives DROP COLUMN IF EXISTS "current_yard_id";`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1782000000001-AddPaymentWebhookEventAndRefund.ts b/apps/edr-freight-api/src/migrations/1782000000001-AddPaymentWebhookEventAndRefund.ts deleted file mode 100644 index e1cddc05b..000000000 --- a/apps/edr-freight-api/src/migrations/1782000000001-AddPaymentWebhookEventAndRefund.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddPaymentWebhookEventAndRefund1782000000001 implements MigrationInterface { - name = "AddPaymentWebhookEventAndRefund1782000000001"; - - public async up(queryRunner: QueryRunner): Promise { - // Enum for webhook provider — shares the same values as payments_method_enum - // but is a separate type so both tables remain independently evolvable. - await queryRunner.query(` - CREATE TYPE freight.payment_webhook_method_enum AS ENUM ('telebirr', 'cbe-birr', 'ebirr'); - `); - - await queryRunner.query(` - CREATE TABLE freight.payment_webhook_events ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - provider freight.payment_webhook_method_enum NOT NULL, - external_event_id varchar(255) NOT NULL, - merchant_order_id varchar(255), - provider_txn_id varchar(255), - signature_valid boolean NOT NULL, - status varchar(100) NOT NULL, - payload jsonb NOT NULL, - received_at TIMESTAMP NOT NULL DEFAULT now(), - processed_at TIMESTAMP, - processing_error text, - - CONSTRAINT PK_payment_webhook_events PRIMARY KEY (id), - CONSTRAINT UQ_payment_webhook_events_provider_event UNIQUE (provider, external_event_id) - ); - `); - - await queryRunner.query(` - CREATE INDEX IDX_payment_webhook_events_merchant_order_id - ON freight.payment_webhook_events (merchant_order_id); - `); - - await queryRunner.query(` - CREATE TABLE freight.payment_refunds ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - payment_id uuid NOT NULL, - amount_minor int NOT NULL, - reason varchar(255), - provider_refund_id varchar(255), - status varchar(50) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT now(), - - CONSTRAINT PK_payment_refunds PRIMARY KEY (id), - CONSTRAINT FK_payment_refunds_payment - FOREIGN KEY (payment_id) - REFERENCES freight.payments (id) - ON DELETE RESTRICT - ); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.payment_refunds;`); - await queryRunner.query(`DROP INDEX IF EXISTS freight.IDX_payment_webhook_events_merchant_order_id;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.payment_webhook_events;`); - await queryRunner.query(`DROP TYPE IF EXISTS freight.payment_webhook_method_enum;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1782000000002-ExtendPaymentMethodEnum.ts b/apps/edr-freight-api/src/migrations/1782000000002-ExtendPaymentMethodEnum.ts deleted file mode 100644 index 607926e92..000000000 --- a/apps/edr-freight-api/src/migrations/1782000000002-ExtendPaymentMethodEnum.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class ExtendPaymentMethodEnum1782000000002 implements MigrationInterface { - name = "ExtendPaymentMethodEnum1782000000002"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TYPE freight.payments_method_enum ADD VALUE IF NOT EXISTS 'waafi';`); - await queryRunner.query(`ALTER TYPE freight.payments_method_enum ADD VALUE IF NOT EXISTS 'card';`); - await queryRunner.query(`ALTER TYPE freight.payments_method_enum ADD VALUE IF NOT EXISTS 'dmoney';`); - } - - public async down(_queryRunner: QueryRunner): Promise { - // PostgreSQL does not support removing enum values directly. - // To roll back, recreate the type without the added values and update the column. - } -} diff --git a/apps/edr-freight-api/src/migrations/1783000000000-ReplacePriorityRulesWithPriorityConfigs.ts b/apps/edr-freight-api/src/migrations/1783000000000-ReplacePriorityRulesWithPriorityConfigs.ts deleted file mode 100644 index b8eccbbaf..000000000 --- a/apps/edr-freight-api/src/migrations/1783000000000-ReplacePriorityRulesWithPriorityConfigs.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class ReplacePriorityRulesWithPriorityConfigs1783000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE freight.priority_configs ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - type VARCHAR(20) NOT NULL CHECK (type IN ('WAGON', 'CURRENCY')), - label VARCHAR(100) NOT NULL, - currency VARCHAR(5) NULL, - min_wagon_count INT NOT NULL, - max_wagon_count INT NOT NULL, - score_points INT NOT NULL DEFAULT 0, - is_active BOOLEAN NOT NULL DEFAULT false, - display_order INT NOT NULL DEFAULT 1, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT chk_wagon_range CHECK (min_wagon_count <= max_wagon_count), - CONSTRAINT chk_currency_for_type CHECK ( - (type = 'WAGON' AND currency IS NULL) OR - (type = 'CURRENCY' AND currency IS NOT NULL) - ) - ); - `); - - await queryRunner.query(` - CREATE INDEX idx_priority_configs_type_active ON freight.priority_configs (type, is_active); - `); - - await queryRunner.query(` - CREATE INDEX idx_priority_configs_currency_type ON freight.priority_configs (currency, type); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.priority_configs;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1784000000000-CreateSavedSignatures.ts b/apps/edr-freight-api/src/migrations/1784000000000-CreateSavedSignatures.ts deleted file mode 100644 index e0c36f67b..000000000 --- a/apps/edr-freight-api/src/migrations/1784000000000-CreateSavedSignatures.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class CreateSavedSignatures1784000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE freight.saved_signatures ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID NOT NULL, - signer_display_name VARCHAR(200) NOT NULL, - signature_file_id UUID NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT uq_saved_signatures_user_id UNIQUE (user_id) - ); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.saved_signatures;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1784000000001-SeedWagonsWithYardAssignment.ts b/apps/edr-freight-api/src/migrations/1784000000001-SeedWagonsWithYardAssignment.ts deleted file mode 100644 index a615fe9a4..000000000 --- a/apps/edr-freight-api/src/migrations/1784000000001-SeedWagonsWithYardAssignment.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Full wagon re-seed — runs in this order: - * - * 1. DELETE all existing wagons (hard delete, not soft). - * 2. UPSERT all 10 standard wagon types so they are guaranteed to exist. - * 3. INSERT 50 wagons per wagon type (500 total), distributed evenly across - * the 5 main operational yards (10 wagons per yard per type): - * - * KALITY — Kality Rail Terminal - * MOJO — Mojo Dry Port - * DIRE_DAWA — Dire Dawa Yard - * DJIB_PORT — Djibouti Port Terminal - * NAGAD — Nagad Terminal, Djibouti - * - * Wagon numbers follow the pattern -NNNN (e.g. NW5-0001 … NW5-0050). - * Yard IDs are fetched live from freight.yards so the migration is safe across - * all environments regardless of UUID values. - */ -export class SeedWagonsWithYardAssignment1784000000001 - implements MigrationInterface -{ - name = 'SeedWagonsWithYardAssignment1784000000001'; - - public async up(queryRunner: QueryRunner): Promise { - // ── STEP 1: Remove all wagons ────────────────────────────────────────── - await queryRunner.query(`DELETE FROM freight.wagons;`); - - // ── STEP 2: Ensure all 10 wagon types exist ──────────────────────────── - await queryRunner.query(` - INSERT INTO freight.wagon_types ( - code, - name, - capacity_tons, - length_meters, - max_wagons_per_train, - supported_load_types, - is_active, - tare_weight_tons - ) - VALUES - ('NW7', 'Double deck sedan wagon', 22, 26.066, NULL, ARRAY['vehicles', 'sedan'], true, 18.0), - ('NW5', 'Flat wagon (container)', 70, 14.000, 53, ARRAY['container', 'steel', 'machinery'], true, 22.0), - ('PW2', 'Box wagon', 70, 17.066, 18, ARRAY['general cargo', 'break bulk'], true, 20.0), - ('GW2', 'Tank wagon', 70, 12.228, 37, ARRAY['liquid', 'fuel'], true, 25.0), - ('CW4', 'Gondola covered wagon', 70, 13.976, 37, ARRAY['covered bulk cargo'], true, 22.0), - ('CW3', 'Gondola open wagon', 70, 13.976, NULL, ARRAY['open bulk cargo'], true, 20.0), - ('KW2', 'Hopper covered wagon', 69, 16.466, NULL, ARRAY['bulk grains'], true, 22.0), - ('KW3', 'Hopper open wagon', 70, 14.400, NULL, ARRAY['coal', 'bulk cargo'], true, 20.0), - ('NW6', 'Flat wagon (long cargo)', 70, 18.560, NULL, ARRAY['long cargo'], true, 22.0), - ('BW1', 'Refrigerated wagon', 38, 21.996, NULL, ARRAY['refrigerated cargo'], true, 24.0) - ON CONFLICT (code) DO UPDATE SET - name = EXCLUDED.name, - capacity_tons = EXCLUDED.capacity_tons, - length_meters = EXCLUDED.length_meters, - max_wagons_per_train = EXCLUDED.max_wagons_per_train, - supported_load_types = EXCLUDED.supported_load_types, - is_active = true, - tare_weight_tons = EXCLUDED.tare_weight_tons, - deleted_at = NULL, - updated_at = now(); - `); - - // ── STEP 3: Seed 50 wagons per type across 5 yards ──────────────────── - await queryRunner.query(` - DO $$ - DECLARE - wt RECORD; - yard_kality UUID; - yard_mojo UUID; - yard_dire_dawa UUID; - yard_djib_port UUID; - yard_nagad UUID; - yards UUID[]; - i INT; - yard_id UUID; - wagon_num TEXT; - v_tare NUMERIC; - v_payload NUMERIC; - BEGIN - -- Fetch yard IDs by code (safe across envs — UUIDs differ per DB) - SELECT id INTO yard_kality FROM freight.yards WHERE code = 'KALITY' LIMIT 1; - SELECT id INTO yard_mojo FROM freight.yards WHERE code = 'MOJO' LIMIT 1; - SELECT id INTO yard_dire_dawa FROM freight.yards WHERE code = 'DIRE_DAWA' LIMIT 1; - SELECT id INTO yard_djib_port FROM freight.yards WHERE code = 'DJIB_PORT' LIMIT 1; - SELECT id INTO yard_nagad FROM freight.yards WHERE code = 'NAGAD' LIMIT 1; - - IF yard_kality IS NULL OR yard_mojo IS NULL OR yard_dire_dawa IS NULL - OR yard_djib_port IS NULL OR yard_nagad IS NULL - THEN - RAISE EXCEPTION 'One or more operational yards not found. Run the yards seed first.'; - END IF; - - yards := ARRAY[ - yard_kality, - yard_mojo, - yard_dire_dawa, - yard_djib_port, - yard_nagad - ]; - - FOR wt IN - SELECT id, code, capacity_tons, tare_weight_tons - FROM freight.wagon_types - WHERE is_active = true - ORDER BY code - LOOP - v_tare := COALESCE(wt.tare_weight_tons, 20.0); - v_payload := COALESCE(wt.capacity_tons, 60.0); - - FOR i IN 1 .. 50 LOOP - wagon_num := wt.code || '-' || LPAD(i::TEXT, 4, '0'); - yard_id := yards[ ((i - 1) % 5) + 1 ]; -- round-robin: 1→K, 2→M, 3→D, 4→J, 5→N, 6→K … - - INSERT INTO freight.wagons ( - id, - wagon_number, - wagon_type_id, - tare_weight, - max_payload_weight, - status, - current_yard_id, - train_id, - sequence_number, - notes, - train_set_wagon_id, - current_train_schedule_id, - created_at, - updated_at - ) - VALUES ( - uuid_generate_v4(), - wagon_num, - wt.id, - v_tare, - v_payload, - 'Available', - yard_id, - NULL, NULL, NULL, NULL, NULL, - now(), now() - ) - ON CONFLICT (wagon_number) DO NOTHING; - END LOOP; - - RAISE NOTICE 'Seeded 50 wagons for type %.', wt.code; - END LOOP; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - // Remove all seeded wagons (full wipe — mirrors what up() did) - await queryRunner.query(`DELETE FROM freight.wagons;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1784100000000-AddBookingRouteDayIndex.ts b/apps/edr-freight-api/src/migrations/1784100000000-AddBookingRouteDayIndex.ts deleted file mode 100644 index 544600bef..000000000 --- a/apps/edr-freight-api/src/migrations/1784100000000-AddBookingRouteDayIndex.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Day-level booking pool: customers select a DAY (route + day), not a specific - * train. The batch engine's pool query filters bookings on - * (origin_yard_id, destination_yard_id, scheduled_date, status); this partial - * index backs that scan. - */ -export class AddBookingRouteDayIndex1784100000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_bookings_route_day - ON freight.bookings (origin_yard_id, destination_yard_id, scheduled_date, status) - WHERE deleted_at IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_route_day;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1790000000000-CreateWarehouseModule.ts b/apps/edr-freight-api/src/migrations/1790000000000-CreateWarehouseModule.ts deleted file mode 100644 index 0f9021ff2..000000000 --- a/apps/edr-freight-api/src/migrations/1790000000000-CreateWarehouseModule.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class CreateWarehouseModule1790000000000 implements MigrationInterface { - name = 'CreateWarehouseModule1790000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouses ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(160) NOT NULL, - code VARCHAR(40) NOT NULL UNIQUE, - type VARCHAR(32) NOT NULL, - station_id UUID NULL, - location_name VARCHAR(200) NULL, - capacity_weight NUMERIC(14,3) NULL, - capacity_containers INT NULL, - current_weight NUMERIC(14,3) NOT NULL DEFAULT 0, - current_containers INT NOT NULL DEFAULT 0, - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_yards ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - warehouse_id UUID NOT NULL REFERENCES freight.warehouses(id) ON DELETE CASCADE, - name VARCHAR(160) NOT NULL, - code VARCHAR(40) NOT NULL, - type VARCHAR(32) NOT NULL, - capacity_weight NUMERIC(14,3) NULL, - capacity_containers INT NULL, - current_weight NUMERIC(14,3) NOT NULL DEFAULT 0, - current_containers INT NOT NULL DEFAULT 0, - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT uq_warehouse_yards_code UNIQUE (warehouse_id, code) - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_zones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - yard_id UUID NOT NULL REFERENCES freight.warehouse_yards(id) ON DELETE CASCADE, - name VARCHAR(160) NOT NULL, - code VARCHAR(40) NOT NULL, - type VARCHAR(32) NOT NULL, - capacity_weight NUMERIC(14,3) NULL, - capacity_containers INT NULL, - current_weight NUMERIC(14,3) NOT NULL DEFAULT 0, - current_containers INT NOT NULL DEFAULT 0, - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT uq_warehouse_zones_code UNIQUE (yard_id, code) - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_inventory ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - warehouse_id UUID NOT NULL REFERENCES freight.warehouses(id), - yard_id UUID NOT NULL REFERENCES freight.warehouse_yards(id), - zone_id UUID NOT NULL REFERENCES freight.warehouse_zones(id), - booking_id UUID NOT NULL, - cargo_id UUID NULL, - container_id UUID NULL, - goods_id UUID NULL, - quantity NUMERIC(12,3) NOT NULL DEFAULT 0, - weight NUMERIC(14,3) NOT NULL DEFAULT 0, - volume NUMERIC(12,3) NULL, - status VARCHAR(32) NOT NULL DEFAULT 'ARRIVED_AT_WAREHOUSE', - inspection_status VARCHAR(20) NULL, - arrived_at TIMESTAMPTZ NULL, - inspected_at TIMESTAMPTZ NULL, - ready_for_loading_at TIMESTAMPTZ NULL, - notes TEXT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL - ); - `); - - const indexes: Array<[string, string, string]> = [ - ['idx_warehouses_type', 'warehouses', 'type'], - ['idx_warehouses_status', 'warehouses', 'status'], - ['idx_warehouses_station_id', 'warehouses', 'station_id'], - ['idx_warehouse_yards_warehouse_id', 'warehouse_yards', 'warehouse_id'], - ['idx_warehouse_yards_type', 'warehouse_yards', 'type'], - ['idx_warehouse_yards_status', 'warehouse_yards', 'status'], - ['idx_warehouse_zones_yard_id', 'warehouse_zones', 'yard_id'], - ['idx_warehouse_zones_type', 'warehouse_zones', 'type'], - ['idx_warehouse_zones_status', 'warehouse_zones', 'status'], - ['idx_warehouse_inventory_warehouse_id', 'warehouse_inventory', 'warehouse_id'], - ['idx_warehouse_inventory_yard_id', 'warehouse_inventory', 'yard_id'], - ['idx_warehouse_inventory_zone_id', 'warehouse_inventory', 'zone_id'], - ['idx_warehouse_inventory_booking_id', 'warehouse_inventory', 'booking_id'], - ['idx_warehouse_inventory_cargo_id', 'warehouse_inventory', 'cargo_id'], - ['idx_warehouse_inventory_container_id', 'warehouse_inventory', 'container_id'], - ['idx_warehouse_inventory_goods_id', 'warehouse_inventory', 'goods_id'], - ['idx_warehouse_inventory_status', 'warehouse_inventory', 'status'], - ]; - - for (const [indexName, table, column] of indexes) { - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS ${indexName} ON freight.${table}(${column});`, - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_inventory;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_zones;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_yards;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouses;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1790000000001-WarehouseBatch2.ts b/apps/edr-freight-api/src/migrations/1790000000001-WarehouseBatch2.ts deleted file mode 100644 index a1431d7bd..000000000 --- a/apps/edr-freight-api/src/migrations/1790000000001-WarehouseBatch2.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class WarehouseBatch21790000000001 implements MigrationInterface { - name = 'WarehouseBatch21790000000001'; - - public async up(queryRunner: QueryRunner): Promise { - // ── Capacity columns (weight + volume) on warehouse / yard / zone ────── - for (const table of ['warehouses', 'warehouse_yards', 'warehouse_zones']) { - await queryRunner.query(` - ALTER TABLE freight.${table} - ADD COLUMN IF NOT EXISTS max_weight NUMERIC(14,3) NULL, - ADD COLUMN IF NOT EXISTS max_volume NUMERIC(14,3) NULL, - ADD COLUMN IF NOT EXISTS current_volume NUMERIC(14,3) NOT NULL DEFAULT 0; - `); - // Backfill max_weight from the Batch 1 capacity_weight column. - await queryRunner.query(` - UPDATE freight.${table} SET max_weight = capacity_weight WHERE max_weight IS NULL; - `); - } - - // ── Inventory lifecycle: migrate Batch 1 statuses to Batch 2 set ─────── - await queryRunner.query(` - ALTER TABLE freight.warehouse_inventory - ALTER COLUMN status SET DEFAULT 'RECEIVED'; - `); - await queryRunner.query(` - UPDATE freight.warehouse_inventory SET status = 'RECEIVED' WHERE status = 'ARRIVED_AT_WAREHOUSE'; - `); - await queryRunner.query(` - UPDATE freight.warehouse_inventory SET status = 'STORED' WHERE status = 'UNDER_INSPECTION'; - `); - - // ── New lifecycle timestamps ────────────────────────────────────────── - await queryRunner.query(` - ALTER TABLE freight.warehouse_inventory - ADD COLUMN IF NOT EXISTS stored_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS reserved_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS loaded_at TIMESTAMPTZ NULL, - ADD COLUMN IF NOT EXISTS dispatched_at TIMESTAMPTZ NULL; - `); - - // booking_id becomes nullable (inventory can exist before booking linkage). - await queryRunner.query(` - ALTER TABLE freight.warehouse_inventory ALTER COLUMN booking_id DROP NOT NULL; - `); - - // ── Movement history ────────────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_inventory_movement ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - inventory_id UUID NOT NULL REFERENCES freight.warehouse_inventory(id) ON DELETE CASCADE, - from_warehouse_id UUID NOT NULL, - from_yard_id UUID NOT NULL, - from_zone_id UUID NOT NULL, - to_warehouse_id UUID NOT NULL, - to_yard_id UUID NOT NULL, - to_zone_id UUID NOT NULL, - remarks TEXT NULL, - moved_by VARCHAR(120) NULL, - moved_at TIMESTAMPTZ NOT NULL DEFAULT now(), - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL - ); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_inventory_movement_inventory_id - ON freight.warehouse_inventory_movement(inventory_id); - `); - - // ── Activity log ────────────────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_activity_log ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - inventory_id UUID NULL, - warehouse_id UUID NULL, - activity_type VARCHAR(40) NOT NULL, - description TEXT NULL, - performed_by VARCHAR(120) NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL - ); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_activity_log_inventory_id - ON freight.warehouse_activity_log(inventory_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_activity_log_warehouse_id - ON freight.warehouse_activity_log(warehouse_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_activity_log_activity_type - ON freight.warehouse_activity_log(activity_type); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_activity_log;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_inventory_movement;`); - - await queryRunner.query(` - ALTER TABLE freight.warehouse_inventory - DROP COLUMN IF EXISTS stored_at, - DROP COLUMN IF EXISTS reserved_at, - DROP COLUMN IF EXISTS loaded_at, - DROP COLUMN IF EXISTS dispatched_at; - `); - await queryRunner.query(` - ALTER TABLE freight.warehouse_inventory ALTER COLUMN status SET DEFAULT 'RECEIVED'; - `); - - for (const table of ['warehouses', 'warehouse_yards', 'warehouse_zones']) { - await queryRunner.query(` - ALTER TABLE freight.${table} - DROP COLUMN IF EXISTS max_weight, - DROP COLUMN IF EXISTS max_volume, - DROP COLUMN IF EXISTS current_volume; - `); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1790000000002-WarehouseBatch3.ts b/apps/edr-freight-api/src/migrations/1790000000002-WarehouseBatch3.ts deleted file mode 100644 index 4dcc9329c..000000000 --- a/apps/edr-freight-api/src/migrations/1790000000002-WarehouseBatch3.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Batch 3 — Warehouse → Loading → Train Departure visibility. - * Adds the warehouse_loadings record (inventory ↔ wagon). Does NOT touch any - * scheduling / wagon tables — the warehouse only reads from those. - */ -export class WarehouseBatch31790000000002 implements MigrationInterface { - name = 'WarehouseBatch31790000000002'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_loadings ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - warehouse_inventory_id UUID NOT NULL REFERENCES freight.warehouse_inventory(id) ON DELETE CASCADE, - booking_id UUID NULL, - wagon_id UUID NOT NULL, - loaded_at TIMESTAMPTZ NOT NULL DEFAULT now(), - loaded_by VARCHAR(120) NULL, - loaded_weight NUMERIC(14,3) NULL, - notes TEXT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ NULL - ); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_loadings_inventory_id - ON freight.warehouse_loadings(warehouse_inventory_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_loadings_booking_id - ON freight.warehouse_loadings(booking_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_loadings_wagon_id - ON freight.warehouse_loadings(wagon_id); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_loadings;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000000-AddActiveModeAndOnboardingToExternalProfiles.ts b/apps/edr-freight-api/src/migrations/1791000000000-AddActiveModeAndOnboardingToExternalProfiles.ts deleted file mode 100644 index 8484ca0f9..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000000-AddActiveModeAndOnboardingToExternalProfiles.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddActiveModeAndOnboardingToExternalProfiles1791000000000 - implements MigrationInterface -{ - name = 'AddActiveModeAndOnboardingToExternalProfiles1791000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.external_profiles - ADD COLUMN IF NOT EXISTS active_profile_type varchar(32); - `); - - await queryRunner.query(` - ALTER TABLE freight.external_profiles - ADD COLUMN IF NOT EXISTS onboarding_step varchar(40); - `); - - await queryRunner.query(` - ALTER TABLE freight.external_profiles - ADD COLUMN IF NOT EXISTS onboarding_completed boolean NOT NULL DEFAULT false; - `); - - // Existing users already use the portal — never re-gate them behind the - // new onboarding wizard. - await queryRunner.query(` - UPDATE freight.external_profiles - SET onboarding_completed = true - WHERE onboarding_completed = false; - `); - - // Backfill the active mode for existing users from their company's - // operational profiles. Prefer importer, then exporter, then whichever - // single profile the company has (forwarder/dj/transporter). - await queryRunner.query(` - UPDATE freight.external_profiles ep - SET active_profile_type = cp.type - FROM ( - SELECT DISTINCT ON (company_id) company_id, type - FROM freight.company_profiles - ORDER BY company_id, - CASE type - WHEN 'importer' THEN 0 - WHEN 'exporter' THEN 1 - ELSE 2 - END - ) cp - WHERE ep.company_id = cp.company_id - AND ep.active_profile_type IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.external_profiles - DROP COLUMN IF EXISTS onboarding_completed; - `); - await queryRunner.query(` - ALTER TABLE freight.external_profiles - DROP COLUMN IF EXISTS onboarding_step; - `); - await queryRunner.query(` - ALTER TABLE freight.external_profiles - DROP COLUMN IF EXISTS active_profile_type; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000000-AddWarehouseAllocationAndFeeRules.ts b/apps/edr-freight-api/src/migrations/1791000000000-AddWarehouseAllocationAndFeeRules.ts deleted file mode 100644 index 1cad0fe66..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000000-AddWarehouseAllocationAndFeeRules.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm'; - -/** - * Batch 5 — warehouse allocation rules, storage/demurrage fee rules, - * and demurrage lifecycle timestamps on inventory. Idempotent. - */ -export class AddWarehouseAllocationAndFeeRules1791000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'warehouse_allocation_rules', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' }, - { name: 'name', type: 'varchar', length: '160' }, - { name: 'priority', type: 'int', default: 100 }, - { name: 'freight_type', type: 'varchar', length: '16', isNullable: true }, - { name: 'trade_direction', type: 'varchar', length: '16', isNullable: true }, - { name: 'cargo_type_code', type: 'varchar', length: '50', isNullable: true }, - { name: 'container_status', type: 'varchar', length: '24', isNullable: true }, - { name: 'requires_inspection', type: 'boolean', isNullable: true }, - { name: 'target_facility_code', type: 'varchar', length: '40', isNullable: true }, - { name: 'target_yard_code', type: 'varchar', length: '40' }, - { name: 'target_warehouse_code', type: 'varchar', length: '40', isNullable: true }, - { name: 'target_zone_code', type: 'varchar', length: '40', isNullable: true }, - { name: 'storage_type', type: 'varchar', length: '80', isNullable: true }, - { name: 'is_active', type: 'boolean', default: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - indices: [ - { name: 'idx_war_priority', columnNames: ['priority'] }, - { name: 'idx_war_active', columnNames: ['is_active'] }, - ], - }), - true, - ); - - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'warehouse_fee_rules', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' }, - { name: 'name', type: 'varchar', length: '160' }, - { name: 'rule_type', type: 'varchar', length: '20' }, - { name: 'priority', type: 'int', default: 100 }, - { name: 'freight_type', type: 'varchar', length: '16', isNullable: true }, - { name: 'trade_direction', type: 'varchar', length: '16', isNullable: true }, - { name: 'cargo_type_code', type: 'varchar', length: '50', isNullable: true }, - { name: 'container_type', type: 'varchar', length: '40', isNullable: true }, - { name: 'facility_id', type: 'uuid', isNullable: true }, - { name: 'warehouse_id', type: 'uuid', isNullable: true }, - { name: 'yard_id', type: 'uuid', isNullable: true }, - { name: 'zone_id', type: 'uuid', isNullable: true }, - { name: 'free_days', type: 'int', default: 0 }, - { name: 'rate_per_day', type: 'numeric', precision: 14, scale: 2, default: 0 }, - { name: 'tiers', type: 'jsonb', default: "'[]'" }, - { name: 'currency', type: 'varchar', length: '8', default: "'USD'" }, - { name: 'is_active', type: 'boolean', default: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - indices: [ - { name: 'idx_wfr_type', columnNames: ['rule_type'] }, - { name: 'idx_wfr_active', columnNames: ['is_active'] }, - ], - }), - true, - ); - - const inventoryTable = await queryRunner.getTable('freight.warehouse_inventory'); - if (inventoryTable) { - const columnsToAdd = [ - { name: 'inspection_started_at', type: 'timestamptz', isNullable: true }, - { name: 'inspection_completed_at', type: 'timestamptz', isNullable: true }, - { name: 'ready_for_pickup_at', type: 'timestamptz', isNullable: true }, - { name: 'release_date', type: 'timestamptz', isNullable: true }, - { name: 'gate_cleared_at', type: 'timestamptz', isNullable: true }, - ]; - - const columnsToCreate = columnsToAdd.filter( - (col) => !inventoryTable.columns.some((c) => c.name === col.name), - ); - - if (columnsToCreate.length > 0) { - await queryRunner.addColumns( - 'freight.warehouse_inventory', - columnsToCreate.map((col) => new TableColumn(col)), - ); - } - } - } - - public async down(queryRunner: QueryRunner): Promise { - const inventoryTable = await queryRunner.getTable('freight.warehouse_inventory'); - if (inventoryTable) { - const columnNames = [ - 'inspection_started_at', - 'inspection_completed_at', - 'ready_for_pickup_at', - 'release_date', - 'gate_cleared_at', - ]; - const columnsToRemove = columnNames.filter((name) => - inventoryTable.columns.some((c) => c.name === name), - ); - - if (columnsToRemove.length > 0) { - await queryRunner.dropColumns('freight.warehouse_inventory', columnsToRemove); - } - } - - const feeRulesTable = await queryRunner.getTable('freight.warehouse_fee_rules'); - if (feeRulesTable) { - await queryRunner.dropTable('freight.warehouse_fee_rules', true); - } - - const allocationRulesTable = await queryRunner.getTable('freight.warehouse_allocation_rules'); - if (allocationRulesTable) { - await queryRunner.dropTable('freight.warehouse_allocation_rules', true); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000001-AddCompanyProfileIdToBookings.ts b/apps/edr-freight-api/src/migrations/1791000000001-AddCompanyProfileIdToBookings.ts deleted file mode 100644 index 0ca1a0d28..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000001-AddCompanyProfileIdToBookings.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddCompanyProfileIdToBookings1791000000001 - implements MigrationInterface -{ - name = 'AddCompanyProfileIdToBookings1791000000001'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS company_profile_id UUID; - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_bookings_company_profile_id - ON freight.bookings(company_profile_id); - `); - - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'FK_bookings_company_profile_id' - ) THEN - ALTER TABLE freight.bookings - ADD CONSTRAINT "FK_bookings_company_profile_id" - FOREIGN KEY (company_profile_id) - REFERENCES freight.company_profiles(id); - END IF; - END $$; - `); - - // Backfill by natural mapping: IMPORT → importer profile, EXPORT → exporter - // profile, for each booking's own company. - await queryRunner.query(` - UPDATE freight.bookings b - SET company_profile_id = cp.id - FROM freight.company_profiles cp - WHERE cp.company_id = b.company_id - AND b.company_profile_id IS NULL - AND ( - (b.trade_direction = 'IMPORT' AND cp.type = 'importer') OR - (b.trade_direction = 'EXPORT' AND cp.type = 'exporter') - ); - `); - - // Forwarder / single-profile companies: one profile per company, so the - // mapping is unambiguous regardless of trade direction. - await queryRunner.query(` - UPDATE freight.bookings b - SET company_profile_id = cp.id - FROM freight.company_profiles cp - JOIN freight.companies c ON c.id = cp.company_id - WHERE cp.company_id = b.company_id - AND c.type <> 'customer' - AND b.company_profile_id IS NULL; - `); - - // Remaining customer-owned rows (e.g. DOMESTIC, or a direction with no - // matching profile): attribute to the company's importer profile, else its - // exporter profile, so nothing disappears from the customer's list. - await queryRunner.query(` - UPDATE freight.bookings b - SET company_profile_id = cp.id - FROM ( - SELECT DISTINCT ON (company_id) company_id, id - FROM freight.company_profiles - ORDER BY company_id, - CASE type - WHEN 'importer' THEN 0 - WHEN 'exporter' THEN 1 - ELSE 2 - END - ) cp - WHERE cp.company_id = b.company_id - AND b.company_id IS NOT NULL - AND b.company_profile_id IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP CONSTRAINT IF EXISTS "FK_bookings_company_profile_id"; - `); - await queryRunner.query(` - DROP INDEX IF EXISTS freight.idx_bookings_company_profile_id; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS company_profile_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000001-AddWarehouseFeeInvoices.ts b/apps/edr-freight-api/src/migrations/1791000000001-AddWarehouseFeeInvoices.ts deleted file mode 100644 index 662a35739..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000001-AddWarehouseFeeInvoices.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { MigrationInterface, QueryRunner, Table } from 'typeorm'; - -/** Batch 6 — warehouse fee invoices + invoice items. Idempotent (createTable ifNotExists). */ -export class AddWarehouseFeeInvoices1791000000001 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'warehouse_fee_invoices', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' }, - { name: 'invoice_number', type: 'varchar', length: '40', isUnique: true }, - { name: 'booking_id', type: 'uuid', isNullable: true }, - { name: 'customer_id', type: 'uuid', isNullable: true }, - { name: 'inventory_id', type: 'uuid' }, - { name: 'facility_id', type: 'uuid', isNullable: true }, - { name: 'warehouse_id', type: 'uuid', isNullable: true }, - { name: 'yard_id', type: 'uuid', isNullable: true }, - { name: 'zone_id', type: 'uuid', isNullable: true }, - { name: 'invoice_type', type: 'varchar', length: '32', default: "'MIXED_WAREHOUSE_FEES'" }, - { name: 'status', type: 'varchar', length: '20', default: "'DRAFT'" }, - { name: 'subtotal_amount', type: 'numeric', precision: 14, scale: 2, default: 0 }, - { name: 'tax_amount', type: 'numeric', precision: 14, scale: 2, default: 0 }, - { name: 'total_amount', type: 'numeric', precision: 14, scale: 2, default: 0 }, - { name: 'paid_amount', type: 'numeric', precision: 14, scale: 2, default: 0 }, - { name: 'balance_amount', type: 'numeric', precision: 14, scale: 2, default: 0 }, - { name: 'currency', type: 'varchar', length: '8', default: "'USD'" }, - { name: 'period_start', type: 'timestamptz', isNullable: true }, - { name: 'period_end', type: 'timestamptz', isNullable: true }, - { name: 'issued_at', type: 'timestamptz', isNullable: true }, - { name: 'due_date', type: 'timestamptz', isNullable: true }, - { name: 'paid_at', type: 'timestamptz', isNullable: true }, - { name: 'cancelled_at', type: 'timestamptz', isNullable: true }, - { name: 'payments', type: 'jsonb', default: "'[]'" }, - { name: 'notes', type: 'text', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - indices: [ - { name: 'idx_wfi_booking', columnNames: ['booking_id'] }, - { name: 'idx_wfi_inventory', columnNames: ['inventory_id'] }, - { name: 'idx_wfi_status', columnNames: ['status'] }, - ], - }), - true, - ); - - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'warehouse_fee_invoice_items', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' }, - { name: 'invoice_id', type: 'uuid' }, - { name: 'fee_rule_id', type: 'uuid', isNullable: true }, - { name: 'fee_type', type: 'varchar', length: '32' }, - { name: 'description', type: 'varchar', length: '255' }, - { name: 'quantity', type: 'numeric', precision: 12, scale: 2, default: 1 }, - { name: 'unit_rate', type: 'numeric', precision: 14, scale: 2, default: 0 }, - { name: 'amount', type: 'numeric', precision: 14, scale: 2, default: 0 }, - { name: 'currency', type: 'varchar', length: '8', default: "'USD'" }, - { name: 'chargeable_days', type: 'int', isNullable: true }, - { name: 'free_days', type: 'int', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['invoice_id'], - referencedSchema: 'freight', - referencedTableName: 'warehouse_fee_invoices', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }, - ], - indices: [{ name: 'idx_wfii_invoice', columnNames: ['invoice_id'] }], - }), - true, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.warehouse_fee_invoice_items', true); - await queryRunner.dropTable('freight.warehouse_fee_invoices', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000002-AddImportPickupDeliveryColumns.ts b/apps/edr-freight-api/src/migrations/1791000000002-AddImportPickupDeliveryColumns.ts deleted file mode 100644 index 3e272c859..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000002-AddImportPickupDeliveryColumns.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -/** - * Import pickup branch on warehouse_inventory: - * - release_order_reference: DO / release order number sent to the customer - * - delivered_at: when the goods were handed over (proof of delivery) - * - * Idempotent: the shared dev DB may already carry some of these columns - * (added by another checkout), so only add what is missing. - */ -export class AddImportPickupDeliveryColumns1791000000002 implements MigrationInterface { - private readonly table = 'freight.warehouse_inventory'; - - public async up(queryRunner: QueryRunner): Promise { - if (!(await queryRunner.hasColumn(this.table, 'release_order_reference'))) { - await queryRunner.addColumn( - this.table, - new TableColumn({ name: 'release_order_reference', type: 'varchar', length: '100', isNullable: true }), - ); - } - if (!(await queryRunner.hasColumn(this.table, 'delivered_at'))) { - await queryRunner.addColumn( - this.table, - new TableColumn({ name: 'delivered_at', type: 'timestamptz', isNullable: true }), - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - if (await queryRunner.hasColumn(this.table, 'release_order_reference')) { - await queryRunner.dropColumn(this.table, 'release_order_reference'); - } - if (await queryRunner.hasColumn(this.table, 'delivered_at')) { - await queryRunner.dropColumn(this.table, 'delivered_at'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000002-AddNationalityToCompanies.ts b/apps/edr-freight-api/src/migrations/1791000000002-AddNationalityToCompanies.ts deleted file mode 100644 index 1a05a9e45..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000002-AddNationalityToCompanies.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddNationalityToCompanies1791000000002 - implements MigrationInterface -{ - name = "AddNationalityToCompanies1791000000002"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS nationality varchar(32); - `); - - // Existing companies default to Ethiopian (country defaults to Ethiopia). - await queryRunner.query(` - UPDATE freight.companies - SET nationality = 'ethiopian' - WHERE nationality IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS nationality; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000003-AddBusinessLicenseFilesToCompanyProfiles.ts b/apps/edr-freight-api/src/migrations/1791000000003-AddBusinessLicenseFilesToCompanyProfiles.ts deleted file mode 100644 index d1e0412f0..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000003-AddBusinessLicenseFilesToCompanyProfiles.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddBusinessLicenseFilesToCompanyProfiles1791000000003 - implements MigrationInterface -{ - name = "AddBusinessLicenseFilesToCompanyProfiles1791000000003"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.company_profiles - ADD COLUMN IF NOT EXISTS business_license_files jsonb; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.company_profiles - DROP COLUMN IF EXISTS business_license_files; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000003-AddETradeFieldsToCompanies.ts b/apps/edr-freight-api/src/migrations/1791000000003-AddETradeFieldsToCompanies.ts deleted file mode 100644 index 07f0d2555..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000003-AddETradeFieldsToCompanies.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddETradeFieldsToCompanies1791000000003 - implements MigrationInterface -{ - name = "AddETradeFieldsToCompanies1791000000003"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS licence_number varchar(100); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS status_description text; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS date_registered varchar(50); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS renewed_from varchar(50); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS renewal_date varchar(50); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS renewed_to varchar(50); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS region varchar(100); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS zone varchar(100); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS woreda varchar(100); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS kebele varchar(100); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS house_no varchar(100); - `); - await queryRunner.query(` - ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS etrade_phone varchar(20); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS licence_number; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS status_description; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS date_registered; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS renewed_from; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS renewal_date; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS renewed_to; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS region; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS zone; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS woreda; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS kebele; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS house_no; - `); - await queryRunner.query(` - ALTER TABLE freight.companies - DROP COLUMN IF EXISTS etrade_phone; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000003-AddInventoryUnloadedAt.ts b/apps/edr-freight-api/src/migrations/1791000000003-AddInventoryUnloadedAt.ts deleted file mode 100644 index 6009d9ea4..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000003-AddInventoryUnloadedAt.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -/** - * Batch 8 — train-arrival unload landing state on warehouse_inventory: - * - unloaded_at: when the goods were unloaded off the arrived train (before storage/inspection) - * - * The `status` column is a free varchar, so the new 'UNLOADED' value needs no schema change. - * Idempotent: the shared dev DB may already carry this column (added by another checkout). - */ -export class AddInventoryUnloadedAt1791000000003 implements MigrationInterface { - private readonly table = 'freight.warehouse_inventory'; - - public async up(queryRunner: QueryRunner): Promise { - if (!(await queryRunner.hasColumn(this.table, 'unloaded_at'))) { - await queryRunner.addColumn( - this.table, - new TableColumn({ name: 'unloaded_at', type: 'timestamptz', isNullable: true }), - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - if (await queryRunner.hasColumn(this.table, 'unloaded_at')) { - await queryRunner.dropColumn(this.table, 'unloaded_at'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000004-AddFacilityIdToWarehousesFix.ts b/apps/edr-freight-api/src/migrations/1791000000004-AddFacilityIdToWarehousesFix.ts deleted file mode 100644 index df239a375..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000004-AddFacilityIdToWarehousesFix.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from 'typeorm'; - -/** - * Fix: migration 1750000000001 (AddFacilityIdToWarehouses) silently skipped because - * the freight.warehouses table didn't exist yet at that timestamp. The column was - * never added. Add it now with idempotent guards. - */ -export class AddFacilityIdToWarehousesFix1791000000004 implements MigrationInterface { - private readonly table = 'freight.warehouses'; - - public async up(queryRunner: QueryRunner): Promise { - if (!(await queryRunner.hasColumn(this.table, 'facility_id'))) { - await queryRunner.addColumn( - this.table, - new TableColumn({ - name: 'facility_id', - type: 'uuid', - isNullable: true, - }), - ); - } - - const table = await queryRunner.getTable(this.table); - const hasFk = table?.foreignKeys.some((fk) => fk.columnNames.includes('facility_id')); - if (!hasFk) { - await queryRunner.createForeignKey( - this.table, - new TableForeignKey({ - columnNames: ['facility_id'], - referencedColumnNames: ['id'], - referencedTableName: 'facilities', - referencedSchema: 'freight', - onDelete: 'SET NULL', - }), - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable(this.table); - if (!table) return; - - const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.includes('facility_id')); - if (foreignKey) { - await queryRunner.dropForeignKey(this.table, foreignKey); - } - - if (await queryRunner.hasColumn(this.table, 'facility_id')) { - await queryRunner.dropColumn(this.table, 'facility_id'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1791000000005-AddWarehouseInventoryInspectionStatusFix.ts b/apps/edr-freight-api/src/migrations/1791000000005-AddWarehouseInventoryInspectionStatusFix.ts deleted file mode 100644 index 4a59e33b1..000000000 --- a/apps/edr-freight-api/src/migrations/1791000000005-AddWarehouseInventoryInspectionStatusFix.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -/** - * Fix for fresh deployments: AddWarehouseInspection1750000000003 runs before - * the warehouse_inventory table exists, so it cannot add inspection_status. - */ -export class AddWarehouseInventoryInspectionStatusFix1791000000005 implements MigrationInterface { - private readonly table = 'freight.warehouse_inventory'; - - public async up(queryRunner: QueryRunner): Promise { - if ((await queryRunner.hasTable(this.table)) && !(await queryRunner.hasColumn(this.table, 'inspection_status'))) { - await queryRunner.addColumn( - this.table, - new TableColumn({ name: 'inspection_status', type: 'varchar', length: '20', isNullable: true }), - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - if ((await queryRunner.hasTable(this.table)) && (await queryRunner.hasColumn(this.table, 'inspection_status'))) { - await queryRunner.dropColumn(this.table, 'inspection_status'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1791999999999-CreateDropdownSettings.ts b/apps/edr-freight-api/src/migrations/1791999999999-CreateDropdownSettings.ts deleted file mode 100644 index d86efa239..000000000 --- a/apps/edr-freight-api/src/migrations/1791999999999-CreateDropdownSettings.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Creates the generic dropdown settings tables (freight.dropdown_settings + - * freight.dropdown_options) backing the DropdownSetting / DropdownOption - * entities. These tables previously only existed via `synchronize` on some - * databases; this migration makes them part of the migration history so the - * SeedGeneralContractPeriod migration (which inserts into them) can run on a - * fresh database. Idempotent so it is safe on DBs where the tables already exist. - */ -export class CreateDropdownSettings1791999999999 - implements MigrationInterface -{ - name = 'CreateDropdownSettings1791999999999'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS "freight"."dropdown_settings" ( - "id" uuid NOT NULL DEFAULT gen_random_uuid(), - "code" varchar(128) NOT NULL, - "label" varchar(256) NOT NULL, - "description" text, - "multiple" boolean NOT NULL DEFAULT false, - "meta" jsonb, - "created_at" timestamptz NOT NULL DEFAULT now(), - "updated_at" timestamptz NOT NULL DEFAULT now(), - "deleted_at" timestamptz, - CONSTRAINT "PK_dropdown_settings" PRIMARY KEY ("id") - ); - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_dropdown_settings_code" - ON "freight"."dropdown_settings" ("code"); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS "freight"."dropdown_options" ( - "id" uuid NOT NULL DEFAULT gen_random_uuid(), - "setting_id" uuid NOT NULL, - "value" varchar(256) NOT NULL, - "label" varchar(256) NOT NULL, - "note" text, - "is_disabled" boolean NOT NULL DEFAULT false, - "display_order" integer NOT NULL DEFAULT 0, - "meta" jsonb, - "created_at" timestamptz NOT NULL DEFAULT now(), - "updated_at" timestamptz NOT NULL DEFAULT now(), - "deleted_at" timestamptz, - CONSTRAINT "PK_dropdown_options" PRIMARY KEY ("id"), - CONSTRAINT "FK_dropdown_options_setting" FOREIGN KEY ("setting_id") - REFERENCES "freight"."dropdown_settings" ("id") ON DELETE CASCADE - ); - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_dropdown_options_setting_value" - ON "freight"."dropdown_options" ("setting_id", "value"); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP TABLE IF EXISTS "freight"."dropdown_options";`, - ); - await queryRunner.query( - `DROP TABLE IF EXISTS "freight"."dropdown_settings";`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1792000000000-AddUnitOfMeasureToCargoTypes.ts b/apps/edr-freight-api/src/migrations/1792000000000-AddUnitOfMeasureToCargoTypes.ts deleted file mode 100644 index 6ec9d60cd..000000000 --- a/apps/edr-freight-api/src/migrations/1792000000000-AddUnitOfMeasureToCargoTypes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddUnitOfMeasureToCargoTypes1792000000000 - implements MigrationInterface -{ - name = 'AddUnitOfMeasureToCargoTypes1792000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.cargo_types ADD COLUMN IF NOT EXISTS unit_of_measure VARCHAR(16);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.cargo_types DROP COLUMN IF EXISTS unit_of_measure;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1792000000001-AddBookingTypeAndContractFields.ts b/apps/edr-freight-api/src/migrations/1792000000001-AddBookingTypeAndContractFields.ts deleted file mode 100644 index c55186a38..000000000 --- a/apps/edr-freight-api/src/migrations/1792000000001-AddBookingTypeAndContractFields.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddBookingTypeAndContractFields1792000000001 - implements MigrationInterface -{ - name = 'AddBookingTypeAndContractFields1792000000001'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS booking_type VARCHAR(20) NOT NULL DEFAULT 'ONE_TIME';`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;`, - ); - // General contracts have no shipment date at creation — relax the NOT NULL. - await queryRunner.query( - `ALTER TABLE freight.bookings ALTER COLUMN scheduled_date DROP NOT NULL;`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_bookings_booking_type ON freight.bookings (booking_type);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight.idx_bookings_booking_type;`, - ); - // Reinstate NOT NULL only if no null rows exist (general contracts would block it). - await queryRunner.query( - `ALTER TABLE freight.bookings ALTER COLUMN scheduled_date SET NOT NULL;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS expires_at;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS booking_type;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1792000000002-CreateBookingOrders.ts b/apps/edr-freight-api/src/migrations/1792000000002-CreateBookingOrders.ts deleted file mode 100644 index ceb98b5d1..000000000 --- a/apps/edr-freight-api/src/migrations/1792000000002-CreateBookingOrders.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -export class CreateBookingOrders1792000000002 implements MigrationInterface { - name = 'CreateBookingOrders1792000000002'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'booking_orders', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'reference', type: 'varchar', length: '64', isUnique: true }, - { name: 'contract_booking_id', type: 'uuid' }, - { name: 'booking_id', type: 'uuid', isNullable: true }, - { name: 'company_id', type: 'uuid', isNullable: true }, - { name: 'scheduled_date', type: 'timestamptz' }, - { name: 'status', type: 'varchar', length: '40', default: "'PAID'" }, - { name: 'scheduling_status', type: 'varchar', length: '30', default: "'NOT_SCHEDULED'" }, - { name: 'train_schedule_id', type: 'uuid', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.booking_orders', - new TableIndex({ name: 'idx_booking_orders_contract', columnNames: ['contract_booking_id'] }), - ); - await queryRunner.createIndex( - 'freight.booking_orders', - new TableIndex({ name: 'idx_booking_orders_company', columnNames: ['company_id'] }), - ); - - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'booking_order_lines', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'order_id', type: 'uuid' }, - { name: 'container_type_id', type: 'uuid', isNullable: true }, - { name: 'quantity', type: 'numeric', precision: 12, scale: 3 }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['order_id'], - referencedSchema: 'freight', - referencedTableName: 'booking_orders', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.booking_order_lines', - new TableIndex({ name: 'idx_booking_order_lines_order', columnNames: ['order_id'] }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.booking_order_lines', true); - await queryRunner.dropTable('freight.booking_orders', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1792000000003-SeedGeneralContractPeriod.ts b/apps/edr-freight-api/src/migrations/1792000000003-SeedGeneralContractPeriod.ts deleted file mode 100644 index e6c0708d4..000000000 --- a/apps/edr-freight-api/src/migrations/1792000000003-SeedGeneralContractPeriod.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Seeds the global "general contract period" setting (months). Stored as a - * dropdown_settings row with a single option whose `value` holds the month count - * so backoffice can manage it through the existing settings UI later. - */ -export class SeedGeneralContractPeriod1792000000003 - implements MigrationInterface -{ - name = 'SeedGeneralContractPeriod1792000000003'; - private readonly code = 'general_contract_period'; - - public async up(queryRunner: QueryRunner): Promise { - const existing = await queryRunner.query( - `SELECT id FROM freight.dropdown_settings WHERE code = $1 LIMIT 1;`, - [this.code], - ); - if (existing.length > 0) return; - - const inserted = await queryRunner.query( - `INSERT INTO freight.dropdown_settings (code, label, description, multiple) - VALUES ($1, $2, $3, false) - RETURNING id;`, - [ - this.code, - 'General Contract Period (months)', - 'How many months a general contract stays open for ordering after activation.', - ], - ); - const settingId = inserted[0].id; - - await queryRunner.query( - `INSERT INTO freight.dropdown_options (setting_id, value, label, display_order) - VALUES ($1, $2, $3, 0);`, - [settingId, '3', '3 months'], - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DELETE FROM freight.dropdown_settings WHERE code = $1;`, - [this.code], - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1792000000004-SeedContractValidityPeriods.ts b/apps/edr-freight-api/src/migrations/1792000000004-SeedContractValidityPeriods.ts deleted file mode 100644 index e635d19c1..000000000 --- a/apps/edr-freight-api/src/migrations/1792000000004-SeedContractValidityPeriods.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Seeds the admin-configurable "contract validity periods" setting (days). Stored - * as a dropdown_settings row whose options each hold a day count in `value`, so - * backoffice manages them through the existing Dropdown Settings UI and the - * contract staff-accept dialog only offers the configured durations. - */ -export class SeedContractValidityPeriods1792000000004 - implements MigrationInterface -{ - name = 'SeedContractValidityPeriods1792000000004'; - private readonly code = 'contract_validity_periods'; - private readonly options: Array<{ value: string; label: string }> = [ - { value: '180', label: '6 months' }, - { value: '365', label: '1 year' }, - { value: '730', label: '2 years' }, - ]; - - public async up(queryRunner: QueryRunner): Promise { - const existing = await queryRunner.query( - `SELECT id FROM freight.dropdown_settings WHERE code = $1 LIMIT 1;`, - [this.code], - ); - if (existing.length > 0) return; - - const inserted = await queryRunner.query( - `INSERT INTO freight.dropdown_settings (code, label, description, multiple) - VALUES ($1, $2, $3, false) - RETURNING id;`, - [ - this.code, - 'Contract Validity Periods (days)', - 'Validity durations (in days) a staff can choose when accepting a submitted contract.', - ], - ); - const settingId = inserted[0].id; - - for (let i = 0; i < this.options.length; i++) { - const opt = this.options[i]; - await queryRunner.query( - `INSERT INTO freight.dropdown_options (setting_id, value, label, display_order) - VALUES ($1, $2, $3, $4);`, - [settingId, opt.value, opt.label, i], - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DELETE FROM freight.dropdown_settings WHERE code = $1;`, - [this.code], - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1800000000001-AddVehicleDriverAssignment.ts b/apps/edr-freight-api/src/migrations/1800000000001-AddVehicleDriverAssignment.ts deleted file mode 100644 index 7a45a881b..000000000 --- a/apps/edr-freight-api/src/migrations/1800000000001-AddVehicleDriverAssignment.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -/** - * Add driver assignment fields to vehicles table - */ -export class AddVehicleDriverAssignment1800000000001 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const vehiclesTable = await queryRunner.getTable('freight.vehicles'); - if (vehiclesTable) { - const hasAssignedDriverId = vehiclesTable.columns.some((col) => col.name === 'assigned_driver_id'); - if (!hasAssignedDriverId) { - await queryRunner.addColumn( - 'freight.vehicles', - new TableColumn({ - name: 'assigned_driver_id', - type: 'uuid', - isNullable: true, - }), - ); - } - - const hasAssignedDriverName = vehiclesTable.columns.some((col) => col.name === 'assigned_driver_name'); - if (!hasAssignedDriverName) { - await queryRunner.addColumn( - 'freight.vehicles', - new TableColumn({ - name: 'assigned_driver_name', - type: 'varchar', - isNullable: true, - }), - ); - } - } - } - - public async down(queryRunner: QueryRunner): Promise { - const vehiclesTable = await queryRunner.getTable('freight.vehicles'); - if (vehiclesTable) { - const hasAssignedDriverId = vehiclesTable.columns.some((col) => col.name === 'assigned_driver_id'); - if (hasAssignedDriverId) { - await queryRunner.dropColumn('freight.vehicles', 'assigned_driver_id'); - } - - const hasAssignedDriverName = vehiclesTable.columns.some((col) => col.name === 'assigned_driver_name'); - if (hasAssignedDriverName) { - await queryRunner.dropColumn('freight.vehicles', 'assigned_driver_name'); - } - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1810000000000-CreateFirstMile.ts b/apps/edr-freight-api/src/migrations/1810000000000-CreateFirstMile.ts deleted file mode 100644 index 10c4f3a44..000000000 --- a/apps/edr-freight-api/src/migrations/1810000000000-CreateFirstMile.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; - -/** - * Create the freight.first_mile table — one row per booking's first-mile - * (door → terminal) leg, with payment split and an optional assigned vehicle. - */ -export class CreateFirstMile1810000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.first_mile'); - if (exists) return; - - await queryRunner.createTable( - new Table({ - name: 'freight.first_mile', - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'gen_random_uuid()', - }, - { name: 'booking_id', type: 'uuid', isNullable: false }, - { - name: 'status', - type: 'varchar', - length: '30', - default: `'PAYMENT_PENDING'`, - isNullable: false, - }, - { - name: 'advanced_payment', - type: 'numeric', - precision: 14, - scale: 2, - default: 0, - isNullable: false, - }, - { - name: 'remaining_payment', - type: 'numeric', - precision: 14, - scale: 2, - default: 0, - isNullable: false, - }, - { - name: 'estimated_km', - type: 'numeric', - precision: 10, - scale: 2, - isNullable: true, - }, - { - name: 'exact_km', - type: 'numeric', - precision: 10, - scale: 2, - isNullable: true, - }, - { name: 'vehicle_id', type: 'uuid', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createForeignKey( - 'freight.first_mile', - new TableForeignKey({ - columnNames: ['booking_id'], - referencedTableName: 'freight.bookings', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }), - ); - - await queryRunner.createForeignKey( - 'freight.first_mile', - new TableForeignKey({ - columnNames: ['vehicle_id'], - referencedTableName: 'freight.vehicles', - referencedColumnNames: ['id'], - onDelete: 'SET NULL', - }), - ); - - await queryRunner.query( - `CREATE INDEX "IDX_first_mile_booking_id" ON "freight"."first_mile" ("booking_id")`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_first_mile_status" ON "freight"."first_mile" ("status")`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_first_mile_vehicle_id" ON "freight"."first_mile" ("vehicle_id")`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.first_mile'); - if (exists) { - await queryRunner.dropTable('freight.first_mile'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1810000000001-CreateLastMile.ts b/apps/edr-freight-api/src/migrations/1810000000001-CreateLastMile.ts deleted file mode 100644 index 3c0ee8dfc..000000000 --- a/apps/edr-freight-api/src/migrations/1810000000001-CreateLastMile.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; - -/** - * Create the freight.last_mile table — one row per booking's last-mile - * (terminal → door) leg, with payment split and an optional assigned vehicle. - */ -export class CreateLastMile1810000000001 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.last_mile'); - if (exists) return; - - await queryRunner.createTable( - new Table({ - name: 'freight.last_mile', - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'gen_random_uuid()', - }, - { name: 'booking_id', type: 'uuid', isNullable: false }, - { - name: 'status', - type: 'varchar', - length: '30', - default: `'PAYMENT_PENDING'`, - isNullable: false, - }, - { - name: 'advanced_payment', - type: 'numeric', - precision: 14, - scale: 2, - default: 0, - isNullable: false, - }, - { - name: 'remaining_payment', - type: 'numeric', - precision: 14, - scale: 2, - default: 0, - isNullable: false, - }, - { - name: 'estimated_km', - type: 'numeric', - precision: 10, - scale: 2, - isNullable: true, - }, - { - name: 'exact_km', - type: 'numeric', - precision: 10, - scale: 2, - isNullable: true, - }, - { name: 'vehicle_id', type: 'uuid', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createForeignKey( - 'freight.last_mile', - new TableForeignKey({ - columnNames: ['booking_id'], - referencedTableName: 'freight.bookings', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }), - ); - - await queryRunner.createForeignKey( - 'freight.last_mile', - new TableForeignKey({ - columnNames: ['vehicle_id'], - referencedTableName: 'freight.vehicles', - referencedColumnNames: ['id'], - onDelete: 'SET NULL', - }), - ); - - await queryRunner.query( - `CREATE INDEX "IDX_last_mile_booking_id" ON "freight"."last_mile" ("booking_id")`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_last_mile_status" ON "freight"."last_mile" ("status")`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_last_mile_vehicle_id" ON "freight"."last_mile" ("vehicle_id")`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.last_mile'); - if (exists) { - await queryRunner.dropTable('freight.last_mile'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1810000000002-AddVehicleCodeAndPlates.ts b/apps/edr-freight-api/src/migrations/1810000000002-AddVehicleCodeAndPlates.ts deleted file mode 100644 index a2a1aae17..000000000 --- a/apps/edr-freight-api/src/migrations/1810000000002-AddVehicleCodeAndPlates.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -export class AddVehicleCodeAndPlates1810000000002 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const hasCode = await queryRunner.hasColumn('freight.vehicles', 'code'); - if (!hasCode) { - await queryRunner.addColumn( - 'freight.vehicles', - new TableColumn({ name: 'code', type: 'varchar', isNullable: true }), - ); - } - - const hasPower = await queryRunner.hasColumn('freight.vehicles', 'power_plate_no'); - if (!hasPower) { - await queryRunner.addColumn( - 'freight.vehicles', - new TableColumn({ name: 'power_plate_no', type: 'varchar', isNullable: true }), - ); - } - - const hasTrailer = await queryRunner.hasColumn('freight.vehicles', 'trailer_plate_no'); - if (!hasTrailer) { - await queryRunner.addColumn( - 'freight.vehicles', - new TableColumn({ name: 'trailer_plate_no', type: 'varchar', isNullable: true }), - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumn('freight.vehicles', 'trailer_plate_no'); - await queryRunner.dropColumn('freight.vehicles', 'power_plate_no'); - await queryRunner.dropColumn('freight.vehicles', 'code'); - } -} diff --git a/apps/edr-freight-api/src/migrations/1810000000002-CreateLastMileContainerAllocations.ts b/apps/edr-freight-api/src/migrations/1810000000002-CreateLastMileContainerAllocations.ts deleted file mode 100644 index b2026b753..000000000 --- a/apps/edr-freight-api/src/migrations/1810000000002-CreateLastMileContainerAllocations.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; - -/** - * Create the freight.last_mile_container_allocations table — container allocation - * records linking last-mile deliveries with containers and vehicles. - */ -export class CreateLastMileContainerAllocations1810000000002 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.last_mile_container_allocations'); - if (exists) return; - - await queryRunner.createTable( - new Table({ - name: 'freight.last_mile_container_allocations', - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'gen_random_uuid()', - }, - { name: 'last_mile_id', type: 'uuid', isNullable: false }, - { name: 'container_id', type: 'uuid', isNullable: false }, - { name: 'vehicle_id', type: 'uuid', isNullable: true }, - { - name: 'container_type', - type: 'text', - isNullable: false, - }, - { - name: 'quantity', - type: 'integer', - default: 1, - isNullable: false, - }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createForeignKey( - 'freight.last_mile_container_allocations', - new TableForeignKey({ - columnNames: ['last_mile_id'], - referencedTableName: 'freight.last_mile', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }), - ); - - await queryRunner.createForeignKey( - 'freight.last_mile_container_allocations', - new TableForeignKey({ - columnNames: ['vehicle_id'], - referencedTableName: 'freight.vehicles', - referencedColumnNames: ['id'], - onDelete: 'SET NULL', - }), - ); - - await queryRunner.query( - `CREATE INDEX "IDX_last_mile_container_allocations_last_mile_id" ON "freight"."last_mile_container_allocations" ("last_mile_id")`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_last_mile_container_allocations_vehicle_id" ON "freight"."last_mile_container_allocations" ("vehicle_id")`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.last_mile_container_allocations'); - if (exists) { - await queryRunner.dropTable('freight.last_mile_container_allocations'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1810000000002-MakeCompanyProfileReferenceNullable.ts b/apps/edr-freight-api/src/migrations/1810000000002-MakeCompanyProfileReferenceNullable.ts deleted file mode 100644 index bc1765cfe..000000000 --- a/apps/edr-freight-api/src/migrations/1810000000002-MakeCompanyProfileReferenceNullable.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Company-profile references are now minted only when a profile is approved - * (status → Active); pending profiles carry NULL. Drop the NOT NULL constraint - * on freight.company_profiles.reference. The existing unique index is kept — - * Postgres treats NULLs as distinct, so multiple pending (NULL) profiles don't - * collide. - */ -export class MakeCompanyProfileReferenceNullable1810000000002 - implements MigrationInterface -{ - name = "MakeCompanyProfileReferenceNullable1810000000002"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "freight"."company_profiles" ALTER COLUMN "reference" DROP NOT NULL`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - // Reinstating NOT NULL requires every row to have a reference; any pending - // (NULL) profiles get a placeholder so the constraint can be re-applied. - await queryRunner.query( - `UPDATE "freight"."company_profiles" SET "reference" = 'PENDING-' || left(replace("id"::text, '-', ''), 12) WHERE "reference" IS NULL`, - ); - await queryRunner.query( - `ALTER TABLE "freight"."company_profiles" ALTER COLUMN "reference" SET NOT NULL`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1810000000003-CreateOtpVerifications.ts b/apps/edr-freight-api/src/migrations/1810000000003-CreateOtpVerifications.ts deleted file mode 100644 index ec0610f52..000000000 --- a/apps/edr-freight-api/src/migrations/1810000000003-CreateOtpVerifications.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { MigrationInterface, QueryRunner, Table } from "typeorm"; - -/** - * Create the public.otp_verifications table backing the OTP module - * (OtpVerification entity). One row per phone, holding the latest server-issued - * code and whether that phone has been verified. - */ -export class CreateOtpVerifications1810000000003 - implements MigrationInterface -{ - name = "CreateOtpVerifications1810000000003"; - - public async up(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable("otp_verifications"); - if (exists) return; - - await queryRunner.createTable( - new Table({ - name: "otp_verifications", - columns: [ - { - name: "id", - type: "uuid", - isPrimary: true, - default: "gen_random_uuid()", - }, - { name: "phone", type: "varchar", isUnique: true }, - { name: "otp", type: "varchar" }, - { name: "verified", type: "boolean", default: false }, - { name: "created_at", type: "timestamptz", default: "now()" }, - { name: "updated_at", type: "timestamptz", default: "now()" }, - { name: "deleted_at", type: "timestamptz", isNullable: true }, - ], - }), - true, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable("otp_verifications", true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1810000000004-AddPostPaymentCompletedColumn.ts b/apps/edr-freight-api/src/migrations/1810000000004-AddPostPaymentCompletedColumn.ts deleted file mode 100644 index aeedae2b1..000000000 --- a/apps/edr-freight-api/src/migrations/1810000000004-AddPostPaymentCompletedColumn.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -export class AddPostPaymentCompletedColumn1810000000004 implements MigrationInterface { - name = 'AddPostPaymentCompletedColumn1810000000004'; - - public async up(queryRunner: QueryRunner): Promise { - const firstMileTable = await queryRunner.hasTable('freight.first_mile_deliveries'); - if (firstMileTable) { - const hasColumn = await queryRunner.hasColumn('freight.first_mile_deliveries', 'is_post_payment_completed'); - if (!hasColumn) { - await queryRunner.addColumn( - 'freight.first_mile_deliveries', - new TableColumn({ - name: 'is_post_payment_completed', - type: 'boolean', - default: false, - isNullable: false, - }) - ); - } - } - - const lastMileTable = await queryRunner.hasTable('freight.last_mile_deliveries'); - if (lastMileTable) { - const hasColumn = await queryRunner.hasColumn('freight.last_mile_deliveries', 'is_post_payment_completed'); - if (!hasColumn) { - await queryRunner.addColumn( - 'freight.last_mile_deliveries', - new TableColumn({ - name: 'is_post_payment_completed', - type: 'boolean', - default: false, - isNullable: false, - }) - ); - } - } - } - - public async down(queryRunner: QueryRunner): Promise { - const lastMileTable = await queryRunner.hasTable('freight.last_mile_deliveries'); - if (lastMileTable) { - await queryRunner.dropColumn('freight.last_mile_deliveries', 'is_post_payment_completed'); - } - - const firstMileTable = await queryRunner.hasTable('freight.first_mile_deliveries'); - if (firstMileTable) { - await queryRunner.dropColumn('freight.first_mile_deliveries', 'is_post_payment_completed'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000000-DropAllowConsolidation.ts b/apps/edr-freight-api/src/migrations/1820000000000-DropAllowConsolidation.ts deleted file mode 100644 index dd232b28a..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000000-DropAllowConsolidation.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Consolidation is now system-managed: the backend consolidates partial-wagon - * container bookings automatically, derived from the container quantities. The - * `allow_consolidation` opt-in flag is therefore redundant and is dropped. - * `consolidation_partner_id` (the actual pairing link) is unaffected. - */ -export class DropAllowConsolidation1820000000000 implements MigrationInterface { - name = 'DropAllowConsolidation1820000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS allow_consolidation;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS allow_consolidation BOOLEAN NOT NULL DEFAULT false;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000001-CreateContractRouteLines.ts b/apps/edr-freight-api/src/migrations/1820000000001-CreateContractRouteLines.ts deleted file mode 100644 index 23eda1730..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000001-CreateContractRouteLines.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -/** - * Multi-route general contracts: a contract may reserve quantity across several - * routes. Each (contract, route, container type) is a row here; drawdown orders - * reference the route line they drew from via booking_orders.route_line_id. - */ -export class CreateContractRouteLines1820000000001 - implements MigrationInterface -{ - name = 'CreateContractRouteLines1820000000001'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'contract_route_lines', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'contract_booking_id', type: 'uuid' }, - { name: 'origin_yard_id', type: 'uuid' }, - { name: 'destination_yard_id', type: 'uuid' }, - { name: 'container_type_id', type: 'uuid', isNullable: true }, - { name: 'quantity', type: 'numeric', precision: 12, scale: 3 }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['contract_booking_id'], - referencedSchema: 'freight', - referencedTableName: 'bookings', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.contract_route_lines', - new TableIndex({ - name: 'idx_contract_route_lines_contract', - columnNames: ['contract_booking_id'], - }), - ); - - await queryRunner.query( - `ALTER TABLE freight.booking_orders ADD COLUMN IF NOT EXISTS route_line_id uuid;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.booking_orders DROP COLUMN IF EXISTS route_line_id;`, - ); - await queryRunner.dropTable('freight.contract_route_lines', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000002-CreateBookingDocumentReview.ts b/apps/edr-freight-api/src/migrations/1820000000002-CreateBookingDocumentReview.ts deleted file mode 100644 index 0237648f7..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000002-CreateBookingDocumentReview.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -/** - * Per-document GL review for the post-counter-sign clearance gate. One row per - * required clearance document; GL marks each APPROVED or QUERIED before the - * booking can proceed to operations. - */ -export class CreateBookingDocumentReview1820000000002 - implements MigrationInterface -{ - name = 'CreateBookingDocumentReview1820000000002'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'booking_document_review', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'booking_id', type: 'uuid' }, - { name: 'setting_code', type: 'varchar', length: '128' }, - { name: 'file_key', type: 'varchar', length: '128' }, - { name: 'file_record_id', type: 'uuid', isNullable: true }, - { name: 'status', type: 'varchar', length: '20', default: "'PENDING'" }, - { name: 'note', type: 'text', isNullable: true }, - { name: 'reviewed_by_staff_id', type: 'uuid', isNullable: true }, - { name: 'reviewed_at', type: 'timestamptz', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['booking_id'], - referencedSchema: 'freight', - referencedTableName: 'bookings', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.booking_document_review', - new TableIndex({ name: 'idx_booking_document_review_booking', columnNames: ['booking_id'] }), - ); - await queryRunner.createIndex( - 'freight.booking_document_review', - new TableIndex({ name: 'idx_booking_document_review_status', columnNames: ['status'] }), - ); - await queryRunner.createIndex( - 'freight.booking_document_review', - new TableIndex({ - name: 'uq_booking_document_review_doc', - columnNames: ['booking_id', 'setting_code', 'file_key'], - isUnique: true, - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.booking_document_review', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000003-AddPriceAdjustment.ts b/apps/edr-freight-api/src/migrations/1820000000003-AddPriceAdjustment.ts deleted file mode 100644 index 7ad97e7bf..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000003-AddPriceAdjustment.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Staff price adjustment: an optional override of a booking's computed total, - * with who/when/why. When set, the customer sees the adjusted total + a badge. - */ -export class AddPriceAdjustment1820000000003 implements MigrationInterface { - name = 'AddPriceAdjustment1820000000003'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjusted_total_amount numeric(14,2);`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjusted_by_staff_id uuid;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjusted_at timestamptz;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjustment_reason text;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjustment_reason;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjusted_at;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjusted_by_staff_id;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjusted_total_amount;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000004-FoldSurchargeTypesIntoRates.ts b/apps/edr-freight-api/src/migrations/1820000000004-FoldSurchargeTypesIntoRates.ts deleted file mode 100644 index d4eadf34e..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000004-FoldSurchargeTypesIntoRates.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Fold the `surcharge_types` table into self-describing rates. - * - * Previously a surcharge was a separate row {trigger_condition, rate_id}. Now - * each rate carries its own `applies_to` (friendly category) and `trigger` - * (ALWAYS = base freight, otherwise a surcharge condition), plus an optional - * `cargo_type_id` for bulk leaf commodities. The rule engine reads triggers - * directly off LIVE rates, so the join table is no longer needed. - * - * This migration: - * 1. adds applies_to / trigger / cargo_type_id to rates and backfills them - * from the existing rate_type matrix, - * 2. repoints booking_cargo_modifier from surcharge_type_id → rate_id - * (backfilled via surcharge_types.rate_id), - * 3. drops surcharge_types and its FK. - */ -export class FoldSurchargeTypesIntoRates1820000000004 implements MigrationInterface { - name = 'FoldSurchargeTypesIntoRates1820000000004'; - - public async up(queryRunner: QueryRunner): Promise { - // ── 1. New rate columns ──────────────────────────────────────────────── - await queryRunner.query(` - ALTER TABLE freight.rates - ADD COLUMN IF NOT EXISTS applies_to varchar(20) NOT NULL DEFAULT 'OTHER', - ADD COLUMN IF NOT EXISTS "trigger" varchar(20) NOT NULL DEFAULT 'ALWAYS', - ADD COLUMN IF NOT EXISTS cargo_type_id uuid NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.rates - ADD CONSTRAINT "FK_rates_cargo_type_id" - FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id) - ON DELETE SET NULL; - `); - - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_rates_trigger" ON freight.rates ("trigger");`, - ); - - // ── 1a. Backfill applies_to from the legacy rate_type matrix ──────────── - await queryRunner.query(` - UPDATE freight.rates SET applies_to = CASE - WHEN rate_type IN ('CONTAINER_IMPORT','CONTAINER_EXPORT','CONTAINER_WITH_RETURN') THEN 'CONTAINER' - WHEN rate_type IN ('BULK_IMPORT','BULK_EXPORT') THEN 'BULK' - WHEN rate_type IN ('INTERCITY_CONTAINER','INTERCITY_BULK') THEN 'INTERCITY' - WHEN rate_type = 'FIRST_MILE' THEN 'FIRST_MILE' - WHEN rate_type = 'LAST_MILE' THEN 'LAST_MILE' - ELSE 'OTHER' - END; - `); - - // ── 1b. Backfill trigger from the legacy rate_type matrix ─────────────── - await queryRunner.query(` - UPDATE freight.rates SET "trigger" = CASE - WHEN rate_type = 'HAZARD_SURCHARGE' THEN 'HAZARDOUS' - WHEN rate_type = 'REEFER_SURCHARGE' THEN 'REEFER' - WHEN rate_type = 'OVERWEIGHT_PER_TON' THEN 'OVERWEIGHT' - WHEN rate_type = 'DOUBLE_HANDLING' THEN 'SHIPPING_LINE' - WHEN rate_type = 'LASHING' THEN 'CONSOLIDATION' - WHEN rate_type = 'CANCELLATION_FEE' THEN 'CANCELLATION' - WHEN rate_type = 'DEMURRAGE' THEN 'DEMURRAGE' - WHEN rate_type = 'PIL_EXTRA_FEE' THEN 'PIL_EXTRA_FEE' - ELSE 'ALWAYS' - END; - `); - - // Align the trigger to the actual surcharge_types mapping where one exists - // (covers any rate wired as a surcharge with a non-obvious rate_type). - await queryRunner.query(` - UPDATE freight.rates r SET "trigger" = m.trig - FROM ( - SELECT st.rate_id, CASE st.trigger_condition - WHEN 'CARGO_FLAG_HAZARDOUS' THEN 'HAZARDOUS' - WHEN 'CARGO_FLAG_REEFER' THEN 'REEFER' - WHEN 'VGM_EXCEEDS_LIMIT' THEN 'OVERWEIGHT' - WHEN 'SHIPPING_LINE_MAPPED' THEN 'SHIPPING_LINE' - WHEN 'CONSOLIDATION_ENABLED' THEN 'CONSOLIDATION' - ELSE 'ALWAYS' - END AS trig - FROM freight.surcharge_types st - WHERE st.rate_id IS NOT NULL AND st.deleted_at IS NULL - ) m - WHERE r.id = m.rate_id AND m.trig <> 'ALWAYS'; - `); - - // ── 2. Repoint booking_cargo_modifier to rate_id ──────────────────────── - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - ADD COLUMN IF NOT EXISTS rate_id uuid NULL; - `); - - await queryRunner.query(` - UPDATE freight.booking_cargo_modifier bcm - SET rate_id = st.rate_id - FROM freight.surcharge_types st - WHERE bcm.surcharge_type_id = st.id AND st.rate_id IS NOT NULL; - `); - - // Rows whose surcharge lost its rate can't be repointed — they reference a - // now-defunct surcharge. Remove them so the NOT NULL + FK can be enforced. - await queryRunner.query(` - DELETE FROM freight.booking_cargo_modifier WHERE rate_id IS NULL; - `); - - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - ALTER COLUMN rate_id SET NOT NULL; - `); - - // Drop the old FK + column + index for surcharge_type_id. - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_surcharge_type_id"; - `); - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_booking_cargo_modifier_surcharge_type_id";`, - ); - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - DROP COLUMN IF EXISTS surcharge_type_id; - `); - - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - ADD CONSTRAINT "FK_booking_cargo_modifier_rate_id" - FOREIGN KEY (rate_id) REFERENCES freight.rates(id); - `); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_booking_cargo_modifier_rate_id" ON freight.booking_cargo_modifier (rate_id);`, - ); - - // ── 3. Drop the surcharge_types table ─────────────────────────────────── - await queryRunner.query(`DROP TABLE IF EXISTS freight.surcharge_types;`); - } - - public async down(queryRunner: QueryRunner): Promise { - // Recreate surcharge_types (structure only — data is not restored). - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.surcharge_types ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - code varchar(40) NOT NULL, - label varchar(100), - trigger_condition varchar(50), - rate_id uuid, - is_active boolean NOT NULL DEFAULT true, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ); - `); - await queryRunner.query( - `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_surcharge_types_code" ON freight.surcharge_types (code);`, - ); - - // Restore booking_cargo_modifier.surcharge_type_id (nullable; not backfilled). - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_rate_id"; - `); - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_booking_cargo_modifier_rate_id";`, - ); - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier - ADD COLUMN IF NOT EXISTS surcharge_type_id uuid NULL; - `); - await queryRunner.query(` - ALTER TABLE freight.booking_cargo_modifier DROP COLUMN IF EXISTS rate_id; - `); - - // Drop the new rate columns. - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_rates_trigger";`, - ); - await queryRunner.query(` - ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "FK_rates_cargo_type_id"; - `); - await queryRunner.query(` - ALTER TABLE freight.rates - DROP COLUMN IF EXISTS cargo_type_id, - DROP COLUMN IF EXISTS "trigger", - DROP COLUMN IF EXISTS applies_to; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000005-AddContractValidityWindow.ts b/apps/edr-freight-api/src/migrations/1820000000005-AddContractValidityWindow.ts deleted file mode 100644 index 56d923ff7..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000005-AddContractValidityWindow.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Contract validity window. When the backoffice accepts a price-confirmed - * booking, staff define how many days the contract stays valid. The window runs - * from the accept moment (valid_from) through valid_from + N days (valid_until). - * Outside that window the contract is considered expired. - */ -export class AddContractValidityWindow1820000000005 - implements MigrationInterface -{ - name = 'AddContractValidityWindow1820000000005'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_validity_days integer;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_valid_from timestamptz;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_valid_until timestamptz;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_valid_until;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_valid_from;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_validity_days;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000006-AddCustomsAgentAndMileCoordinates.ts b/apps/edr-freight-api/src/migrations/1820000000006-AddCustomsAgentAndMileCoordinates.ts deleted file mode 100644 index 11d1b617a..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000006-AddCustomsAgentAndMileCoordinates.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Booking now captures: - * - customs clearing as an explicit flag + the customs clearing agent name - * (shown when the service includes customs), and - * - first/last-mile pickup & delivery coordinates (lat/lng) alongside the - * existing address text, so the map picker can store and restore the pin. - * - * Shipping line is no longer collected from the booking form; the column stays - * for historical data and the (now dormant) shipping-line pricing trigger. - */ -export class AddCustomsAgentAndMileCoordinates1820000000006 - implements MigrationInterface -{ - name = 'AddCustomsAgentAndMileCoordinates1820000000006'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS first_mile_pickup_lat numeric(10,7) NULL, - ADD COLUMN IF NOT EXISTS first_mile_pickup_lng numeric(10,7) NULL, - ADD COLUMN IF NOT EXISTS last_mile_delivery_lat numeric(10,7) NULL, - ADD COLUMN IF NOT EXISTS last_mile_delivery_lng numeric(10,7) NULL, - ADD COLUMN IF NOT EXISTS customs_clearing_enabled boolean NOT NULL DEFAULT false, - ADD COLUMN IF NOT EXISTS customs_clearing_agent varchar(200) NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS customs_clearing_agent, - DROP COLUMN IF EXISTS customs_clearing_enabled, - DROP COLUMN IF EXISTS last_mile_delivery_lng, - DROP COLUMN IF EXISTS last_mile_delivery_lat, - DROP COLUMN IF EXISTS first_mile_pickup_lng, - DROP COLUMN IF EXISTS first_mile_pickup_lat; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000010-AddGeneralContractOrderFields.ts b/apps/edr-freight-api/src/migrations/1820000000010-AddGeneralContractOrderFields.ts deleted file mode 100644 index a23c152b5..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000010-AddGeneralContractOrderFields.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * General-contract drawdown order fields: - * - booking_order_lines.hazardous_quantity / reefer_quantity — per-order counts - * the customer enters when toggling hazardous/reefer; drive the surcharge - * rates on the spawned child booking. - * - bookings.is_reefer — booking-level refrigerated flag so REEFER_SURCHARGE - * applies to a contract order even when the container type is not a reefer. - * - contract_route_lines.km — road distance configured with the route; road - * orders bill KM × the PER_KM rate. - * - * NOTE: the shared dev DB has no applied migration history, so these columns - * are also hand-applied there. ADD COLUMN IF NOT EXISTS keeps that idempotent. - */ -export class AddGeneralContractOrderFields1820000000010 - implements MigrationInterface -{ - name = 'AddGeneralContractOrderFields1820000000010'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.booking_order_lines ADD COLUMN IF NOT EXISTS hazardous_quantity numeric(12,3) NOT NULL DEFAULT 0;`, - ); - await queryRunner.query( - `ALTER TABLE freight.booking_order_lines ADD COLUMN IF NOT EXISTS reefer_quantity numeric(12,3) NOT NULL DEFAULT 0;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS is_reefer boolean NOT NULL DEFAULT false;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contract_route_lines ADD COLUMN IF NOT EXISTS km numeric(10,2);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_route_lines DROP COLUMN IF EXISTS km;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS is_reefer;`, - ); - await queryRunner.query( - `ALTER TABLE freight.booking_order_lines DROP COLUMN IF EXISTS reefer_quantity;`, - ); - await queryRunner.query( - `ALTER TABLE freight.booking_order_lines DROP COLUMN IF EXISTS hazardous_quantity;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000011-AddTrainSetLocomotives.ts b/apps/edr-freight-api/src/migrations/1820000000011-AddTrainSetLocomotives.ts deleted file mode 100644 index b014e71a2..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000011-AddTrainSetLocomotives.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Multi-locomotive train sets: a train set is now pulled by 2+ locomotives. - * - * Adds the `freight.train_set_locomotives` link table (train set ⇄ locomotive, - * with an order index) and backfills one row per existing train set from its - * current `locomotive_id`, so existing read paths keep resolving locomotives. - * The `train_sets.locomotive_id` column is retained as the "primary" locomotive. - * - * NOTE: the shared dev DB has no applied migration history, so this is also - * hand-applied there. IF NOT EXISTS keeps that idempotent. - */ -export class AddTrainSetLocomotives1820000000011 implements MigrationInterface { - name = 'AddTrainSetLocomotives1820000000011'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.train_set_locomotives ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - train_set_id uuid NOT NULL, - locomotive_id uuid NOT NULL, - sequence_no int NOT NULL DEFAULT 0, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT "PK_train_set_locomotives" PRIMARY KEY (id), - CONSTRAINT "FK_train_set_locomotives_train_set" FOREIGN KEY (train_set_id) - REFERENCES freight.train_sets (id) ON DELETE CASCADE, - CONSTRAINT "FK_train_set_locomotives_locomotive" FOREIGN KEY (locomotive_id) - REFERENCES freight.locomotives (id) - ); - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_train_set_locomotives_set_loco" - ON freight.train_set_locomotives (train_set_id, locomotive_id); - `); - - // Backfill: one link row per existing train set, from its current primary loco. - await queryRunner.query(` - INSERT INTO freight.train_set_locomotives (train_set_id, locomotive_id, sequence_no) - SELECT ts.id, ts.locomotive_id, 0 - FROM freight.train_sets ts - WHERE ts.locomotive_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM freight.train_set_locomotives tsl - WHERE tsl.train_set_id = ts.id AND tsl.locomotive_id = ts.locomotive_id - ); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight."UQ_train_set_locomotives_set_loco";`, - ); - await queryRunner.query(`DROP TABLE IF EXISTS freight.train_set_locomotives;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000011-DropEmailPhoneFromExternalProfiles.ts b/apps/edr-freight-api/src/migrations/1820000000011-DropEmailPhoneFromExternalProfiles.ts deleted file mode 100644 index 757c20720..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000011-DropEmailPhoneFromExternalProfiles.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Contact email/phone for an external profile is sourced from IAM (the user's - * identity) and from the company record, so the duplicated `email`/`phone` - * columns on external_profiles are redundant and are dropped. Dropping `email` - * also removes its UNIQUE constraint. - */ -export class DropEmailPhoneFromExternalProfiles1820000000011 - implements MigrationInterface -{ - name = 'DropEmailPhoneFromExternalProfiles1820000000011'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.external_profiles DROP COLUMN IF EXISTS email;`, - ); - await queryRunner.query( - `ALTER TABLE freight.external_profiles DROP COLUMN IF EXISTS phone;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - // Re-added as nullable (the original email was UNIQUE NOT NULL) since the - // dropped values cannot be recovered to satisfy those constraints. - await queryRunner.query( - `ALTER TABLE freight.external_profiles ADD COLUMN IF NOT EXISTS email varchar(150);`, - ); - await queryRunner.query( - `ALTER TABLE freight.external_profiles ADD COLUMN IF NOT EXISTS phone varchar(20);`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1820000000012-AddEstimatedShipmentDate.ts b/apps/edr-freight-api/src/migrations/1820000000012-AddEstimatedShipmentDate.ts deleted file mode 100644 index 6b77f53a3..000000000 --- a/apps/edr-freight-api/src/migrations/1820000000012-AddEstimatedShipmentDate.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * The booking wizard now captures a NON-BINDING estimated shipment date instead - * of the binding scheduledDate. The binding scheduledDate (validated against - * open train departures) is set later, at the operation-request step. - */ -export class AddEstimatedShipmentDate1820000000012 - implements MigrationInterface -{ - name = 'AddEstimatedShipmentDate1820000000012'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS estimated_shipment_date timestamptz NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS estimated_shipment_date; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1821000000000-CreateInterchangeDocuments.ts b/apps/edr-freight-api/src/migrations/1821000000000-CreateInterchangeDocuments.ts deleted file mode 100644 index 386eabd4e..000000000 --- a/apps/edr-freight-api/src/migrations/1821000000000-CreateInterchangeDocuments.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { MigrationInterface, QueryRunner, Table } from 'typeorm'; - -export class CreateInterchangeDocuments1821000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'interchange_documents', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' }, - { name: 'document_no', type: 'varchar', length: '40', isUnique: true }, - { name: 'direction', type: 'varchar', length: '10' }, - { name: 'schedule_id', type: 'uuid', isNullable: true }, - { name: 'train_no', type: 'varchar', length: '40', isNullable: true }, - { name: 'route_id', type: 'uuid', isNullable: true }, - { name: 'origin_facility_id', type: 'uuid', isNullable: true }, - { name: 'destination_facility_id', type: 'uuid', isNullable: true }, - { name: 'handover_location', type: 'varchar', length: '255' }, - { name: 'handover_from', type: 'varchar', length: '255' }, - { name: 'handover_to', type: 'varchar', length: '255' }, - { name: 'operator_name', type: 'varchar', length: '255', isNullable: true }, - { name: 'port_operator_name', type: 'varchar', length: '255', isNullable: true }, - { name: 'shipping_line_name', type: 'varchar', length: '255', isNullable: true }, - { name: 'customs_reference', type: 'varchar', length: '120', isNullable: true }, - { name: 'manifest_reference', type: 'varchar', length: '120', isNullable: true }, - { name: 'status', type: 'varchar', length: '20', default: "'DRAFT'" }, - { name: 'generated_at', type: 'timestamptz', isNullable: true }, - { name: 'acknowledged_at', type: 'timestamptz', isNullable: true }, - { name: 'generated_by', type: 'varchar', length: '120', isNullable: true }, - { name: 'acknowledged_by', type: 'varchar', length: '120', isNullable: true }, - { name: 'remarks', type: 'text', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - indices: [ - { name: 'idx_interchange_documents_direction', columnNames: ['direction'] }, - { name: 'idx_interchange_documents_status', columnNames: ['status'] }, - { name: 'idx_interchange_documents_schedule', columnNames: ['schedule_id'] }, - ], - }), - true, - ); - - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'interchange_document_items', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' }, - { name: 'interchange_document_id', type: 'uuid' }, - { name: 'booking_id', type: 'uuid', isNullable: true }, - { name: 'booking_reference', type: 'varchar', length: '64', isNullable: true }, - { name: 'item_type', type: 'varchar', length: '20' }, - { name: 'booking_container_id', type: 'uuid', isNullable: true }, - { name: 'booking_cargo_id', type: 'uuid', isNullable: true }, - { name: 'container_number', type: 'varchar', length: '64', isNullable: true }, - { name: 'seal_number', type: 'varchar', length: '100', isNullable: true }, - { name: 'cargo_id', type: 'uuid', isNullable: true }, - { name: 'cargo_type', type: 'varchar', length: '255', isNullable: true }, - { name: 'cargo_description', type: 'text', isNullable: true }, - { name: 'weight', type: 'numeric', precision: 14, scale: 3, isNullable: true }, - { name: 'quantity', type: 'numeric', precision: 12, scale: 3, isNullable: true }, - { name: 'package_count', type: 'int', isNullable: true }, - { name: 'wagon_number', type: 'varchar', length: '80', isNullable: true }, - { name: 'condition_status', type: 'varchar', length: '20', default: "'GOOD'" }, - { name: 'damage_description', type: 'text', isNullable: true }, - { name: 'remarks', type: 'text', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['interchange_document_id'], - referencedSchema: 'freight', - referencedTableName: 'interchange_documents', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }, - { - columnNames: ['booking_id'], - referencedSchema: 'freight', - referencedTableName: 'bookings', - referencedColumnNames: ['id'], - onDelete: 'SET NULL', - }, - ], - indices: [ - { name: 'idx_interchange_items_document', columnNames: ['interchange_document_id'] }, - { name: 'idx_interchange_items_booking', columnNames: ['booking_id'] }, - ], - }), - true, - ); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS uq_interchange_active_schedule_direction - ON freight.interchange_documents(schedule_id, direction) - WHERE schedule_id IS NOT NULL AND status <> 'CANCELLED' AND deleted_at IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('DROP INDEX IF EXISTS freight.uq_interchange_active_schedule_direction;'); - await queryRunner.dropTable('freight.interchange_document_items', true); - await queryRunner.dropTable('freight.interchange_documents', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1821000000001-EnsureWarehouseInventoryInspectionStatus.ts b/apps/edr-freight-api/src/migrations/1821000000001-EnsureWarehouseInventoryInspectionStatus.ts deleted file mode 100644 index 523772b39..000000000 --- a/apps/edr-freight-api/src/migrations/1821000000001-EnsureWarehouseInventoryInspectionStatus.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -/** - * Catch-up for environments where AddWarehouseInspection ran before the - * warehouse module table existed. Production needs this column for unload and - * inspection flows because the WarehouseInventory entity maps inspectionStatus. - */ -export class EnsureWarehouseInventoryInspectionStatus1821000000001 implements MigrationInterface { - private readonly table = 'freight.warehouse_inventory'; - - public async up(queryRunner: QueryRunner): Promise { - if (!(await queryRunner.hasColumn(this.table, 'inspection_status'))) { - await queryRunner.addColumn( - this.table, - new TableColumn({ - name: 'inspection_status', - type: 'varchar', - length: '20', - isNullable: true, - }), - ); - } - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_inventory_inspection_status - ON freight.warehouse_inventory(inspection_status) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DROP INDEX IF EXISTS freight.idx_warehouse_inventory_inspection_status - `); - - if (await queryRunner.hasColumn(this.table, 'inspection_status')) { - await queryRunner.dropColumn(this.table, 'inspection_status'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1821000000002-AddDistanceColumnsToVehicles.ts b/apps/edr-freight-api/src/migrations/1821000000002-AddDistanceColumnsToVehicles.ts deleted file mode 100644 index 98fa8b228..000000000 --- a/apps/edr-freight-api/src/migrations/1821000000002-AddDistanceColumnsToVehicles.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddDistanceColumnsToVehicles1821000000002 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - ADD COLUMN IF NOT EXISTS estimated_distance_km NUMERIC, - ADD COLUMN IF NOT EXISTS actual_distance_km NUMERIC; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - DROP COLUMN IF EXISTS estimated_distance_km, - DROP COLUMN IF EXISTS actual_distance_km; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1821000000002-CreateInvoices.ts b/apps/edr-freight-api/src/migrations/1821000000002-CreateInvoices.ts deleted file mode 100644 index 4c2fb5d95..000000000 --- a/apps/edr-freight-api/src/migrations/1821000000002-CreateInvoices.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Freight billing — `invoices` + `invoice_lines` tables. - * - * Matches: - * - billing/entities/invoice.entity.ts - * - billing/entities/invoice-line.entity.ts - * - * The status enum mirrors `Freight.InvoiceStatus` and uses TypeORM's default - * enum-type name (`__enum`) so the entity's `type: "enum"` - * column resolves to it without an explicit `enumName`. - */ -export class CreateInvoices1821000000002 implements MigrationInterface { - name = "CreateInvoices1821000000002"; - - public async up(queryRunner: QueryRunner): Promise { - const typeExists = await queryRunner.query( - `SELECT 1 FROM pg_type WHERE typname = 'invoices_status_enum' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'freight');`, - ); - - if (!typeExists.length) { - await queryRunner.query(` - CREATE TYPE freight.invoices_status_enum AS ENUM ( - 'DRAFT', - 'PENDING', - 'PAID', - 'OVERDUE', - 'CANCELLED', - 'REFUNDED' - ); - `); - } - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.invoices ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - invoice_number varchar(64) NOT NULL, - company_id uuid NOT NULL, - company_profile_id uuid NOT NULL, - total_amount numeric(14, 2) NOT NULL, - currency varchar(8) NOT NULL DEFAULT 'ETB', - status freight.invoices_status_enum NOT NULL DEFAULT 'DRAFT', - source varchar(255) NOT NULL, - source_id varchar(255) NOT NULL, - type varchar(255) NOT NULL, - issued_at timestamptz, - payment_id uuid, - due_at timestamptz NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT pk_invoices PRIMARY KEY (id), - CONSTRAINT uq_invoices_invoice_number UNIQUE (invoice_number), - CONSTRAINT fk_invoices_company FOREIGN KEY (company_id) - REFERENCES freight.companies (id) ON DELETE RESTRICT, - CONSTRAINT fk_invoices_company_profile FOREIGN KEY (company_profile_id) - REFERENCES freight.company_profiles (id) ON DELETE RESTRICT, - CONSTRAINT fk_invoices_payment FOREIGN KEY (payment_id) - REFERENCES freight.payments (id) ON DELETE SET NULL - ); - `); - - await queryRunner.query( - ` - ALTER TABLE freight.invoices - ADD COLUMN IF NOT EXISTS id uuid DEFAULT uuid_generate_v4(), - ADD COLUMN IF NOT EXISTS invoice_number varchar(64), - ADD COLUMN IF NOT EXISTS company_id uuid, - ADD COLUMN IF NOT EXISTS company_profile_id uuid, - ADD COLUMN IF NOT EXISTS total_amount numeric(14, 2) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS currency varchar(8) NOT NULL DEFAULT 'ETB', - ADD COLUMN IF NOT EXISTS status freight.invoices_status_enum NOT NULL DEFAULT 'DRAFT', - ADD COLUMN IF NOT EXISTS source varchar(255), - ADD COLUMN IF NOT EXISTS source_id varchar(255), - ADD COLUMN IF NOT EXISTS type varchar(255), - ADD COLUMN IF NOT EXISTS issued_at timestamptz, - ADD COLUMN IF NOT EXISTS payment_id uuid, - ADD COLUMN IF NOT EXISTS due_at timestamptz, - ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(), - ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(), - ADD COLUMN IF NOT EXISTS deleted_at timestamptz; - `, - ); - await queryRunner.query(` - UPDATE freight.invoices - SET due_at = COALESCE(due_at, issued_at, created_at, now()) - WHERE due_at IS NULL; - `); - await queryRunner.query(`ALTER TABLE freight.invoices ALTER COLUMN due_at SET NOT NULL;`); - - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE contype = 'p' - AND conrelid = 'freight.invoices'::regclass - ) THEN - ALTER TABLE freight.invoices ADD CONSTRAINT pk_invoices PRIMARY KEY (id); - END IF; - - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conname = 'uq_invoices_invoice_number' - AND conrelid = 'freight.invoices'::regclass - ) THEN - ALTER TABLE freight.invoices ADD CONSTRAINT uq_invoices_invoice_number UNIQUE (invoice_number); - END IF; - - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conname = 'fk_invoices_company' - AND conrelid = 'freight.invoices'::regclass - ) THEN - ALTER TABLE freight.invoices ADD CONSTRAINT fk_invoices_company - FOREIGN KEY (company_id) REFERENCES freight.companies (id) ON DELETE RESTRICT; - END IF; - - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conname = 'fk_invoices_company_profile' - AND conrelid = 'freight.invoices'::regclass - ) THEN - ALTER TABLE freight.invoices ADD CONSTRAINT fk_invoices_company_profile - FOREIGN KEY (company_profile_id) REFERENCES freight.company_profiles (id) ON DELETE RESTRICT; - END IF; - - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conname = 'fk_invoices_payment' - AND conrelid = 'freight.invoices'::regclass - ) THEN - ALTER TABLE freight.invoices ADD CONSTRAINT fk_invoices_payment - FOREIGN KEY (payment_id) REFERENCES freight.payments (id) ON DELETE SET NULL; - END IF; - END $$; - `); - - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_invoices_company ON freight.invoices (company_id);`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_invoices_company_profile ON freight.invoices (company_profile_id);`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_invoices_source ON freight.invoices (source, source_id);`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_invoices_status ON freight.invoices (status);`, - ); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.invoice_lines ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - invoice_id uuid NOT NULL, - charge_type varchar NOT NULL, - description varchar(255), - quantity numeric(12, 2) NOT NULL DEFAULT 1, - unit_rate numeric(14, 2) NOT NULL DEFAULT 0, - amount numeric(14, 2) NOT NULL, - currency varchar(8) NOT NULL DEFAULT 'ETB', - metadata jsonb, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT pk_invoice_lines PRIMARY KEY (id), - CONSTRAINT fk_invoice_lines_invoice FOREIGN KEY (invoice_id) - REFERENCES freight.invoices (id) ON DELETE CASCADE - ); - `); - - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_invoice_lines_invoice ON freight.invoice_lines (invoice_id);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.invoice_lines;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.invoices;`); - await queryRunner.query( - `DROP TYPE IF EXISTS freight.invoices_status_enum;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1821000000003-AddCompanyKindAndGovBookingLinks.ts b/apps/edr-freight-api/src/migrations/1821000000003-AddCompanyKindAndGovBookingLinks.ts deleted file mode 100644 index 95ea3db1b..000000000 --- a/apps/edr-freight-api/src/migrations/1821000000003-AddCompanyKindAndGovBookingLinks.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Government bookings now bill to a real seeded government company + an explicit - * importer/exporter profile, instead of carrying a null company + free-text - * institution. This migration: - * - * 1. Adds `companies.kind` (commercial | government). - * 2. Seeds the Ethiopian government entities + their importer/exporter - * profiles (mirrors src/seed/data/gov-companies.data.ts — keep in sync). - * 3. Backfills every booking with a NULL company_id / company_profile_id so - * the NOT NULL constraints below can be applied: - * - NULL company_id → the default government company. - * - NULL company_profile_id → the company's profile matching the booking - * trade direction; else any profile of the company; else the default - * government importer profile. - * 4. Enforces NOT NULL on bookings.company_id and bookings.company_profile_id. - */ -export class AddCompanyKindAndGovBookingLinks1821000000003 - implements MigrationInterface -{ - name = "AddCompanyKindAndGovBookingLinks1821000000003"; - - // Mirrors src/seed/data/gov-companies.data.ts - private readonly govCompanies = [ - { id: "0a1b0001-0000-4000-8000-000000000001", name: "Federal Government of Ethiopia", tin: "0000000001", email: "procurement@gov.et", phone: "+251111000001", im: "0b1c0001-0000-4000-8000-000000000001", ex: "0b1c0001-0000-4000-8000-000000000002", imRef: "IM-90001", exRef: "EX-90001" }, - { id: "0a1b0002-0000-4000-8000-000000000002", name: "Ministry of National Defense", tin: "0000000002", email: "logistics@mod.gov.et", phone: "+251111000002", im: "0b1c0002-0000-4000-8000-000000000001", ex: "0b1c0002-0000-4000-8000-000000000002", imRef: "IM-90002", exRef: "EX-90002" }, - { id: "0a1b0003-0000-4000-8000-000000000003", name: "Ethiopian Roads Administration", tin: "0000000003", email: "supply@era.gov.et", phone: "+251111000003", im: "0b1c0003-0000-4000-8000-000000000001", ex: "0b1c0003-0000-4000-8000-000000000002", imRef: "IM-90003", exRef: "EX-90003" }, - { id: "0a1b0004-0000-4000-8000-000000000004", name: "Ministry of Agriculture", tin: "0000000004", email: "imports@moa.gov.et", phone: "+251111000004", im: "0b1c0004-0000-4000-8000-000000000001", ex: "0b1c0004-0000-4000-8000-000000000002", imRef: "IM-90004", exRef: "EX-90004" }, - { id: "0a1b0005-0000-4000-8000-000000000005", name: "Ministry of Trade and Regional Integration", tin: "0000000005", email: "trade@motri.gov.et", phone: "+251111000005", im: "0b1c0005-0000-4000-8000-000000000001", ex: "0b1c0005-0000-4000-8000-000000000002", imRef: "IM-90005", exRef: "EX-90005" }, - { id: "0a1b0006-0000-4000-8000-000000000006", name: "Ethiopian Disaster Risk Management Commission", tin: "0000000006", email: "relief@edrmc.gov.et", phone: "+251111000006", im: "0b1c0006-0000-4000-8000-000000000001", ex: "0b1c0006-0000-4000-8000-000000000002", imRef: "IM-90006", exRef: "EX-90006" }, - ]; - - private get defaultCompanyId(): string { - return this.govCompanies[0].id; - } - private get defaultImporterProfileId(): string { - return this.govCompanies[0].im; - } - - public async up(queryRunner: QueryRunner): Promise { - // 1. kind column - await queryRunner.query( - `ALTER TABLE "freight"."companies" ADD COLUMN IF NOT EXISTS "kind" varchar(20) NOT NULL DEFAULT 'commercial'`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_companies_kind" ON "freight"."companies" ("kind")`, - ); - - // 2. seed government companies + importer/exporter profiles (idempotent) - for (const g of this.govCompanies) { - await queryRunner.query( - `INSERT INTO "freight"."companies" ("id", "name", "type", "kind", "status", "tin", "country", "email", "phone") - VALUES ($1, $2, 'customer', 'government', 'active', $3, 'Ethiopia', $4, $5) - ON CONFLICT ("id") DO NOTHING`, - [g.id, g.name, g.tin, g.email, g.phone], - ); - await queryRunner.query( - `INSERT INTO "freight"."company_profiles" ("id", "company_id", "type", "reference", "status") - VALUES ($1, $2, 'importer', $3, 'active'), ($4, $2, 'exporter', $5, 'active') - ON CONFLICT ("id") DO NOTHING`, - [g.im, g.id, g.imRef, g.ex, g.exRef], - ); - } - - // 3a. backfill NULL company_id → default government company - await queryRunner.query( - `UPDATE "freight"."bookings" SET "company_id" = $1 WHERE "company_id" IS NULL`, - [this.defaultCompanyId], - ); - - // 3b. backfill NULL company_profile_id → profile matching trade direction - await queryRunner.query( - `UPDATE "freight"."bookings" b - SET "company_profile_id" = cp."id" - FROM "freight"."company_profiles" cp - WHERE b."company_profile_id" IS NULL - AND cp."company_id" = b."company_id" - AND cp."deleted_at" IS NULL - AND cp."type" = CASE b."trade_direction" - WHEN 'IMPORT' THEN 'importer' - WHEN 'EXPORT' THEN 'exporter' - ELSE NULL END`, - ); - - // 3c. fallback → any profile of the booking's company - await queryRunner.query( - `UPDATE "freight"."bookings" b - SET "company_profile_id" = ( - SELECT cp."id" FROM "freight"."company_profiles" cp - WHERE cp."company_id" = b."company_id" AND cp."deleted_at" IS NULL - ORDER BY cp."created_at" ASC LIMIT 1) - WHERE b."company_profile_id" IS NULL - AND EXISTS ( - SELECT 1 FROM "freight"."company_profiles" cp - WHERE cp."company_id" = b."company_id" AND cp."deleted_at" IS NULL)`, - ); - - // 3d. final fallback → default government importer profile - await queryRunner.query( - `UPDATE "freight"."bookings" SET "company_profile_id" = $1 WHERE "company_profile_id" IS NULL`, - [this.defaultImporterProfileId], - ); - - // 4. enforce NOT NULL - await queryRunner.query( - `ALTER TABLE "freight"."bookings" ALTER COLUMN "company_id" SET NOT NULL`, - ); - await queryRunner.query( - `ALTER TABLE "freight"."bookings" ALTER COLUMN "company_profile_id" SET NOT NULL`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "freight"."bookings" ALTER COLUMN "company_profile_id" DROP NOT NULL`, - ); - await queryRunner.query( - `ALTER TABLE "freight"."bookings" ALTER COLUMN "company_id" DROP NOT NULL`, - ); - await queryRunner.query( - `DROP INDEX IF EXISTS "freight"."IDX_companies_kind"`, - ); - await queryRunner.query( - `ALTER TABLE "freight"."companies" DROP COLUMN IF EXISTS "kind"`, - ); - // Seeded government rows are intentionally left in place. - } -} diff --git a/apps/edr-freight-api/src/migrations/1821000000004-MakePaymentsTypeGeneric.ts b/apps/edr-freight-api/src/migrations/1821000000004-MakePaymentsTypeGeneric.ts deleted file mode 100644 index 9f0e8e7bd..000000000 --- a/apps/edr-freight-api/src/migrations/1821000000004-MakePaymentsTypeGeneric.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Make the payment projection source-agnostic so any domain (not just bookings) - * can own a payment intent. - * - * - `payments.type` enum `('booking')` → `varchar(50)`. It now stores the - * invoice SOURCE (e.g. 'booking', 'demurrage'), supplied by the caller, so a - * new domain no longer needs an enum migration to write its intents. - * - adds `payments.reference_type varchar(40)` — the gateway reference type - * (`PaymentReferenceType`) the intent was opened with, so the reconcile/poll - * path can query the provider without hardcoding it. - * - * Matches payment/entities/payment.entity.ts. - */ -export class MakePaymentsTypeGeneric1821000000004 implements MigrationInterface { - name = "MakePaymentsTypeGeneric1821000000004"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.payments ALTER COLUMN type TYPE varchar(50) USING type::text;`, - ); - await queryRunner.query(`DROP TYPE IF EXISTS freight.payments_type_enum;`); - - await queryRunner.query( - `ALTER TABLE freight.payments ADD COLUMN reference_type varchar(40);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.payments DROP COLUMN IF EXISTS reference_type;`, - ); - - // Restore the single-value enum. Any non-'booking' rows would block the cast; - // collapse them first so the down migration is safe. - await queryRunner.query( - `UPDATE freight.payments SET type = 'booking' WHERE type <> 'booking';`, - ); - await queryRunner.query( - `CREATE TYPE freight.payments_type_enum AS ENUM ('booking');`, - ); - await queryRunner.query( - `ALTER TABLE freight.payments ALTER COLUMN type TYPE freight.payments_type_enum USING type::freight.payments_type_enum;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1822000000000-CreateContracts.ts b/apps/edr-freight-api/src/migrations/1822000000000-CreateContracts.ts deleted file mode 100644 index 7995e5023..000000000 --- a/apps/edr-freight-api/src/migrations/1822000000000-CreateContracts.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Contract–Booking separation (additive phase). Introduces a first-class - * `freight.contracts` aggregate that owns the legal/commercial agreement (scope - * + unit rates, no quantities) and spawns shipment `bookings` via `contract_id`. - * - * Purely additive: no legacy columns are dropped here. The data backfill and - * legacy-column removal happen in a later cutover migration. - * - * See docs/new-doc.md §5. - */ -export class CreateContracts1822000000000 implements MigrationInterface { - name = 'CreateContracts1822000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // ── contracts ─────────────────────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contracts ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - reference VARCHAR(64) NOT NULL UNIQUE, - - company_id UUID, - company_profile_id UUID, - is_government BOOLEAN NOT NULL DEFAULT FALSE, - government_institution VARCHAR(255), - - contract_kind VARCHAR(20) NOT NULL, - renewal_of_id UUID REFERENCES freight.contracts(id), - trade_direction VARCHAR(10) NOT NULL, - freight_type VARCHAR(20) NOT NULL, - - service_type_id UUID NOT NULL, - payment_currency VARCHAR(5) NOT NULL, - customs_clearing_enabled BOOLEAN NOT NULL DEFAULT FALSE, - customs_clearing_agent VARCHAR(200), - equipment_return VARCHAR(20), - - first_mile_pickup_address TEXT, - first_mile_pickup_lat NUMERIC(10,7), - first_mile_pickup_lng NUMERIC(10,7), - last_mile_delivery_address TEXT, - last_mile_delivery_lat NUMERIC(10,7), - last_mile_delivery_lng NUMERIC(10,7), - - is_hazardous BOOLEAN NOT NULL DEFAULT FALSE, - is_reefer BOOLEAN NOT NULL DEFAULT FALSE, - - estimated_shipment_date TIMESTAMPTZ, - contract_validity_days INT, - contract_valid_from TIMESTAMPTZ, - contract_valid_until TIMESTAMPTZ, - expires_at TIMESTAMPTZ, - - status VARCHAR(40) NOT NULL DEFAULT 'DRAFT', - clearance_status VARCHAR(40) NOT NULL DEFAULT 'NOT_APPLICABLE', - clearance_cycle_number INT NOT NULL DEFAULT 0, - - pricing_breakdown JSONB, - pricing_display_mode VARCHAR(20) DEFAULT 'UNIT_RATES', - - contract_type VARCHAR(20), - contract_template_key VARCHAR(128), - contract_generated_at TIMESTAMPTZ, - contract_summary TEXT, - version_number INT NOT NULL DEFAULT 1, - financial_terms JSONB, - - approved_by_staff_id UUID, - approved_by_staff_at TIMESTAMPTZ, - signed_by_director_id UUID, - signed_by_director_at TIMESTAMPTZ, - signed_by_ceo_id UUID, - signed_by_ceo_at TIMESTAMPTZ, - customer_signed_at TIMESTAMPTZ, - fully_executed_at TIMESTAMPTZ, - locked_at TIMESTAMPTZ, - - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contracts_company ON freight.contracts(company_id);`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contracts_status ON freight.contracts(status);`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contracts_kind ON freight.contracts(contract_kind);`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contracts_valid_until ON freight.contracts(contract_valid_until);`); - - // ── contract_routes ────────────────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_routes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE, - origin_yard_id UUID NOT NULL, - destination_yard_id UUID NOT NULL, - km NUMERIC(10,2), - sort_order SMALLINT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ, - CONSTRAINT uq_contract_route UNIQUE (contract_id, origin_yard_id, destination_yard_id) - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_routes_contract ON freight.contract_routes(contract_id);`); - - // ── contract_cargo_scope ───────────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_cargo_scope ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE, - container_size VARCHAR(10), - cargo_type_id UUID, - cargo_free_text VARCHAR(200), - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ, - CONSTRAINT uq_contract_container_size UNIQUE NULLS NOT DISTINCT (contract_id, container_size) - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_cargo_scope_contract ON freight.contract_cargo_scope(contract_id);`); - - // ── contract_signatures ────────────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_signatures ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE, - role VARCHAR(20) NOT NULL, - signer_display_name VARCHAR(255) NOT NULL, - signature_file_id UUID, - consent_text TEXT, - signed_at TIMESTAMPTZ NOT NULL DEFAULT now(), - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_signatures_contract ON freight.contract_signatures(contract_id);`); - - // ── contract_approval_steps ────────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_approval_steps ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE, - step_order SMALLINT NOT NULL DEFAULT 0, - required_role VARCHAR(40) NOT NULL, - blocks_role VARCHAR(40), - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - acted_by_staff_id UUID, - acted_at TIMESTAMPTZ, - note TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_approval_steps_contract ON freight.contract_approval_steps(contract_id);`); - - // ── contract_rate_snapshots ────────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_rate_snapshots ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE, - rate_id UUID, - rate_code VARCHAR(64) NOT NULL, - description VARCHAR(255), - unit_price NUMERIC(14,2) NOT NULL, - unit_of_measure VARCHAR(32) NOT NULL, - currency VARCHAR(5) NOT NULL, - container_size VARCHAR(10), - is_surcharge BOOLEAN DEFAULT FALSE, - conditional_on VARCHAR(32), - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_rate_snapshots_contract ON freight.contract_rate_snapshots(contract_id);`); - - // ── contract_review_notes ──────────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_review_notes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE, - note_type VARCHAR(40) NOT NULL, - body TEXT NOT NULL, - author_role VARCHAR(20), - author_user_id UUID, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_review_notes_contract ON freight.contract_review_notes(contract_id);`); - - // ── contract_clearance_cycles ──────────────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_clearance_cycles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE, - cycle_number INT NOT NULL, - status VARCHAR(40) NOT NULL DEFAULT 'AWAITING_DOCUMENTS', - booking_id UUID, - started_at TIMESTAMPTZ NOT NULL DEFAULT now(), - clearance_ready_at TIMESTAMPTZ, - completed_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ, - CONSTRAINT uq_contract_clearance_cycle UNIQUE (contract_id, cycle_number) - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_clearance_cycles_contract ON freight.contract_clearance_cycles(contract_id);`); - - // ── contract_document_review (pre-booking clearance, Path B) ───────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_document_review ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE, - clearance_cycle_id UUID REFERENCES freight.contract_clearance_cycles(id), - setting_code VARCHAR(128) NOT NULL, - file_key VARCHAR(128) NOT NULL, - file_record_id UUID, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - note TEXT, - uploaded_by_role VARCHAR(20) NOT NULL DEFAULT 'CUSTOMER', - reviewed_by_staff_id UUID, - reviewed_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ, - CONSTRAINT uq_contract_document_review_doc - UNIQUE NULLS NOT DISTINCT (contract_id, clearance_cycle_id, setting_code, file_key) - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_doc_review_contract ON freight.contract_document_review(contract_id);`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_doc_review_status ON freight.contract_document_review(status);`); - - // ── clearance_milestones (GL tracking) ─────────────────────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.clearance_milestones ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - booking_id UUID REFERENCES freight.bookings(id) ON DELETE CASCADE, - contract_id UUID REFERENCES freight.contracts(id) ON DELETE CASCADE, - clearance_cycle_id UUID REFERENCES freight.contract_clearance_cycles(id), - milestone_code VARCHAR(64) NOT NULL, - milestone_label VARCHAR(255) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'PENDING', - owner_region VARCHAR(5), - triggered_by_doc BOOLEAN DEFAULT FALSE, - triggered_at TIMESTAMPTZ, - triggered_by_user_id UUID, - note TEXT, - sort_order SMALLINT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_clearance_milestones_booking ON freight.clearance_milestones(booking_id);`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_clearance_milestones_contract ON freight.clearance_milestones(contract_id);`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_clearance_milestones_region ON freight.clearance_milestones(owner_region, status);`); - // booking-scoped and contract-cycle-scoped uniqueness for milestone codes - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS uq_clearance_milestone_booking - ON freight.clearance_milestones(booking_id, milestone_code) WHERE booking_id IS NOT NULL; - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS uq_clearance_milestone_cycle - ON freight.clearance_milestones(clearance_cycle_id, milestone_code) WHERE clearance_cycle_id IS NOT NULL; - `); - - // ── booking_container_units (per-unit container detail) ────────────────── - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.booking_container_units ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - booking_container_id UUID NOT NULL REFERENCES freight.booking_container(id) ON DELETE CASCADE, - container_number VARCHAR(64) NOT NULL, - seal_number VARCHAR(64), - vgm_tons NUMERIC(10,3) NOT NULL, - is_hazardous BOOLEAN DEFAULT FALSE, - is_reefer BOOLEAN DEFAULT FALSE, - sort_order SMALLINT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ, - CONSTRAINT uq_booking_container_unit_number UNIQUE (booking_container_id, container_number) - ); - `); - - // ── ALTER bookings ─────────────────────────────────────────────────────── - await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_id UUID REFERENCES freight.contracts(id);`); - await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_route_id UUID REFERENCES freight.contract_routes(id);`); - await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS created_by_role VARCHAR(20) DEFAULT 'CUSTOMER';`); - await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS created_by_user_id UUID;`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_bookings_contract ON freight.bookings(contract_id);`); - // One active booking per ONE_TIME contract. Postgres forbids a subquery in an - // index predicate, so we denormalize the contract kind onto the booking and - // predicate on that. The column is stamped at booking creation from the - // contract; the app layer (ContractBookingService) is the primary guard and - // this index is the backstop. - await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_kind VARCHAR(20);`); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS uq_one_active_booking_per_one_time_contract - ON freight.bookings (contract_id) - WHERE status NOT IN ('EXPIRED', 'CANCELLED', 'COMPLETED', 'REJECTED') - AND contract_id IS NOT NULL - AND contract_kind = 'ONE_TIME'; - `); - - // ── ALTER booking_container ────────────────────────────────────────────── - await queryRunner.query(`ALTER TABLE freight.booking_container ADD COLUMN IF NOT EXISTS container_size VARCHAR(10);`); - await queryRunner.query(`ALTER TABLE freight.booking_container ADD COLUMN IF NOT EXISTS hazardous_quantity SMALLINT DEFAULT 0;`); - await queryRunner.query(`ALTER TABLE freight.booking_container ADD COLUMN IF NOT EXISTS reefer_quantity SMALLINT DEFAULT 0;`); - - // ── ALTER booking_document_review (denormalized contract link) ─────────── - await queryRunner.query(`ALTER TABLE freight.booking_document_review ADD COLUMN IF NOT EXISTS contract_id UUID REFERENCES freight.contracts(id);`); - - // ── Extend file_upload_fields with phased GL metadata ──────────────────── - await queryRunner.query(`ALTER TABLE freight.file_upload_fields ADD COLUMN IF NOT EXISTS phase VARCHAR(40);`); - await queryRunner.query(`ALTER TABLE freight.file_upload_fields ADD COLUMN IF NOT EXISTS owner_region VARCHAR(5);`); - await queryRunner.query(`ALTER TABLE freight.file_upload_fields ADD COLUMN IF NOT EXISTS trade_direction VARCHAR(10);`); - await queryRunner.query(`ALTER TABLE freight.file_upload_fields ADD COLUMN IF NOT EXISTS triggers_milestone_code VARCHAR(64);`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE freight.file_upload_fields DROP COLUMN IF EXISTS triggers_milestone_code;`); - await queryRunner.query(`ALTER TABLE freight.file_upload_fields DROP COLUMN IF EXISTS trade_direction;`); - await queryRunner.query(`ALTER TABLE freight.file_upload_fields DROP COLUMN IF EXISTS owner_region;`); - await queryRunner.query(`ALTER TABLE freight.file_upload_fields DROP COLUMN IF EXISTS phase;`); - - await queryRunner.query(`ALTER TABLE freight.booking_document_review DROP COLUMN IF EXISTS contract_id;`); - - await queryRunner.query(`ALTER TABLE freight.booking_container DROP COLUMN IF EXISTS reefer_quantity;`); - await queryRunner.query(`ALTER TABLE freight.booking_container DROP COLUMN IF EXISTS hazardous_quantity;`); - await queryRunner.query(`ALTER TABLE freight.booking_container DROP COLUMN IF EXISTS container_size;`); - - await queryRunner.query(`DROP INDEX IF EXISTS freight.uq_one_active_booking_per_one_time_contract;`); - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_contract;`); - await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_kind;`); - await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS created_by_user_id;`); - await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS created_by_role;`); - await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_route_id;`); - await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_id;`); - - await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_container_units;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.clearance_milestones;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_document_review;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_clearance_cycles;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_review_notes;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_rate_snapshots;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_approval_steps;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_signatures;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_cargo_scope;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_routes;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contracts;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1822000000000-CreateImportDjiboutiOperations.ts b/apps/edr-freight-api/src/migrations/1822000000000-CreateImportDjiboutiOperations.ts deleted file mode 100644 index 981918f25..000000000 --- a/apps/edr-freight-api/src/migrations/1822000000000-CreateImportDjiboutiOperations.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm'; - -export class CreateImportDjiboutiOperations1822000000000 implements MigrationInterface { - name = 'CreateImportDjiboutiOperations1822000000000'; - - async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'import_djibouti_operations', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'train_schedule_id', type: 'uuid', isUnique: true }, - { name: 'documents', type: 'jsonb', default: "'{}'::jsonb" }, - { name: 'gatepass_granted_at', type: 'timestamptz', isNullable: true }, - { name: 'ready_for_loading_at', type: 'timestamptz', isNullable: true }, - { name: 'loaded_on_train_at', type: 'timestamptz', isNullable: true }, - { name: 'departed_from_djibouti_at', type: 'timestamptz', isNullable: true }, - { name: 'load_list_generated_at', type: 'timestamptz', isNullable: true }, - { name: 'performed_by', type: 'varchar', length: '120', isNullable: true }, - { name: 'notes', type: 'text', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.import_djibouti_operations', - new TableIndex({ - name: 'idx_import_djibouti_operations_schedule', - columnNames: ['train_schedule_id'], - }), - ); - - await queryRunner.createForeignKey( - 'freight.import_djibouti_operations', - new TableForeignKey({ - columnNames: ['train_schedule_id'], - referencedTableName: 'train_schedules', - referencedSchema: 'freight', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }), - ); - } - - async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.import_djibouti_operations', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1823000000000-BackfillContractsFromBookings.ts b/apps/edr-freight-api/src/migrations/1823000000000-BackfillContractsFromBookings.ts deleted file mode 100644 index 552e4affd..000000000 --- a/apps/edr-freight-api/src/migrations/1823000000000-BackfillContractsFromBookings.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Data backfill for the contract–booking separation (docs/new-doc.md §17). - * - * For every legacy `booking_type = 'GENERAL_CONTRACT'` booking we synthesise a - * `freight.contracts` row from its contract-phase columns, copy its routes - * (contract_route_lines → contract_routes, dropping quantity), and point the - * contract + every child shipment booking (linked via booking_orders) at it. - * - * Per §19 item 1, historical ONE_TIME bookings that went through the full - * contract flow get a contract parent inserted and `contract_id` set on the same - * booking row (no row split). - * - * Idempotent: skips bookings that already have `contract_id` set, and matches a - * synthesised contract by a deterministic `CTR-` reference. - */ -export class BackfillContractsFromBookings1823000000000 - implements MigrationInterface -{ - name = 'BackfillContractsFromBookings1823000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // 1. One contract per GENERAL_CONTRACT booking, carrying the contract-phase - // columns. Reference is derived from the source booking id so re-runs are - // idempotent (ON CONFLICT DO NOTHING on the unique reference). - await queryRunner.query(` - INSERT INTO freight.contracts ( - reference, company_id, company_profile_id, is_government, government_institution, - contract_kind, trade_direction, freight_type, service_type_id, payment_currency, - customs_clearing_enabled, customs_clearing_agent, equipment_return, - first_mile_pickup_address, first_mile_pickup_lat, first_mile_pickup_lng, - last_mile_delivery_address, last_mile_delivery_lat, last_mile_delivery_lng, - is_hazardous, is_reefer, estimated_shipment_date, - contract_validity_days, contract_valid_from, contract_valid_until, expires_at, - status, clearance_status, clearance_cycle_number, - pricing_breakdown, contract_type, contract_template_key, contract_generated_at, - contract_summary, version_number, - approved_by_staff_id, approved_by_staff_at, - signed_by_director_id, signed_by_director_at, - signed_by_ceo_id, signed_by_ceo_at, customer_signed_at, fully_executed_at, - created_at, updated_at - ) - SELECT - 'CTR-' || b.id::text, b.company_id, b.company_profile_id, b.is_government, b.government_institution, - 'GENERAL', b.trade_direction, b.freight_type, b.service_type_id, b.payment_currency, - b.customs_clearing_enabled, b.customs_clearing_agent, b.equipment_return, - b.first_mile_pickup_address, b.first_mile_pickup_lat, b.first_mile_pickup_lng, - b.last_mile_delivery_address, b.last_mile_delivery_lat, b.last_mile_delivery_lng, - b.is_hazardous, b.is_reefer, b.estimated_shipment_date, - b.contract_validity_days, b.contract_valid_from, b.contract_valid_until, b.expires_at, - CASE - WHEN b.status IN ('CONTRACT_ACTIVE') THEN 'CONTRACT_ACTIVE' - WHEN b.status IN ('CONTRACT_CLOSED') THEN 'CONTRACT_CLOSED' - WHEN b.status IN ('EXPIRED') THEN 'EXPIRED' - WHEN b.status IN ('CANCELLED') THEN 'CANCELLED' - WHEN b.status IN ('REJECTED') THEN 'REJECTED' - ELSE 'CONTRACT_ACTIVE' - END, - CASE WHEN b.customs_clearing_enabled THEN 'NOT_APPLICABLE' ELSE 'NOT_APPLICABLE' END, - 0, - b.pricing_breakdown, - b.contract_type, b.contract_template_key, b.contract_generated_at, - b.contract_summary, COALESCE(b.version_number, 1), - b.approved_by_staff_id, b.approved_by_staff_at, - b.signed_by_director_id, b.signed_by_director_at, - b.signed_by_ceo_id, b.signed_by_ceo_at, b.customer_signed_at, b.fully_executed_at, - b.created_at, b.updated_at - FROM freight.bookings b - WHERE b.booking_type = 'GENERAL_CONTRACT' - ON CONFLICT (reference) DO NOTHING; - `); - - // 2. Copy each general contract's route lines into contract_routes (no qty). - await queryRunner.query(` - INSERT INTO freight.contract_routes (contract_id, origin_yard_id, destination_yard_id, km, sort_order, created_at, updated_at) - SELECT c.id, crl.origin_yard_id, crl.destination_yard_id, crl.km, 0, now(), now() - FROM freight.contract_route_lines crl - JOIN freight.contracts c ON c.reference = 'CTR-' || crl.contract_booking_id::text - ON CONFLICT (contract_id, origin_yard_id, destination_yard_id) DO NOTHING; - `); - - // 3. Point the general-contract booking itself at its new contract, and stamp - // the denormalized contract_kind for the active-booking index. - await queryRunner.query(` - UPDATE freight.bookings b - SET contract_id = c.id, contract_kind = 'GENERAL', created_by_role = 'CUSTOMER' - FROM freight.contracts c - WHERE c.reference = 'CTR-' || b.id::text - AND b.booking_type = 'GENERAL_CONTRACT' - AND b.contract_id IS NULL; - `); - - // 4. Point each child shipment booking (spawned via booking_orders) at the - // same contract as its parent general contract. - await queryRunner.query(` - UPDATE freight.bookings child - SET contract_id = c.id, contract_kind = 'GENERAL', created_by_role = 'CUSTOMER' - FROM freight.booking_orders bo - JOIN freight.contracts c ON c.reference = 'CTR-' || bo.contract_booking_id::text - WHERE child.id = bo.booking_id - AND child.contract_id IS NULL; - `); - - // 5. Historical ONE_TIME bookings that completed the contract flow: synthesise - // a contract parent and point the same booking row at it (no row split). - await queryRunner.query(` - INSERT INTO freight.contracts ( - reference, company_id, company_profile_id, is_government, government_institution, - contract_kind, trade_direction, freight_type, service_type_id, payment_currency, - customs_clearing_enabled, customs_clearing_agent, equipment_return, - first_mile_pickup_address, first_mile_pickup_lat, first_mile_pickup_lng, - last_mile_delivery_address, last_mile_delivery_lat, last_mile_delivery_lng, - is_hazardous, is_reefer, estimated_shipment_date, - contract_validity_days, contract_valid_from, contract_valid_until, - status, clearance_status, clearance_cycle_number, - pricing_breakdown, contract_type, contract_template_key, contract_generated_at, - contract_summary, version_number, - approved_by_staff_id, approved_by_staff_at, - signed_by_director_id, signed_by_director_at, - signed_by_ceo_id, signed_by_ceo_at, customer_signed_at, fully_executed_at, - created_at, updated_at - ) - SELECT - 'CTR-' || b.id::text, b.company_id, b.company_profile_id, b.is_government, b.government_institution, - 'ONE_TIME', b.trade_direction, b.freight_type, b.service_type_id, b.payment_currency, - b.customs_clearing_enabled, b.customs_clearing_agent, b.equipment_return, - b.first_mile_pickup_address, b.first_mile_pickup_lat, b.first_mile_pickup_lng, - b.last_mile_delivery_address, b.last_mile_delivery_lat, b.last_mile_delivery_lng, - b.is_hazardous, b.is_reefer, b.estimated_shipment_date, - b.contract_validity_days, b.contract_valid_from, b.contract_valid_until, - 'FULLY_EXECUTED', 'NOT_APPLICABLE', 0, - b.pricing_breakdown, b.contract_type, b.contract_template_key, b.contract_generated_at, - b.contract_summary, COALESCE(b.version_number, 1), - b.approved_by_staff_id, b.approved_by_staff_at, - b.signed_by_director_id, b.signed_by_director_at, - b.signed_by_ceo_id, b.signed_by_ceo_at, b.customer_signed_at, b.fully_executed_at, - b.created_at, b.updated_at - FROM freight.bookings b - WHERE COALESCE(b.booking_type, 'ONE_TIME') = 'ONE_TIME' - AND b.contract_id IS NULL - AND b.contract_generated_at IS NOT NULL - ON CONFLICT (reference) DO NOTHING; - `); - - await queryRunner.query(` - UPDATE freight.bookings b - SET contract_id = c.id, contract_kind = 'ONE_TIME', created_by_role = 'CUSTOMER' - FROM freight.contracts c - WHERE c.reference = 'CTR-' || b.id::text - AND COALESCE(b.booking_type, 'ONE_TIME') = 'ONE_TIME' - AND b.contract_id IS NULL; - `); - - // 6. Build a single route per ONE_TIME contract from the booking's own - // origin/destination (general contracts already got their routes in step 2). - await queryRunner.query(` - INSERT INTO freight.contract_routes (contract_id, origin_yard_id, destination_yard_id, sort_order, created_at, updated_at) - SELECT c.id, b.origin_yard_id, b.destination_yard_id, 0, now(), now() - FROM freight.bookings b - JOIN freight.contracts c ON c.id = b.contract_id AND c.contract_kind = 'ONE_TIME' - WHERE b.origin_yard_id IS NOT NULL AND b.destination_yard_id IS NOT NULL - ON CONFLICT (contract_id, origin_yard_id, destination_yard_id) DO NOTHING; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - // Unlink bookings and drop the synthesised contracts (and their cascaded routes). - await queryRunner.query(` - UPDATE freight.bookings SET contract_id = NULL, contract_route_id = NULL - WHERE contract_id IN (SELECT id FROM freight.contracts WHERE reference LIKE 'CTR-%'); - `); - await queryRunner.query(`DELETE FROM freight.contracts WHERE reference LIKE 'CTR-%';`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1823000000000-CreateImportOperationsTables.ts b/apps/edr-freight-api/src/migrations/1823000000000-CreateImportOperationsTables.ts deleted file mode 100644 index 1a198e983..000000000 --- a/apps/edr-freight-api/src/migrations/1823000000000-CreateImportOperationsTables.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -export class CreateImportOperationsTables1823000000000 implements MigrationInterface { - name = 'CreateImportOperationsTables1823000000000'; - - async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'djibouti_import_incidents', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'booking_id', type: 'uuid' }, - { name: 'container_number', type: 'varchar', length: '80', isNullable: true }, - { name: 'cargo_id', type: 'uuid', isNullable: true }, - { name: 'facility', type: 'varchar', length: '120', isNullable: true }, - { name: 'station', type: 'varchar', length: '120', isNullable: true }, - { name: 'incident_type', type: 'varchar', length: '40' }, - { name: 'description', type: 'text' }, - { name: 'photos', type: 'jsonb', default: "'[]'::jsonb" }, - { name: 'reported_by', type: 'varchar', length: '120', isNullable: true }, - { name: 'reported_at', type: 'timestamptz' }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createIndex('freight.djibouti_import_incidents', new TableIndex({ name: 'idx_djibouti_incidents_booking', columnNames: ['booking_id'] })); - await queryRunner.createIndex('freight.djibouti_import_incidents', new TableIndex({ name: 'idx_djibouti_incidents_container', columnNames: ['container_number'] })); - await queryRunner.createIndex('freight.djibouti_import_incidents', new TableIndex({ name: 'idx_djibouti_incidents_type', columnNames: ['incident_type'] })); - - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'import_customs_finalizations', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'booking_id', type: 'uuid', isUnique: true }, - { name: 'documents', type: 'jsonb', default: "'{}'::jsonb" }, - { name: 'declaration_serial_number', type: 'varchar', length: '120', isNullable: true }, - { name: 'duties_taxes_notified_at', type: 'timestamptz', isNullable: true }, - { name: 'duties_taxes_paid_at', type: 'timestamptz', isNullable: true }, - { name: 'customs_risk', type: 'varchar', length: '12', isNullable: true }, - { name: 'import_release_permitted_at', type: 'timestamptz', isNullable: true }, - { name: 'completed_at', type: 'timestamptz', isNullable: true }, - { name: 'performed_by', type: 'varchar', length: '120', isNullable: true }, - { name: 'notes', type: 'text', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createIndex('freight.import_customs_finalizations', new TableIndex({ name: 'idx_import_customs_booking', columnNames: ['booking_id'] })); - await queryRunner.createIndex('freight.import_customs_finalizations', new TableIndex({ name: 'idx_import_customs_risk', columnNames: ['customs_risk'] })); - - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'empty_container_returns', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, - { name: 'container_number', type: 'varchar', length: '80' }, - { name: 'booking_id', type: 'uuid', isNullable: true }, - { name: 'customer_id', type: 'uuid', isNullable: true }, - { name: 'return_date', type: 'timestamptz' }, - { name: 'facility', type: 'varchar', length: '120', isNullable: true }, - { name: 'yard', type: 'varchar', length: '120', isNullable: true }, - { name: 'zone', type: 'varchar', length: '120', isNullable: true }, - { name: 'condition', type: 'text', isNullable: true }, - { name: 'handover_note', type: 'text', isNullable: true }, - { name: 'status', type: 'varchar', length: '40', default: "'RETURNED'" }, - { name: 'wagon_allocation_reference', type: 'varchar', length: '120', isNullable: true }, - { name: 'performed_by', type: 'varchar', length: '120', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - await queryRunner.createIndex('freight.empty_container_returns', new TableIndex({ name: 'idx_empty_returns_container', columnNames: ['container_number'] })); - await queryRunner.createIndex('freight.empty_container_returns', new TableIndex({ name: 'idx_empty_returns_booking', columnNames: ['booking_id'] })); - await queryRunner.createIndex('freight.empty_container_returns', new TableIndex({ name: 'idx_empty_returns_status', columnNames: ['status'] })); - } - - async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.empty_container_returns', true); - await queryRunner.dropTable('freight.import_customs_finalizations', true); - await queryRunner.dropTable('freight.djibouti_import_incidents', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1824000000000-DropLegacyContractTables.ts b/apps/edr-freight-api/src/migrations/1824000000000-DropLegacyContractTables.ts deleted file mode 100644 index 06bcff14a..000000000 --- a/apps/edr-freight-api/src/migrations/1824000000000-DropLegacyContractTables.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Cutover cleanup (docs/new-doc.md §17 Phase 4). Runs AFTER the backfill - * (1823…) so every legacy general contract + drawdown already lives in the - * `contracts` aggregate. - * - * Drops the now-unused booking-as-contract artifacts: - * - `bookings.booking_type` (every booking is a real shipment now) - * - `bookings.previous_contract_id` (renewal lives on `contracts.renewal_of_id`) - * - the `booking_orders` / `booking_order_lines` drawdown ledger - * - `contract_route_lines` (superseded by `contract_routes`) - * - * The shipment/payment/scheduling/allocation columns on `bookings` are kept — - * the operational pipeline is unchanged. - */ -export class DropLegacyContractTables1824000000000 implements MigrationInterface { - name = 'DropLegacyContractTables1824000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // booking_order_lines references booking_orders → drop child first. - await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_order_lines CASCADE;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_orders CASCADE;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_route_lines CASCADE;`); - - await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS booking_type;`); - await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS previous_contract_id;`); - } - - public async down(queryRunner: QueryRunner): Promise { - // Re-add the dropped columns (data is not restored — this is a one-way cutover). - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS booking_type VARCHAR(20) DEFAULT 'ONE_TIME';`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS previous_contract_id UUID;`, - ); - // The legacy ledger/route tables are intentionally NOT recreated here; restore - // from a backup if a rollback past the cutover is ever required. - } -} diff --git a/apps/edr-freight-api/src/migrations/1825000000000-AddGlOperations.ts b/apps/edr-freight-api/src/migrations/1825000000000-AddGlOperations.ts deleted file mode 100644 index cf0cc51f4..000000000 --- a/apps/edr-freight-api/src/migrations/1825000000000-AddGlOperations.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Global Logistics Phase-2 operational features (docs/new-doc.md §11–§13, gap - * matrix #14/#16/#17/#18): - * - `clearance_milestones.metadata` — structured payload for RISK_ASSIGNED - * (risk level) and DUTY_TAXES_ADVISED (amount, currency, declaration serial) - * - `bookings.gl_station_yard_id` / `gl_assigned_staff_id` / `gl_assigned_at` - * — station routing + staff binding (GL US-02) - * - `freight.clearance_incidents` — cargo exception/damage reports with photos - * (GL Import US-07) - */ -export class AddGlOperations1825000000000 implements MigrationInterface { - name = 'AddGlOperations1825000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.clearance_milestones ADD COLUMN IF NOT EXISTS metadata JSONB;`, - ); - - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS gl_station_yard_id UUID;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS gl_assigned_staff_id UUID;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS gl_assigned_at TIMESTAMPTZ;`, - ); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.clearance_incidents ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - booking_id UUID NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE, - incident_type VARCHAR(32) NOT NULL, - description TEXT NOT NULL, - photo_file_ids JSONB NOT NULL DEFAULT '[]', - reported_by_user_id UUID, - reported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - deleted_at TIMESTAMPTZ - ); - `); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_clearance_incidents_booking ON freight.clearance_incidents(booking_id);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.clearance_incidents CASCADE;`); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS gl_assigned_at;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS gl_assigned_staff_id;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS gl_station_yard_id;`, - ); - await queryRunner.query( - `ALTER TABLE freight.clearance_milestones DROP COLUMN IF EXISTS metadata;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1825000000000-CreateBookingContainerAllocations.ts b/apps/edr-freight-api/src/migrations/1825000000000-CreateBookingContainerAllocations.ts deleted file mode 100644 index 70b0832f5..000000000 --- a/apps/edr-freight-api/src/migrations/1825000000000-CreateBookingContainerAllocations.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; - -/** - * Create the freight.booking_container_allocations table — container-to-vehicle - * allocation mapping for flexible routing of containers across available vehicles. - */ -export class CreateBookingContainerAllocations1825000000000 implements MigrationInterface { - name = 'CreateBookingContainerAllocations1825000000000'; - - public async up(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.booking_container_allocations'); - if (exists) return; - - await queryRunner.createTable( - new Table({ - name: 'freight.booking_container_allocations', - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'gen_random_uuid()', - }, - { name: 'booking_id', type: 'uuid', isNullable: false }, - { name: 'container_id', type: 'uuid', isNullable: false }, - { name: 'vehicle_id', type: 'uuid', isNullable: true }, - { - name: 'container_type', - type: 'text', - isNullable: false, - }, - { - name: 'quantity', - type: 'integer', - default: 1, - isNullable: false, - }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createForeignKey( - 'freight.booking_container_allocations', - new TableForeignKey({ - columnNames: ['booking_id'], - referencedTableName: 'freight.bookings', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }), - ); - - await queryRunner.createForeignKey( - 'freight.booking_container_allocations', - new TableForeignKey({ - columnNames: ['vehicle_id'], - referencedTableName: 'freight.vehicles', - referencedColumnNames: ['id'], - onDelete: 'SET NULL', - }), - ); - - await queryRunner.query( - `CREATE INDEX "IDX_booking_container_allocations_booking_id" ON "freight"."booking_container_allocations" ("booking_id")`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_booking_container_allocations_vehicle_id" ON "freight"."booking_container_allocations" ("vehicle_id")`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.booking_container_allocations'); - if (exists) { - await queryRunner.dropTable('freight.booking_container_allocations'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1826000000000-AddCargoScopeQuantityCap.ts b/apps/edr-freight-api/src/migrations/1826000000000-AddCargoScopeQuantityCap.ts deleted file mode 100644 index 97428c11b..000000000 --- a/apps/edr-freight-api/src/migrations/1826000000000-AddCargoScopeQuantityCap.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * GENERAL contracts can be booked repeatedly until a total cargo quantity cap is - * reached (e.g. 100 containers across many shipments). `quantity_cap` on each - * cargo-scope line holds that ceiling (containers per size, or tons/items for - * bulk). NULL = uncapped; always NULL for ONE_TIME (single booking). - */ -export class AddCargoScopeQuantityCap1826000000000 implements MigrationInterface { - name = 'AddCargoScopeQuantityCap1826000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_cargo_scope ADD COLUMN IF NOT EXISTS quantity_cap NUMERIC(12,2);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_cargo_scope DROP COLUMN IF EXISTS quantity_cap;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1827000000000-CreateBookingRequests.ts b/apps/edr-freight-api/src/migrations/1827000000000-CreateBookingRequests.ts deleted file mode 100644 index e88824e28..000000000 --- a/apps/edr-freight-api/src/migrations/1827000000000-CreateBookingRequests.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -/** - * Customer shipment requests for GENERAL customs (Path B) contracts. The customer - * submits date + quantities; Global Logistics reviews, then creates the booking - * on their behalf and per-booking clearance begins. Additive — no change to - * existing tables; ONE_TIME contracts are unaffected. - */ -export class CreateBookingRequests1827000000000 implements MigrationInterface { - name = 'CreateBookingRequests1827000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'booking_requests', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'reference', type: 'varchar', length: '40', default: "''" }, - { name: 'contract_id', type: 'uuid' }, - { name: 'requested_by_user_id', type: 'uuid', isNullable: true }, - { name: 'contract_route_id', type: 'uuid', isNullable: true }, - { name: 'scheduled_date', type: 'timestamptz', isNullable: true }, - { name: 'status', type: 'varchar', length: '16', default: "'PENDING'" }, - { name: 'requested_lines', type: 'jsonb', default: "'{}'::jsonb" }, - { name: 'notes', type: 'text', isNullable: true }, - { name: 'created_booking_id', type: 'uuid', isNullable: true }, - { name: 'reviewed_by_staff_id', type: 'uuid', isNullable: true }, - { name: 'reviewed_at', type: 'timestamptz', isNullable: true }, - { name: 'review_note', type: 'text', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['contract_id'], - referencedSchema: 'freight', - referencedTableName: 'contracts', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }, - { - columnNames: ['created_booking_id'], - referencedSchema: 'freight', - referencedTableName: 'bookings', - referencedColumnNames: ['id'], - onDelete: 'SET NULL', - }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.booking_requests', - new TableIndex({ name: 'idx_booking_requests_contract', columnNames: ['contract_id'] }), - ); - await queryRunner.createIndex( - 'freight.booking_requests', - new TableIndex({ name: 'idx_booking_requests_status', columnNames: ['status'] }), - ); - await queryRunner.createIndex( - 'freight.booking_requests', - new TableIndex({ - name: 'idx_booking_requests_contract_status', - columnNames: ['contract_id', 'status'], - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.booking_requests', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/1828000000000-AddBulkHazmatReeferQuantity.ts b/apps/edr-freight-api/src/migrations/1828000000000-AddBulkHazmatReeferQuantity.ts deleted file mode 100644 index cc9437eaf..000000000 --- a/apps/edr-freight-api/src/migrations/1828000000000-AddBulkHazmatReeferQuantity.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Bulk / break-bulk freight can now declare HOW MUCH of the cargo is hazardous - * or refrigerated, in the cargo's own unit of measure (tons for PER_TON, item - * count for PER_ITEM). These two columns hold that amount on the booking; they - * stay 0 for container freight (which tracks it per line on booking_container) - * and for bulk cargo with no hazardous/reefer portion. The existing - * is_hazardous / is_reefer booleans remain the surcharge trigger. - */ -export class AddBulkHazmatReeferQuantity1828000000000 implements MigrationInterface { - name = 'AddBulkHazmatReeferQuantity1828000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS bulk_hazardous_quantity NUMERIC(12,3) NOT NULL DEFAULT 0;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS bulk_reefer_quantity NUMERIC(12,3) NOT NULL DEFAULT 0;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS bulk_reefer_quantity;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS bulk_hazardous_quantity;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1828000000000-AddGrnNumberToWarehouseInventory.ts b/apps/edr-freight-api/src/migrations/1828000000000-AddGrnNumberToWarehouseInventory.ts deleted file mode 100644 index c57a43aaa..000000000 --- a/apps/edr-freight-api/src/migrations/1828000000000-AddGrnNumberToWarehouseInventory.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddGrnNumberToWarehouseInventory1828000000000 implements MigrationInterface { - name = 'AddGrnNumberToWarehouseInventory1828000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.warehouse_inventory - ADD COLUMN IF NOT EXISTS grn_number VARCHAR(100) NULL - `); - - await queryRunner.query(` - UPDATE freight.warehouse_inventory - SET grn_number = substring(notes FROM 'GRN Number: ([^\\n\\r]+)') - WHERE grn_number IS NULL - AND notes IS NOT NULL - AND notes ~ 'GRN Number: ' - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_inventory_grn_number - ON freight.warehouse_inventory(grn_number) - WHERE grn_number IS NOT NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_warehouse_inventory_grn_number`); - await queryRunner.query(` - ALTER TABLE freight.warehouse_inventory - DROP COLUMN IF EXISTS grn_number - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1828000000000-ExtendInvoicesForPartialPayment.ts b/apps/edr-freight-api/src/migrations/1828000000000-ExtendInvoicesForPartialPayment.ts deleted file mode 100644 index 55239c13f..000000000 --- a/apps/edr-freight-api/src/migrations/1828000000000-ExtendInvoicesForPartialPayment.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Extend `freight.invoices` into the billing record of record for every source - * (booking, demurrage, warehouse fees, …) so warehouse fee invoices can be - * centralized onto it instead of the parallel `warehouse_fee_invoices` table. - * - * Adds money tracking that supports partial payment (`subtotal/tax/paid/balance`), - * a `paid_at` stamp, a `payments` jsonb ledger, and the `ISSUED` / `PARTIALLY_PAID` - * statuses the warehouse flow uses. - * - * Matches billing/entities/invoice.entity.ts. All columns are additive with - * defaults, so existing booking/demurrage rows are unaffected. - */ -export class ExtendInvoicesForPartialPayment1828000000000 - implements MigrationInterface -{ - name = "ExtendInvoicesForPartialPayment1828000000000"; - - public async up(queryRunner: QueryRunner): Promise { - // New statuses. ADD VALUE is non-transactional-value-safe on PG 12+ as long - // as the value is not referenced in the same transaction (it is not here). - await queryRunner.query( - `ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'ISSUED' BEFORE 'PENDING';`, - ); - await queryRunner.query( - `ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'PARTIALLY_PAID' BEFORE 'PAID';`, - ); - - await queryRunner.query(` - ALTER TABLE freight.invoices - ADD COLUMN IF NOT EXISTS subtotal_amount numeric(14, 2) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS tax_amount numeric(14, 2) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS paid_amount numeric(14, 2) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS balance_amount numeric(14, 2) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS paid_at timestamptz, - ADD COLUMN IF NOT EXISTS payments jsonb NOT NULL DEFAULT '[]'; - `); - - // Backfill existing rows: subtotal mirrors the total (no tax was modeled), - // the outstanding balance is the full total for unpaid invoices. - await queryRunner.query(` - UPDATE freight.invoices - SET subtotal_amount = total_amount, - balance_amount = total_amount; - `); - - // Already-settled invoices: fully paid, zero balance, stamped from updated_at. - await queryRunner.query(` - UPDATE freight.invoices - SET paid_amount = total_amount, - balance_amount = 0, - paid_at = updated_at - WHERE status = 'PAID'; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.invoices - DROP COLUMN IF EXISTS payments, - DROP COLUMN IF EXISTS paid_at, - DROP COLUMN IF EXISTS balance_amount, - DROP COLUMN IF EXISTS paid_amount, - DROP COLUMN IF EXISTS tax_amount, - DROP COLUMN IF EXISTS subtotal_amount; - `); - // Postgres cannot drop individual enum values; ISSUED / PARTIALLY_PAID are - // left on freight.invoices_status_enum (harmless, unused after down). - } -} diff --git a/apps/edr-freight-api/src/migrations/1829000000000-CentralizeWarehouseInvoices.ts b/apps/edr-freight-api/src/migrations/1829000000000-CentralizeWarehouseInvoices.ts deleted file mode 100644 index 75082c5c4..000000000 --- a/apps/edr-freight-api/src/migrations/1829000000000-CentralizeWarehouseInvoices.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Fold warehouse fee invoices into the central billing system. - * - * Warehouse fee invoices are no longer a standalone aggregate: each becomes a - * global `freight.invoices` row (`source = 'warehouse'`, `source_id = - * inventory_id`) with its items as `freight.invoice_lines`. The warehouse - * service is now a thin layer over `BillingService`. This migration backfills the - * existing rows (preserving ids, numbers, status, amounts and payment history), - * then drops the two legacy tables. - * - * Rows that cannot be billed centrally — no company to bill (`company_id` / - * `company_profile_id` underivable from the customer or the booking) — are not - * migrated; they could never have been charged through the gateway and are - * dropped with the table. - */ -export class CentralizeWarehouseInvoices1829000000000 implements MigrationInterface { - name = 'CentralizeWarehouseInvoices1829000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'freight' - AND table_name = 'invoices' - AND column_name = 'booking_id' - ) THEN - ALTER TABLE freight.invoices ALTER COLUMN booking_id DROP NOT NULL; - END IF; - - IF EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'freight' - AND table_name = 'invoices' - AND column_name = 'amount' - ) THEN - ALTER TABLE freight.invoices ALTER COLUMN amount DROP NOT NULL; - END IF; - END $$; - `); - - // 1. Invoice headers. Keep the same id so items still link, and so any - // external reference to the invoice id stays valid. - await queryRunner.query(` - INSERT INTO freight.invoices ( - id, invoice_number, company_id, company_profile_id, - subtotal_amount, tax_amount, total_amount, paid_amount, balance_amount, - currency, status, source, source_id, type, - issued_at, paid_at, payments, payment_id, due_at, - created_at, updated_at, deleted_at - ) - SELECT - fee.id, - fee.invoice_number, - COALESCE(fee.customer_id, b.company_id), - COALESCE( - b.company_profile_id, - (SELECT cp.id - FROM freight.company_profiles cp - WHERE cp.company_id = COALESCE(fee.customer_id, b.company_id) - AND cp.deleted_at IS NULL - ORDER BY cp.created_at ASC - LIMIT 1) - ), - fee.subtotal_amount, fee.tax_amount, fee.total_amount, fee.paid_amount, fee.balance_amount, - fee.currency, - fee.status::freight.invoices_status_enum, - 'warehouse', - fee.inventory_id, - fee.invoice_type, - fee.issued_at, - fee.paid_at, - COALESCE(fee.payments, '[]'::jsonb), - NULL, - COALESCE(fee.due_date, fee.issued_at, fee.created_at), - fee.created_at, fee.updated_at, fee.deleted_at - FROM freight.warehouse_fee_invoices fee - LEFT JOIN freight.bookings b ON b.id = fee.booking_id - WHERE COALESCE(fee.customer_id, b.company_id) IS NOT NULL - AND COALESCE( - b.company_profile_id, - (SELECT cp.id - FROM freight.company_profiles cp - WHERE cp.company_id = COALESCE(fee.customer_id, b.company_id) - AND cp.deleted_at IS NULL - ORDER BY cp.created_at ASC - LIMIT 1) - ) IS NOT NULL - ON CONFLICT (id) DO NOTHING; - `); - - // 2. Invoice lines — only for items whose parent invoice migrated. Warehouse - // fee fields (fee_rule_id / chargeable_days / free_days) move into the - // line's jsonb metadata. - await queryRunner.query(` - INSERT INTO freight.invoice_lines ( - id, invoice_id, charge_type, description, quantity, unit_rate, amount, - currency, metadata, created_at, updated_at, deleted_at - ) - SELECT - item.id, - item.invoice_id, - item.fee_type, - item.description, - item.quantity, - item.unit_rate, - item.amount, - item.currency, - jsonb_build_object( - 'feeRuleId', item.fee_rule_id, - 'chargeableDays', item.chargeable_days, - 'freeDays', item.free_days - ), - item.created_at, item.updated_at, item.deleted_at - FROM freight.warehouse_fee_invoice_items item - JOIN freight.invoices i ON i.id = item.invoice_id AND i.source = 'warehouse' - ON CONFLICT (id) DO NOTHING; - `); - - // 3. Drop the legacy tables (items first — FK to invoices). - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_fee_invoice_items;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_fee_invoices;`); - } - - public async down(queryRunner: QueryRunner): Promise { - // Recreate the legacy tables … - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_fee_invoices ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - invoice_number varchar(40) NOT NULL, - booking_id uuid, - customer_id uuid, - inventory_id uuid NOT NULL, - facility_id uuid, - warehouse_id uuid, - yard_id uuid, - zone_id uuid, - invoice_type varchar(32) NOT NULL DEFAULT 'MIXED_WAREHOUSE_FEES', - status varchar(20) NOT NULL DEFAULT 'DRAFT', - subtotal_amount numeric(14,2) NOT NULL DEFAULT 0, - tax_amount numeric(14,2) NOT NULL DEFAULT 0, - total_amount numeric(14,2) NOT NULL DEFAULT 0, - paid_amount numeric(14,2) NOT NULL DEFAULT 0, - balance_amount numeric(14,2) NOT NULL DEFAULT 0, - currency varchar(8) NOT NULL DEFAULT 'USD', - period_start timestamptz, - period_end timestamptz, - issued_at timestamptz, - due_date timestamptz, - paid_at timestamptz, - cancelled_at timestamptz, - payments jsonb NOT NULL DEFAULT '[]', - notes text, - CONSTRAINT "PK_warehouse_fee_invoices" PRIMARY KEY (id), - CONSTRAINT "UQ_warehouse_fee_invoices_invoice_number" UNIQUE (invoice_number) - ); - `); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_warehouse_fee_invoices_booking_id" ON freight.warehouse_fee_invoices (booking_id);`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_warehouse_fee_invoices_inventory_id" ON freight.warehouse_fee_invoices (inventory_id);`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_warehouse_fee_invoices_status" ON freight.warehouse_fee_invoices (status);`, - ); - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_fee_invoice_items ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - invoice_id uuid NOT NULL, - fee_rule_id uuid, - fee_type varchar(32) NOT NULL, - description varchar(255) NOT NULL, - quantity numeric(12,2) NOT NULL DEFAULT 1, - unit_rate numeric(14,2) NOT NULL DEFAULT 0, - amount numeric(14,2) NOT NULL DEFAULT 0, - currency varchar(8) NOT NULL DEFAULT 'USD', - chargeable_days int, - free_days int, - CONSTRAINT "PK_warehouse_fee_invoice_items" PRIMARY KEY (id), - CONSTRAINT "FK_warehouse_fee_invoice_items_invoice" - FOREIGN KEY (invoice_id) REFERENCES freight.warehouse_fee_invoices (id) ON DELETE CASCADE - ); - `); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_warehouse_fee_invoice_items_invoice_id" ON freight.warehouse_fee_invoice_items (invoice_id);`, - ); - - // … then copy the warehouse-source invoices back, deriving the typed FKs and - // period from the linked inventory item. - await queryRunner.query(` - INSERT INTO freight.warehouse_fee_invoices ( - id, created_at, updated_at, deleted_at, invoice_number, - booking_id, customer_id, inventory_id, facility_id, warehouse_id, yard_id, zone_id, - invoice_type, status, subtotal_amount, tax_amount, total_amount, paid_amount, balance_amount, - currency, period_start, period_end, issued_at, due_date, paid_at, cancelled_at, payments, notes - ) - SELECT - i.id, i.created_at, i.updated_at, i.deleted_at, i.invoice_number, - inv.booking_id, i.company_id, i.source_id, w.facility_id, inv.warehouse_id, inv.yard_id, inv.zone_id, - i.type, i.status::text, i.subtotal_amount, i.tax_amount, i.total_amount, i.paid_amount, i.balance_amount, - i.currency, inv.arrived_at, i.issued_at, i.issued_at, i.due_at, i.paid_at, - CASE WHEN i.status::text = 'CANCELLED' THEN i.updated_at ELSE NULL END, - i.payments, NULL - FROM freight.invoices i - LEFT JOIN freight.warehouse_inventory inv ON inv.id = i.source_id - LEFT JOIN freight.warehouses w ON w.id = inv.warehouse_id - WHERE i.source = 'warehouse' - ON CONFLICT (id) DO NOTHING; - `); - await queryRunner.query(` - INSERT INTO freight.warehouse_fee_invoice_items ( - id, created_at, updated_at, deleted_at, invoice_id, fee_rule_id, fee_type, - description, quantity, unit_rate, amount, currency, chargeable_days, free_days - ) - SELECT - l.id, l.created_at, l.updated_at, l.deleted_at, l.invoice_id, - NULLIF(l.metadata->>'feeRuleId', '')::uuid, - l.charge_type, - COALESCE(l.description, ''), - l.quantity, l.unit_rate, l.amount, l.currency, - NULLIF(l.metadata->>'chargeableDays', '')::int, - NULLIF(l.metadata->>'freeDays', '')::int - FROM freight.invoice_lines l - JOIN freight.invoices i ON i.id = l.invoice_id AND i.source = 'warehouse' - ON CONFLICT (id) DO NOTHING; - `); - - // Remove the migrated rows from the central tables. - await queryRunner.query(` - DELETE FROM freight.invoice_lines - WHERE invoice_id IN (SELECT id FROM freight.invoices WHERE source = 'warehouse'); - `); - await queryRunner.query(`DELETE FROM freight.invoices WHERE source = 'warehouse';`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1829000000000-PhasedClearanceCycleMeta.ts b/apps/edr-freight-api/src/migrations/1829000000000-PhasedClearanceCycleMeta.ts deleted file mode 100644 index 75288e94c..000000000 --- a/apps/edr-freight-api/src/migrations/1829000000000-PhasedClearanceCycleMeta.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class PhasedClearanceCycleMeta1829000000000 implements MigrationInterface { - name = 'PhasedClearanceCycleMeta1829000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles ADD COLUMN IF NOT EXISTS duty_required BOOLEAN;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles ADD COLUMN IF NOT EXISTS vessel_departure_date DATE;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles ADD COLUMN IF NOT EXISTS ro_amendment_requested_at TIMESTAMPTZ;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles ADD COLUMN IF NOT EXISTS ro_hold_reason TEXT;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles ADD COLUMN IF NOT EXISTS current_phase VARCHAR(40);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles DROP COLUMN IF EXISTS duty_required;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles DROP COLUMN IF EXISTS vessel_departure_date;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles DROP COLUMN IF EXISTS ro_amendment_requested_at;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles DROP COLUMN IF EXISTS ro_hold_reason;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles DROP COLUMN IF EXISTS current_phase;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1829000000001-SeedRoVesselMinDays.ts b/apps/edr-freight-api/src/migrations/1829000000001-SeedRoVesselMinDays.ts deleted file mode 100644 index 7165ea7a8..000000000 --- a/apps/edr-freight-api/src/migrations/1829000000001-SeedRoVesselMinDays.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** Admin-configurable minimum days between today and export RO vessel departure. */ -export class SeedRoVesselMinDays1829000000001 implements MigrationInterface { - name = 'SeedRoVesselMinDays1829000000001'; - private readonly code = 'ro_vessel_min_days'; - private readonly options: Array<{ value: string; label: string }> = [ - { value: '2', label: '2 days' }, - { value: '3', label: '3 days' }, - ]; - - public async up(queryRunner: QueryRunner): Promise { - const existing = await queryRunner.query( - `SELECT id FROM freight.dropdown_settings WHERE code = $1 LIMIT 1;`, - [this.code], - ); - if (existing.length > 0) return; - - const inserted = await queryRunner.query( - `INSERT INTO freight.dropdown_settings (code, label, description, multiple) - VALUES ($1, $2, $3, false) - RETURNING id;`, - [ - this.code, - 'RO vessel minimum lead time (days)', - 'Minimum days between today and the vessel departure date on an export Release Order.', - ], - ); - const settingId = inserted[0].id; - - for (let i = 0; i < this.options.length; i++) { - const opt = this.options[i]; - await queryRunner.query( - `INSERT INTO freight.dropdown_options (setting_id, value, label, display_order) - VALUES ($1, $2, $3, $4);`, - [settingId, opt.value, opt.label, i], - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DELETE FROM freight.dropdown_settings WHERE code = $1;`, [ - this.code, - ]); - } -} diff --git a/apps/edr-freight-api/src/migrations/1829000000002-BookingClearanceMeta.ts b/apps/edr-freight-api/src/migrations/1829000000002-BookingClearanceMeta.ts deleted file mode 100644 index c108b7baa..000000000 --- a/apps/edr-freight-api/src/migrations/1829000000002-BookingClearanceMeta.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class BookingClearanceMeta1829000000002 implements MigrationInterface { - name = 'BookingClearanceMeta1829000000002'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS clearance_current_phase VARCHAR(40);`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS duty_required BOOLEAN;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS vessel_departure_date DATE;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS ro_amendment_requested_at TIMESTAMPTZ;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS ro_hold_reason TEXT;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS clearance_current_phase;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS duty_required;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS vessel_departure_date;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS ro_amendment_requested_at;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS ro_hold_reason;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1830000000000-AddExpiredInvoiceStatus.ts b/apps/edr-freight-api/src/migrations/1830000000000-AddExpiredInvoiceStatus.ts deleted file mode 100644 index e4b353b94..000000000 --- a/apps/edr-freight-api/src/migrations/1830000000000-AddExpiredInvoiceStatus.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add the `EXPIRED` invoice status. An invoice expires when its source's pay - * window closes before settlement (e.g. a booking whose `paymentDeadline` - * lapses) — driven event-style from the domain via `BillingService.expirePayable`, - * which emits `${source}.invoice.expired`. Terminal and not settle-able (kept out - * of `OPEN_STATUSES`), so it is distinct from `CANCELLED` (manual void) and - * `OVERDUE` (still payable). - * - * Matches Freight.InvoiceStatus in packages/types. ADD VALUE only — additive and - * not referenced in this same transaction, so it is PG 12+ safe. - */ -export class AddExpiredInvoiceStatus1830000000000 implements MigrationInterface { - name = "AddExpiredInvoiceStatus1830000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'EXPIRED' AFTER 'REFUNDED';`, - ); - } - - public async down(): Promise { - // Postgres cannot drop individual enum values; EXPIRED is left on - // freight.invoices_status_enum (harmless, unused after down). - } -} diff --git a/apps/edr-freight-api/src/migrations/1830000000000-CreateFirstMileContainerAllocations.ts b/apps/edr-freight-api/src/migrations/1830000000000-CreateFirstMileContainerAllocations.ts deleted file mode 100644 index b91e88633..000000000 --- a/apps/edr-freight-api/src/migrations/1830000000000-CreateFirstMileContainerAllocations.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; - -/** - * Create freight.first_mile_container_allocations table — tracks - * container allocations per first-mile shipment with optional vehicle assignment. - */ -export class CreateFirstMileContainerAllocations1830000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.first_mile_container_allocations'); - if (exists) return; - - await queryRunner.createTable( - new Table({ - name: 'freight.first_mile_container_allocations', - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'gen_random_uuid()', - }, - { name: 'first_mile_id', type: 'uuid', isNullable: false }, - { name: 'container_id', type: 'uuid', isNullable: false }, - { name: 'vehicle_id', type: 'uuid', isNullable: true }, - { name: 'container_type', type: 'text', isNullable: false }, - { - name: 'quantity', - type: 'int', - default: 1, - isNullable: false, - }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createForeignKey( - 'freight.first_mile_container_allocations', - new TableForeignKey({ - columnNames: ['first_mile_id'], - referencedTableName: 'freight.first_mile', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }), - ); - - await queryRunner.createForeignKey( - 'freight.first_mile_container_allocations', - new TableForeignKey({ - columnNames: ['vehicle_id'], - referencedTableName: 'freight.vehicles', - referencedColumnNames: ['id'], - onDelete: 'SET NULL', - }), - ); - - await queryRunner.query( - `CREATE INDEX "IDX_first_mile_container_allocations_first_mile_id" ON "freight"."first_mile_container_allocations" ("first_mile_id")`, - ); - await queryRunner.query( - `CREATE INDEX "IDX_first_mile_container_allocations_vehicle_id" ON "freight"."first_mile_container_allocations" ("vehicle_id")`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable('freight.first_mile_container_allocations'); - if (exists) { - await queryRunner.dropTable('freight.first_mile_container_allocations'); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/1830000000000-DropCargoTypeShowFreeTextBox.ts b/apps/edr-freight-api/src/migrations/1830000000000-DropCargoTypeShowFreeTextBox.ts deleted file mode 100644 index 5bdfe1f30..000000000 --- a/apps/edr-freight-api/src/migrations/1830000000000-DropCargoTypeShowFreeTextBox.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class DropCargoTypeShowFreeTextBox1830000000000 implements MigrationInterface { - name = 'DropCargoTypeShowFreeTextBox1830000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.cargo_types - DROP COLUMN IF EXISTS show_free_text_box - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.cargo_types - ADD COLUMN IF NOT EXISTS show_free_text_box boolean NOT NULL DEFAULT false - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1830000000001-RouteStatusAndSegmentKm.ts b/apps/edr-freight-api/src/migrations/1830000000001-RouteStatusAndSegmentKm.ts deleted file mode 100644 index 701b52c18..000000000 --- a/apps/edr-freight-api/src/migrations/1830000000001-RouteStatusAndSegmentKm.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class RouteStatusAndSegmentKm1830000000001 implements MigrationInterface { - name = 'RouteStatusAndSegmentKm1830000000001'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.routes - ADD COLUMN IF NOT EXISTS status varchar(32) NOT NULL DEFAULT 'AVAILABLE' - `); - - await queryRunner.query(` - UPDATE freight.routes - SET status = CASE WHEN is_active = true THEN 'AVAILABLE' ELSE 'STOP_WORKING' END - `); - - await queryRunner.query(` - DROP INDEX IF EXISTS freight."IDX_routes_name" - `); - await queryRunner.query(` - ALTER TABLE freight.routes DROP COLUMN IF EXISTS name - `); - await queryRunner.query(` - ALTER TABLE freight.routes DROP COLUMN IF EXISTS is_active - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_routes_status" ON freight.routes (status) - `); - - await queryRunner.query(` - ALTER TABLE freight.route_milestones - ADD COLUMN IF NOT EXISTS distance_km numeric(10,2) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.route_milestones DROP COLUMN IF EXISTS distance_km - `); - - await queryRunner.query(` - ALTER TABLE freight.routes - ADD COLUMN IF NOT EXISTS name varchar(120) - `); - await queryRunner.query(` - UPDATE freight.routes SET name = id::text WHERE name IS NULL - `); - await queryRunner.query(` - ALTER TABLE freight.routes ALTER COLUMN name SET NOT NULL - `); - - await queryRunner.query(` - ALTER TABLE freight.routes - ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true - `); - await queryRunner.query(` - UPDATE freight.routes - SET is_active = CASE WHEN status = 'AVAILABLE' THEN true ELSE false END - `); - - await queryRunner.query(` - ALTER TABLE freight.routes DROP COLUMN IF EXISTS status - `); - await queryRunner.query(` - DROP INDEX IF EXISTS freight."IDX_routes_status" - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "IDX_routes_name" ON freight.routes (name) - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1830000000002-PreClearanceFinalizedAt.ts b/apps/edr-freight-api/src/migrations/1830000000002-PreClearanceFinalizedAt.ts deleted file mode 100644 index df57cd828..000000000 --- a/apps/edr-freight-api/src/migrations/1830000000002-PreClearanceFinalizedAt.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class PreClearanceFinalizedAt1830000000002 implements MigrationInterface { - name = 'PreClearanceFinalizedAt1830000000002'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles ADD COLUMN IF NOT EXISTS pre_clearance_finalized_at TIMESTAMPTZ;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS pre_clearance_finalized_at TIMESTAMPTZ;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_clearance_cycles DROP COLUMN IF EXISTS pre_clearance_finalized_at;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS pre_clearance_finalized_at;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1831000000000-AddWarehouseFeeRuleTiers.ts b/apps/edr-freight-api/src/migrations/1831000000000-AddWarehouseFeeRuleTiers.ts deleted file mode 100644 index c6da11cc8..000000000 --- a/apps/edr-freight-api/src/migrations/1831000000000-AddWarehouseFeeRuleTiers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddWarehouseFeeRuleTiers1831000000000 implements MigrationInterface { - name = 'AddWarehouseFeeRuleTiers1831000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.warehouse_fee_rules - ADD COLUMN IF NOT EXISTS tiers jsonb NOT NULL DEFAULT '[]'; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.warehouse_fee_rules - DROP COLUMN IF EXISTS tiers; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1832000000000-AddCustomerTruckAssignmentToBookings.ts b/apps/edr-freight-api/src/migrations/1832000000000-AddCustomerTruckAssignmentToBookings.ts deleted file mode 100644 index c137ca264..000000000 --- a/apps/edr-freight-api/src/migrations/1832000000000-AddCustomerTruckAssignmentToBookings.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddCustomerTruckAssignmentToBookings1832000000000 implements MigrationInterface { - name = 'AddCustomerTruckAssignmentToBookings1832000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS customer_truck_plate_number varchar(32), - ADD COLUMN IF NOT EXISTS customer_truck_driver_name varchar(120), - ADD COLUMN IF NOT EXISTS customer_truck_type varchar(60), - ADD COLUMN IF NOT EXISTS customer_truck_container_number varchar(16), - ADD COLUMN IF NOT EXISTS customer_truck_assigned_at timestamptz, - ADD COLUMN IF NOT EXISTS customer_truck_arrived_at timestamptz - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS customer_truck_arrived_at, - DROP COLUMN IF EXISTS customer_truck_assigned_at, - DROP COLUMN IF EXISTS customer_truck_container_number, - DROP COLUMN IF EXISTS customer_truck_type, - DROP COLUMN IF EXISTS customer_truck_driver_name, - DROP COLUMN IF EXISTS customer_truck_plate_number - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1840000000000-CreateFuelTables.ts b/apps/edr-freight-api/src/migrations/1840000000000-CreateFuelTables.ts deleted file mode 100644 index a74a58f8e..000000000 --- a/apps/edr-freight-api/src/migrations/1840000000000-CreateFuelTables.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class CreateFuelTables1840000000000 implements MigrationInterface { - name = "CreateFuelTables1840000000000"; - - public async up(queryRunner: QueryRunner): Promise { - const fuelPurchasesExists = await queryRunner.query( - `SELECT 1 FROM information_schema.tables WHERE table_schema = 'freight' AND table_name = 'fuel_purchases';`, - ); - - if (!fuelPurchasesExists.length) { - await queryRunner.query(` - CREATE TABLE freight.fuel_purchases ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - vehicle_id uuid NOT NULL, - purchase_date timestamptz NOT NULL, - liters numeric(10, 2) NOT NULL, - cost_per_liter numeric(10, 2) NOT NULL, - total_cost numeric(14, 2) NOT NULL, - fuel_station varchar(255) NULL, - payment_method varchar(50) DEFAULT 'CASH', - odometer_reading numeric(10, 2) NULL, - driver_id uuid NULL, - receipt_number varchar(255) NULL, - notes text NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL, - CONSTRAINT pk_fuel_purchases PRIMARY KEY (id), - CONSTRAINT fk_fuel_purchases_vehicle FOREIGN KEY (vehicle_id) - REFERENCES freight.vehicles (id) ON DELETE CASCADE - ); - `); - - await queryRunner.query( - `CREATE INDEX idx_fuel_purchases_vehicle ON freight.fuel_purchases (vehicle_id);`, - ); - await queryRunner.query( - `CREATE INDEX idx_fuel_purchases_date ON freight.fuel_purchases (purchase_date);`, - ); - } - - const fuelConsumptionExists = await queryRunner.query( - `SELECT 1 FROM information_schema.tables WHERE table_schema = 'freight' AND table_name = 'fuel_consumption';`, - ); - - if (!fuelConsumptionExists.length) { - await queryRunner.query(` - CREATE TABLE freight.fuel_consumption ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - vehicle_id uuid NOT NULL, - month date NOT NULL, - total_liters numeric(10, 2) NOT NULL, - total_cost numeric(14, 2) NOT NULL, - total_distance_km numeric(10, 2) NOT NULL, - fuel_efficiency_km_per_l numeric(10, 2) NULL, - number_of_purchases integer DEFAULT 0, - average_cost_per_liter numeric(10, 2) NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL, - CONSTRAINT pk_fuel_consumption PRIMARY KEY (id), - CONSTRAINT fk_fuel_consumption_vehicle FOREIGN KEY (vehicle_id) - REFERENCES freight.vehicles (id) ON DELETE CASCADE, - CONSTRAINT uq_fuel_consumption_vehicle_month UNIQUE (vehicle_id, month) - ); - `); - - await queryRunner.query( - `CREATE INDEX idx_fuel_consumption_vehicle_month ON freight.fuel_consumption (vehicle_id, month);`, - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.fuel_consumption;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.fuel_purchases;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1850000000000-CreateMaintenanceTables.ts b/apps/edr-freight-api/src/migrations/1850000000000-CreateMaintenanceTables.ts deleted file mode 100644 index 26d4afe21..000000000 --- a/apps/edr-freight-api/src/migrations/1850000000000-CreateMaintenanceTables.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class CreateMaintenanceTables1850000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - // Create maintenance_schedules table - const scheduleTableExists = await queryRunner.query(` - SELECT EXISTS( - SELECT 1 FROM information_schema.tables - WHERE table_schema = 'freight' AND table_name = 'maintenance_schedules' - ) - `); - - if (!scheduleTableExists[0].exists) { - await queryRunner.query(` - CREATE TABLE "freight"."maintenance_schedules" ( - "id" uuid NOT NULL DEFAULT gen_random_uuid(), - "vehicle_id" uuid NOT NULL, - "maintenance_type" varchar NOT NULL, - "description" varchar NOT NULL, - "scheduled_date" timestamptz NOT NULL, - "completed_date" timestamptz, - "estimated_cost" numeric(14,2), - "actual_cost" numeric(14,2), - "status" varchar NOT NULL DEFAULT 'SCHEDULED', - "odometer_reading" numeric, - "service_provider" varchar, - "notes" text, - "next_due_km" numeric, - "next_due_date" timestamptz, - "created_at" timestamptz NOT NULL DEFAULT now(), - "updated_at" timestamptz NOT NULL DEFAULT now(), - "deleted_at" timestamptz, - PRIMARY KEY ("id") - ) - `); - - await queryRunner.query( - `CREATE INDEX "idx_maintenance_schedules_vehicle_date" ON "freight"."maintenance_schedules" ("vehicle_id", "scheduled_date")` - ); - } - - // Create maintenance_costs table - const costsTableExists = await queryRunner.query(` - SELECT EXISTS( - SELECT 1 FROM information_schema.tables - WHERE table_schema = 'freight' AND table_name = 'maintenance_costs' - ) - `); - - if (!costsTableExists[0].exists) { - await queryRunner.query(` - CREATE TABLE "freight"."maintenance_costs" ( - "id" uuid NOT NULL DEFAULT gen_random_uuid(), - "vehicle_id" uuid NOT NULL, - "maintenance_schedule_id" uuid, - "incurred_date" timestamptz NOT NULL, - "cost_amount" numeric(14,2) NOT NULL, - "cost_type" varchar NOT NULL, - "description" varchar NOT NULL, - "service_provider" varchar, - "invoice_number" varchar, - "notes" text, - "created_at" timestamptz NOT NULL DEFAULT now(), - "updated_at" timestamptz NOT NULL DEFAULT now(), - "deleted_at" timestamptz, - PRIMARY KEY ("id"), - CONSTRAINT "fk_maintenance_schedule" FOREIGN KEY ("maintenance_schedule_id") - REFERENCES "freight"."maintenance_schedules" ("id") ON DELETE SET NULL - ) - `); - - await queryRunner.query( - `CREATE INDEX "idx_maintenance_costs_vehicle_date" ON "freight"."maintenance_costs" ("vehicle_id", "incurred_date")` - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS "freight"."maintenance_costs"`); - await queryRunner.query(`DROP TABLE IF EXISTS "freight"."maintenance_schedules"`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1860000000000-AddPaidToFirstAndLastMile.ts b/apps/edr-freight-api/src/migrations/1860000000000-AddPaidToFirstAndLastMile.ts deleted file mode 100644 index 261e16099..000000000 --- a/apps/edr-freight-api/src/migrations/1860000000000-AddPaidToFirstAndLastMile.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add paid column to first_mile and last_mile tables to track invoice payment status. - */ -export class AddPaidToFirstAndLastMile1860000000000 - implements MigrationInterface -{ - name = "AddPaidToFirstAndLastMile1860000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.first_mile - ADD COLUMN IF NOT EXISTS paid boolean NOT NULL DEFAULT false; - `); - - await queryRunner.query(` - ALTER TABLE freight.last_mile - ADD COLUMN IF NOT EXISTS paid boolean NOT NULL DEFAULT false; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.first_mile - DROP COLUMN IF EXISTS paid; - `); - - await queryRunner.query(` - ALTER TABLE freight.last_mile - DROP COLUMN IF EXISTS paid; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1861000000000-AddBookingWindowGlobalRules.ts b/apps/edr-freight-api/src/migrations/1861000000000-AddBookingWindowGlobalRules.ts deleted file mode 100644 index a98fcd80d..000000000 --- a/apps/edr-freight-api/src/migrations/1861000000000-AddBookingWindowGlobalRules.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddBookingWindowGlobalRules1861000000000 implements MigrationInterface { - name = "AddBookingWindowGlobalRules1861000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - ADD COLUMN import_window_lead_days integer NOT NULL DEFAULT 3, - ADD COLUMN export_booking_lead_hours integer NOT NULL DEFAULT 24, - ADD COLUMN window_open_hour integer NOT NULL DEFAULT 8, - ADD COLUMN window_duration_hours numeric(4, 2) NOT NULL DEFAULT 3, - ADD COLUMN doc_review_minutes integer NOT NULL DEFAULT 30, - ADD COLUMN payment_window_minutes integer NOT NULL DEFAULT 60, - ADD COLUMN reopen_delay_minutes integer NOT NULL DEFAULT 90; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - DROP COLUMN IF EXISTS import_window_lead_days, - DROP COLUMN IF EXISTS export_booking_lead_hours, - DROP COLUMN IF EXISTS window_open_hour, - DROP COLUMN IF EXISTS window_duration_hours, - DROP COLUMN IF EXISTS doc_review_minutes, - DROP COLUMN IF EXISTS payment_window_minutes, - DROP COLUMN IF EXISTS reopen_delay_minutes; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1861000000001-ReleaseStuckAssignedLocomotives.ts b/apps/edr-freight-api/src/migrations/1861000000001-ReleaseStuckAssignedLocomotives.ts deleted file mode 100644 index 0d5155391..000000000 --- a/apps/edr-freight-api/src/migrations/1861000000001-ReleaseStuckAssignedLocomotives.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * arriveSchedule used to release only the primary locomotive of a train set, leaving - * secondary locomotives ASSIGNED forever. Locomotives are now only ASSIGNED while out - * on a dispatched train — release every ASSIGNED locomotive that is not attached to a - * currently-DISPATCHED schedule. - */ -export class ReleaseStuckAssignedLocomotives1861000000001 implements MigrationInterface { - name = "ReleaseStuckAssignedLocomotives1861000000001"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.locomotives l - SET status = 'AVAILABLE' - WHERE l.status = 'ASSIGNED' - AND NOT EXISTS ( - SELECT 1 - FROM freight.train_schedules ts - JOIN freight.train_sets tset ON tset.id = ts.train_set_id - JOIN ( - SELECT tsl.train_set_id, tsl.locomotive_id - FROM freight.train_set_locomotives tsl - WHERE tsl.deleted_at IS NULL - UNION - SELECT t.id AS train_set_id, t.locomotive_id - FROM freight.train_sets t - WHERE t.locomotive_id IS NOT NULL - ) loco ON loco.train_set_id = tset.id - WHERE ts.status = 'DISPATCHED' - AND ts.deleted_at IS NULL - AND loco.locomotive_id = l.id - ); - `); - } - - public async down(): Promise { - // Data fix — not reversible. - } -} diff --git a/apps/edr-freight-api/src/migrations/1862000000000-AddScheduleWindowPhases.ts b/apps/edr-freight-api/src/migrations/1862000000000-AddScheduleWindowPhases.ts deleted file mode 100644 index cae33a534..000000000 --- a/apps/edr-freight-api/src/migrations/1862000000000-AddScheduleWindowPhases.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddScheduleWindowPhases1862000000000 implements MigrationInterface { - name = "AddScheduleWindowPhases1862000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN window_phase varchar(20) NULL, - ADD COLUMN window_opens_at timestamptz NULL, - ADD COLUMN window_closes_at timestamptz NULL, - ADD COLUMN doc_review_ends_at timestamptz NULL, - ADD COLUMN doc_review_completed_at timestamptz NULL, - ADD COLUMN payment_phase_ends_at timestamptz NULL, - ADD COLUMN booking_cycle_no integer NOT NULL DEFAULT 0; - `); - await queryRunner.query(` - CREATE INDEX idx_train_schedules_window_phase - ON freight.train_schedules (window_phase) - WHERE window_phase IS NOT NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_train_schedules_window_phase;`); - await queryRunner.query(` - ALTER TABLE freight.train_schedules - DROP COLUMN IF EXISTS window_phase, - DROP COLUMN IF EXISTS window_opens_at, - DROP COLUMN IF EXISTS window_closes_at, - DROP COLUMN IF EXISTS doc_review_ends_at, - DROP COLUMN IF EXISTS doc_review_completed_at, - DROP COLUMN IF EXISTS payment_phase_ends_at, - DROP COLUMN IF EXISTS booking_cycle_no; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1863000000000-CreateBookingBatchOffers.ts b/apps/edr-freight-api/src/migrations/1863000000000-CreateBookingBatchOffers.ts deleted file mode 100644 index 779807a57..000000000 --- a/apps/edr-freight-api/src/migrations/1863000000000-CreateBookingBatchOffers.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class CreateBookingBatchOffers1863000000000 implements MigrationInterface { - name = "CreateBookingBatchOffers1863000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE freight.booking_batch_offers ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - booking_id uuid NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE, - train_schedule_id uuid NOT NULL REFERENCES freight.train_schedules(id) ON DELETE CASCADE, - offered_wagons integer NOT NULL, - total_wagons integer NOT NULL, - offered_lines jsonb NULL, - offered_weight_tons numeric(12, 3) NOT NULL, - offered_amount numeric(14, 2) NOT NULL, - offered_pricing_breakdown jsonb NULL, - invoice_id uuid NULL, - payment_deadline timestamptz NOT NULL, - status varchar(10) NOT NULL DEFAULT 'OFFERED', - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL - ); - `); - await queryRunner.query( - `CREATE INDEX idx_booking_batch_offers_booking ON freight.booking_batch_offers (booking_id);`, - ); - await queryRunner.query( - `CREATE INDEX idx_booking_batch_offers_schedule ON freight.booking_batch_offers (train_schedule_id);`, - ); - await queryRunner.query( - `CREATE INDEX idx_booking_batch_offers_status ON freight.booking_batch_offers (status);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_batch_offers;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1870000000000-AddLocationToVehicles.ts b/apps/edr-freight-api/src/migrations/1870000000000-AddLocationToVehicles.ts deleted file mode 100644 index 3434631a2..000000000 --- a/apps/edr-freight-api/src/migrations/1870000000000-AddLocationToVehicles.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add location_id column to vehicles table to track vehicle base location. - */ -export class AddLocationToVehicles1870000000000 implements MigrationInterface { - name = "AddLocationToVehicles1870000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - ADD COLUMN IF NOT EXISTS location_id uuid; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - DROP COLUMN IF EXISTS location_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1870000000000-RepairSynchronizeDrift.ts b/apps/edr-freight-api/src/migrations/1870000000000-RepairSynchronizeDrift.ts deleted file mode 100644 index 32f1e6f0c..000000000 --- a/apps/edr-freight-api/src/migrations/1870000000000-RepairSynchronizeDrift.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Repairs schema drift on databases that were originally built by TypeORM - * `synchronize` (at an older entity snapshot) and never had their migration - * history recorded. Such databases have `freight.migrations` empty while most - * of the schema already exists, so a from-scratch migration run aborts on the - * first non-idempotent statement and never reaches the columns/tables added - * after synchronize was last used. - * - * The deployment procedure for those databases is: - * 1. Baseline every pre-existing migration into `freight.migrations`. - * 2. Run migrations — this file is the only pending one and back-fills the - * objects the drift scan found missing. - * - * Every statement is idempotent (IF NOT EXISTS / guarded CREATE TYPE), so it is - * also safe on a clean database where the earlier migrations already created - * these objects — it simply no-ops. - */ -export class RepairSynchronizeDrift1870000000000 - implements MigrationInterface -{ - name = 'RepairSynchronizeDrift1870000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // --- enum types (derived from entities that never had a source migration) --- - await queryRunner.query(`DO $$ BEGIN - CREATE TYPE freight.consignments_cargo_type_enum AS ENUM ( - 'CONTAINER', 'BULK_LIQUID', 'BULK_DRY', 'GENERAL', 'REFRIGERATED', 'HAZARDOUS' - ); - EXCEPTION WHEN duplicate_object THEN null; END $$;`); - await queryRunner.query(`DO $$ BEGIN - CREATE TYPE freight.consignments_status_enum AS ENUM ( - 'PENDING', 'LOADED', 'IN_TRANSIT', 'AT_DESTINATION', 'DELIVERED', 'RETURNED' - ); - EXCEPTION WHEN duplicate_object THEN null; END $$;`); - await queryRunner.query(`DO $$ BEGIN - CREATE TYPE freight.tracking_events_status_enum AS ENUM ( - 'PENDING', 'LOADED', 'IN_TRANSIT', 'AT_DESTINATION', 'DELIVERED', 'RETURNED' - ); - EXCEPTION WHEN duplicate_object THEN null; END $$;`); - - // --- missing tables --- - await queryRunner.query(`CREATE TABLE IF NOT EXISTS freight.consignments ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - booking_id uuid NOT NULL, - tracking_number varchar(64) NOT NULL, - cargo_type freight.consignments_cargo_type_enum NOT NULL, - weight_kg numeric(12, 2) NOT NULL, - status freight.consignments_status_enum NOT NULL DEFAULT 'PENDING', - origin_station varchar(128) NOT NULL, - destination_station varchar(128) NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT pk_consignments PRIMARY KEY (id), - CONSTRAINT uq_consignments_tracking_number UNIQUE (tracking_number) - );`); - - await queryRunner.query(`CREATE TABLE IF NOT EXISTS freight.tracking_events ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - consignment_id uuid NOT NULL, - location varchar(256) NOT NULL, - status freight.tracking_events_status_enum NOT NULL, - occurred_at timestamptz NOT NULL, - description text, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT pk_tracking_events PRIMARY KEY (id) - );`); - - await queryRunner.query(`CREATE TABLE IF NOT EXISTS freight.fuel_purchases ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - vehicle_id uuid NOT NULL, - purchase_date timestamptz NOT NULL, - liters numeric(10, 2) NOT NULL, - cost_per_liter numeric(10, 2) NOT NULL, - total_cost numeric(14, 2) NOT NULL, - fuel_station varchar(255) NULL, - payment_method varchar(50) DEFAULT 'CASH', - odometer_reading numeric(10, 2) NULL, - driver_id uuid NULL, - receipt_number varchar(255) NULL, - notes text NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL, - CONSTRAINT pk_fuel_purchases PRIMARY KEY (id), - CONSTRAINT fk_fuel_purchases_vehicle FOREIGN KEY (vehicle_id) - REFERENCES freight.vehicles (id) ON DELETE CASCADE - );`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_fuel_purchases_vehicle ON freight.fuel_purchases (vehicle_id);`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_fuel_purchases_date ON freight.fuel_purchases (purchase_date);`); - - await queryRunner.query(`CREATE TABLE IF NOT EXISTS freight.fuel_consumption ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - vehicle_id uuid NOT NULL, - month date NOT NULL, - total_liters numeric(10, 2) NOT NULL, - total_cost numeric(14, 2) NOT NULL, - total_distance_km numeric(10, 2) NOT NULL, - fuel_efficiency_km_per_l numeric(10, 2) NULL, - number_of_purchases integer DEFAULT 0, - average_cost_per_liter numeric(10, 2) NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL, - CONSTRAINT pk_fuel_consumption PRIMARY KEY (id), - CONSTRAINT fk_fuel_consumption_vehicle FOREIGN KEY (vehicle_id) - REFERENCES freight.vehicles (id) ON DELETE CASCADE, - CONSTRAINT uq_fuel_consumption_vehicle_month UNIQUE (vehicle_id, month) - );`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_fuel_consumption_vehicle_month ON freight.fuel_consumption (vehicle_id, month);`); - - await queryRunner.query(`CREATE TABLE IF NOT EXISTS freight.maintenance_schedules ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - vehicle_id uuid NOT NULL, - maintenance_type varchar NOT NULL, - description varchar NOT NULL, - scheduled_date timestamptz NOT NULL, - completed_date timestamptz, - estimated_cost numeric(14,2), - actual_cost numeric(14,2), - status varchar NOT NULL DEFAULT 'SCHEDULED', - odometer_reading numeric, - service_provider varchar, - notes text, - next_due_km numeric, - next_due_date timestamptz, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - PRIMARY KEY (id) - );`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_maintenance_schedules_vehicle_date ON freight.maintenance_schedules (vehicle_id, scheduled_date);`); - - await queryRunner.query(`CREATE TABLE IF NOT EXISTS freight.maintenance_costs ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - vehicle_id uuid NOT NULL, - maintenance_schedule_id uuid, - incurred_date timestamptz NOT NULL, - cost_amount numeric(14,2) NOT NULL, - cost_type varchar NOT NULL, - description varchar NOT NULL, - service_provider varchar, - invoice_number varchar, - notes text, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - PRIMARY KEY (id), - CONSTRAINT fk_maintenance_schedule FOREIGN KEY (maintenance_schedule_id) - REFERENCES freight.maintenance_schedules (id) ON DELETE SET NULL - );`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_maintenance_costs_vehicle_date ON freight.maintenance_costs (vehicle_id, incurred_date);`); - - await queryRunner.query(`CREATE TABLE IF NOT EXISTS freight.otp_verifications ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - phone varchar NOT NULL, - otp varchar NOT NULL, - verified boolean NOT NULL DEFAULT false, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT pk_otp_verifications PRIMARY KEY (id), - CONSTRAINT uq_otp_verifications_phone UNIQUE (phone) - );`); - - await queryRunner.query(`CREATE TABLE IF NOT EXISTS freight.booking_batch_offers ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - booking_id uuid NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE, - train_schedule_id uuid NOT NULL REFERENCES freight.train_schedules(id) ON DELETE CASCADE, - offered_wagons integer NOT NULL, - total_wagons integer NOT NULL, - offered_lines jsonb NULL, - offered_weight_tons numeric(12, 3) NOT NULL, - offered_amount numeric(14, 2) NOT NULL, - offered_pricing_breakdown jsonb NULL, - invoice_id uuid NULL, - payment_deadline timestamptz NOT NULL, - status varchar(10) NOT NULL DEFAULT 'OFFERED', - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL - );`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_booking_batch_offers_booking ON freight.booking_batch_offers (booking_id);`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_booking_batch_offers_schedule ON freight.booking_batch_offers (train_schedule_id);`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_booking_batch_offers_status ON freight.booking_batch_offers (status);`); - - // --- missing columns on existing tables --- - await queryRunner.query(`ALTER TABLE freight.invoices - ADD COLUMN IF NOT EXISTS subtotal_amount numeric(14, 2) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS tax_amount numeric(14, 2) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS paid_amount numeric(14, 2) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS balance_amount numeric(14, 2) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS paid_at timestamptz;`); - - await queryRunner.query(`ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS booking_type varchar(20) NOT NULL DEFAULT 'ONE_TIME', - ADD COLUMN IF NOT EXISTS customer_truck_plate_number varchar(32), - ADD COLUMN IF NOT EXISTS customer_truck_driver_name varchar(120), - ADD COLUMN IF NOT EXISTS customer_truck_type varchar(60), - ADD COLUMN IF NOT EXISTS customer_truck_container_number varchar(16), - ADD COLUMN IF NOT EXISTS customer_truck_assigned_at timestamptz, - ADD COLUMN IF NOT EXISTS customer_truck_arrived_at timestamptz, - ADD COLUMN IF NOT EXISTS bulk_hazardous_quantity numeric(12,3) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS bulk_reefer_quantity numeric(12,3) NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS clearance_current_phase varchar(40), - ADD COLUMN IF NOT EXISTS duty_required boolean, - ADD COLUMN IF NOT EXISTS vessel_departure_date date, - ADD COLUMN IF NOT EXISTS ro_amendment_requested_at timestamptz, - ADD COLUMN IF NOT EXISTS ro_hold_reason text, - ADD COLUMN IF NOT EXISTS pre_clearance_finalized_at timestamptz;`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_bookings_booking_type ON freight.bookings (booking_type);`); - - await queryRunner.query(`ALTER TABLE freight.cargoes - ADD COLUMN IF NOT EXISTS receiver_name varchar, - ADD COLUMN IF NOT EXISTS delivered_at timestamp, - ADD COLUMN IF NOT EXISTS delivery_remarks text;`); - - await queryRunner.query(`ALTER TABLE freight.contract_clearance_cycles - ADD COLUMN IF NOT EXISTS duty_required boolean, - ADD COLUMN IF NOT EXISTS vessel_departure_date date, - ADD COLUMN IF NOT EXISTS ro_amendment_requested_at timestamptz, - ADD COLUMN IF NOT EXISTS ro_hold_reason text, - ADD COLUMN IF NOT EXISTS current_phase varchar(40), - ADD COLUMN IF NOT EXISTS pre_clearance_finalized_at timestamptz;`); - - await queryRunner.query(`ALTER TABLE freight.first_mile - ADD COLUMN IF NOT EXISTS paid boolean NOT NULL DEFAULT false;`); - await queryRunner.query(`ALTER TABLE freight.last_mile - ADD COLUMN IF NOT EXISTS paid boolean NOT NULL DEFAULT false;`); - - await queryRunner.query(`ALTER TABLE freight.route_milestones - ADD COLUMN IF NOT EXISTS distance_km numeric(10,2);`); - - await queryRunner.query(`ALTER TABLE freight.routes - ADD COLUMN IF NOT EXISTS status varchar(32) NOT NULL DEFAULT 'AVAILABLE';`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_routes_status" ON freight.routes (status);`); - - await queryRunner.query(`ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS window_phase varchar(20) NULL, - ADD COLUMN IF NOT EXISTS window_opens_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS window_closes_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS doc_review_ends_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS doc_review_completed_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS payment_phase_ends_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS booking_cycle_no integer NOT NULL DEFAULT 0;`); - await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_train_schedules_window_phase - ON freight.train_schedules (window_phase) WHERE window_phase IS NOT NULL;`); - - await queryRunner.query(`ALTER TABLE freight.train_scheduling_global_rules - ADD COLUMN IF NOT EXISTS import_window_lead_days integer NOT NULL DEFAULT 3, - ADD COLUMN IF NOT EXISTS export_booking_lead_hours integer NOT NULL DEFAULT 24, - ADD COLUMN IF NOT EXISTS window_open_hour integer NOT NULL DEFAULT 8, - ADD COLUMN IF NOT EXISTS window_duration_hours numeric(4, 2) NOT NULL DEFAULT 3, - ADD COLUMN IF NOT EXISTS doc_review_minutes integer NOT NULL DEFAULT 30, - ADD COLUMN IF NOT EXISTS payment_window_minutes integer NOT NULL DEFAULT 60, - ADD COLUMN IF NOT EXISTS reopen_delay_minutes integer NOT NULL DEFAULT 90;`); - } - - public async down(): Promise { - // No-op: this migration only repairs drift by additively creating objects - // that other migrations own. Rolling it back would drop objects those - // migrations legitimately created. Revert individual feature migrations - // instead if needed. - } -} diff --git a/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts b/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts deleted file mode 100644 index 484a2686b..000000000 --- a/apps/edr-freight-api/src/migrations/1880000000000-AddVehicleStatuses.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add FREE and BUSY statuses to vehicle status enum. - */ -export class AddVehicleStatuses1880000000000 implements MigrationInterface { - name = "AddVehicleStatuses1880000000000"; - - public async up(queryRunner: QueryRunner): Promise { - // Create enum type if it doesn't exist - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'vehicles_status_enum' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'freight')) THEN - CREATE TYPE freight.vehicles_status_enum AS ENUM ('ACTIVE', 'FREE', 'BUSY', 'MAINTENANCE', 'RETIRED', 'OUT_OF_SERVICE'); - ELSE - -- Add values if enum already exists but doesn't have them - ALTER TYPE freight.vehicles_status_enum ADD VALUE IF NOT EXISTS 'FREE' BEFORE 'MAINTENANCE'; - ALTER TYPE freight.vehicles_status_enum ADD VALUE IF NOT EXISTS 'BUSY' AFTER 'FREE'; - END IF; - END $$; - `); - } - - public async down(_queryRunner: QueryRunner): Promise { - // Note: Postgres cannot drop individual enum values, so the down migration is a no-op - // The enum values FREE and BUSY will remain but will be unused after downgrade - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000000-SeparateVehicleAvailability.ts b/apps/edr-freight-api/src/migrations/1890000000000-SeparateVehicleAvailability.ts deleted file mode 100644 index c0015c3c6..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000000-SeparateVehicleAvailability.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Split the mixed vehicle status into two fields: - * - status: operational state (ACTIVE, MAINTENANCE, RETIRED, OUT_OF_SERVICE) - * - availability: assignment state (FREE, BUSY) - * - * Existing FREE/BUSY statuses are moved to availability and the status is - * normalized back to ACTIVE. - */ -export class SeparateVehicleAvailability1890000000000 implements MigrationInterface { - name = "SeparateVehicleAvailability1890000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - ADD COLUMN IF NOT EXISTS availability varchar DEFAULT 'FREE' - `); - await queryRunner.query(` - UPDATE freight.vehicles SET availability = 'BUSY' WHERE status = 'BUSY' - `); - await queryRunner.query(` - UPDATE freight.vehicles SET availability = 'FREE' WHERE availability IS NULL - `); - await queryRunner.query(` - UPDATE freight.vehicles SET status = 'ACTIVE' WHERE status IN ('FREE', 'BUSY') - `); - } - - public async down(queryRunner: QueryRunner): Promise { - // Fold availability back into status before dropping the column - await queryRunner.query(` - UPDATE freight.vehicles SET status = availability - WHERE status = 'ACTIVE' AND availability IN ('FREE', 'BUSY') - `); - await queryRunner.query(` - ALTER TABLE freight.vehicles DROP COLUMN IF EXISTS availability - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000001-AddVehicleCodeAndPlates.ts b/apps/edr-freight-api/src/migrations/1890000000001-AddVehicleCodeAndPlates.ts deleted file mode 100644 index 6f9faa1f8..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000001-AddVehicleCodeAndPlates.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add code, power_plate_no and trailer_plate_no columns to vehicles. - * These fields existed in the DTO and UI form but had no entity columns, - * so submitted values were silently dropped. - */ -export class AddVehicleCodeAndPlates1890000000001 implements MigrationInterface { - name = "AddVehicleCodeAndPlates1890000000001"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - ADD COLUMN IF NOT EXISTS code varchar, - ADD COLUMN IF NOT EXISTS power_plate_no varchar, - ADD COLUMN IF NOT EXISTS trailer_plate_no varchar - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - DROP COLUMN IF EXISTS code, - DROP COLUMN IF EXISTS power_plate_no, - DROP COLUMN IF EXISTS trailer_plate_no - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000002-AddFaydaVerificationSessions.ts b/apps/edr-freight-api/src/migrations/1890000000002-AddFaydaVerificationSessions.ts deleted file mode 100644 index 10b358eea..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000002-AddFaydaVerificationSessions.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Session store for the VeriFayda 2.0 OIDC verification flow (ported from - * passenger-api). One row per started verification; `state` is the - * single-use CSRF token linking the eSignet redirect back to the session. - */ -export class AddFaydaVerificationSessions1890000000002 implements MigrationInterface { - name = "AddFaydaVerificationSessions1890000000002"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.fayda_verification_sessions ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - state varchar NOT NULL UNIQUE, - code_verifier varchar NOT NULL, - purpose varchar NOT NULL DEFAULT 'VERIFY', - platform varchar NOT NULL DEFAULT 'WEB', - save_to_account boolean NOT NULL DEFAULT false, - status varchar NOT NULL DEFAULT 'PENDING', - error_code varchar, - error_description text, - iam_user_id uuid, - expires_at timestamptz NOT NULL, - completed_at timestamptz, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_FAYDA_SESSIONS_EXPIRES_AT" - ON freight.fayda_verification_sessions (expires_at) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_FAYDA_SESSIONS_IAM_USER_ID" - ON freight.fayda_verification_sessions (iam_user_id) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.fayda_verification_sessions`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000003-AddDriverFaydaVerification.ts b/apps/edr-freight-api/src/migrations/1890000000003-AddDriverFaydaVerification.ts deleted file mode 100644 index ba8003143..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000003-AddDriverFaydaVerification.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Track Fayda identity verification on drivers: whether the driver's - * identity was verified through VeriFayda and the OIDC subject it was - * verified against. - */ -export class AddDriverFaydaVerification1890000000003 implements MigrationInterface { - name = "AddDriverFaydaVerification1890000000003"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.drivers - ADD COLUMN IF NOT EXISTS fayda_verified boolean DEFAULT false, - ADD COLUMN IF NOT EXISTS fayda_sub varchar - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.drivers - DROP COLUMN IF EXISTS fayda_verified, - DROP COLUMN IF EXISTS fayda_sub - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000004-AddLastMileVehicleAssignments.ts b/apps/edr-freight-api/src/migrations/1890000000004-AddLastMileVehicleAssignments.ts deleted file mode 100644 index 7c1413617..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000004-AddLastMileVehicleAssignments.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Allow more than one vehicle per last-mile delivery. Junction table joins - * last_mile ⇄ vehicles; existing single vehicle_id values are backfilled as - * the first assignment so nothing is lost. - */ -export class AddLastMileVehicleAssignments1890000000004 implements MigrationInterface { - name = "AddLastMileVehicleAssignments1890000000004"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.last_mile_vehicle_assignments ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - last_mile_id uuid NOT NULL REFERENCES freight.last_mile(id) ON DELETE CASCADE, - vehicle_id uuid NOT NULL REFERENCES freight.vehicles(id), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT "UQ_LAST_MILE_VEHICLE" UNIQUE (last_mile_id, vehicle_id) - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_LM_VEHICLE_ASSIGNMENTS_VEHICLE" - ON freight.last_mile_vehicle_assignments (vehicle_id) - `); - // Backfill: existing single-vehicle assignments become the first row - await queryRunner.query(` - INSERT INTO freight.last_mile_vehicle_assignments (last_mile_id, vehicle_id) - SELECT id, vehicle_id FROM freight.last_mile - WHERE vehicle_id IS NOT NULL AND deleted_at IS NULL - ON CONFLICT (last_mile_id, vehicle_id) DO NOTHING - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.last_mile_vehicle_assignments`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000005-AddDriverGender.ts b/apps/edr-freight-api/src/migrations/1890000000005-AddDriverGender.ts deleted file mode 100644 index 2ec26e034..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000005-AddDriverGender.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Store the driver's gender. Prefilled from the Fayda VERIFY response - * (Male/Female) but editable; nullable so existing rows and manual, - * non-Fayda driver records stay valid. - */ -export class AddDriverGender1890000000005 implements MigrationInterface { - name = "AddDriverGender1890000000005"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.drivers - ADD COLUMN IF NOT EXISTS gender varchar - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.drivers - DROP COLUMN IF EXISTS gender - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000006-AddDriverFaydaSubUnique.ts b/apps/edr-freight-api/src/migrations/1890000000006-AddDriverFaydaSubUnique.ts deleted file mode 100644 index 04c9a7000..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000006-AddDriverFaydaSubUnique.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Enforce one driver record per verified Fayda identity. A unique index on - * fayda_sub blocks a second driver from being created against the same Fayda - * OIDC subject; NULLs stay distinct so legacy/unverified rows are unaffected. - */ -export class AddDriverFaydaSubUnique1890000000006 implements MigrationInterface { - name = "AddDriverFaydaSubUnique1890000000006"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DRIVERS_FAYDA_SUB" - ON freight.drivers (fayda_sub) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DROP INDEX IF EXISTS freight."UQ_DRIVERS_FAYDA_SUB" - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000007-DriverUniquePartialSoftDelete.ts b/apps/edr-freight-api/src/migrations/1890000000007-DriverUniquePartialSoftDelete.ts deleted file mode 100644 index 77d0a3043..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000007-DriverUniquePartialSoftDelete.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Make driver uniqueness soft-delete aware. The original table used plain - * column UNIQUE constraints (drivers_email_key, etc.) which count soft-deleted - * rows, so deleting a driver then re-adding the same email/phone/license/Fayda - * identity failed at the DB with a raw 500 — even though the service's own - * (deleted_at-excluding) duplicate check saw nothing. Replace them with partial - * unique indexes scoped to live rows (deleted_at IS NULL) so uniqueness matches - * what the service enforces and freed values become reusable after deletion. - */ -export class DriverUniquePartialSoftDelete1890000000007 implements MigrationInterface { - name = "DriverUniquePartialSoftDelete1890000000007"; - - public async up(queryRunner: QueryRunner): Promise { - // Drop the full-table unique constraints from CreateDriversTable... - await queryRunner.query(` - ALTER TABLE freight.drivers - DROP CONSTRAINT IF EXISTS drivers_email_key, - DROP CONSTRAINT IF EXISTS drivers_phone_number_key, - DROP CONSTRAINT IF EXISTS drivers_license_number_key - `); - // ...and the plain fayda_sub unique index from 1890000000006. - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_DRIVERS_FAYDA_SUB"`); - - // Re-add each as a partial unique index scoped to non-deleted rows. - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DRIVERS_EMAIL_ACTIVE" - ON freight.drivers (email) WHERE deleted_at IS NULL - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DRIVERS_PHONE_ACTIVE" - ON freight.drivers (phone_number) WHERE deleted_at IS NULL - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DRIVERS_LICENSE_ACTIVE" - ON freight.drivers (license_number) WHERE deleted_at IS NULL - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DRIVERS_FAYDA_SUB_ACTIVE" - ON freight.drivers (fayda_sub) WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_DRIVERS_EMAIL_ACTIVE"`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_DRIVERS_PHONE_ACTIVE"`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_DRIVERS_LICENSE_ACTIVE"`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_DRIVERS_FAYDA_SUB_ACTIVE"`); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DRIVERS_FAYDA_SUB" - ON freight.drivers (fayda_sub) - `); - await queryRunner.query(` - ALTER TABLE freight.drivers - ADD CONSTRAINT drivers_email_key UNIQUE (email), - ADD CONSTRAINT drivers_phone_number_key UNIQUE (phone_number), - ADD CONSTRAINT drivers_license_number_key UNIQUE (license_number) - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000008-AddFleetEvents.ts b/apps/edr-freight-api/src/migrations/1890000000008-AddFleetEvents.ts deleted file mode 100644 index 8fbb688d3..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000008-AddFleetEvents.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Append-only audit log for fleet activity (driver↔vehicle assignments, vehicle - * status/availability transitions, first/last-mile vehicle assignments + mile - * status changes). Queried by vehicle_id or driver_id to build a per-record - * timeline. Populated going forward — existing records have no back-history. - */ -export class AddFleetEvents1890000000008 implements MigrationInterface { - name = "AddFleetEvents1890000000008"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.fleet_events ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - event_type varchar NOT NULL, - vehicle_id uuid, - driver_id uuid, - first_mile_id uuid, - last_mile_id uuid, - from_value varchar, - to_value varchar, - label varchar, - metadata jsonb, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_FLEET_EVENTS_VEHICLE" - ON freight.fleet_events (vehicle_id, created_at) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_FLEET_EVENTS_DRIVER" - ON freight.fleet_events (driver_id, created_at) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.fleet_events`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000009-AddLastMileAssignmentContainerNumber.ts b/apps/edr-freight-api/src/migrations/1890000000009-AddLastMileAssignmentContainerNumber.ts deleted file mode 100644 index d8bc1c5c0..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000009-AddLastMileAssignmentContainerNumber.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Container number carried by each vehicle on a last-mile delivery. Auto-filled - * from the booking's container number when present, else entered by the operator - * at assignment time. - */ -export class AddLastMileAssignmentContainerNumber1890000000009 - implements MigrationInterface -{ - name = "AddLastMileAssignmentContainerNumber1890000000009"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.last_mile_vehicle_assignments - ADD COLUMN IF NOT EXISTS container_number varchar - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.last_mile_vehicle_assignments - DROP COLUMN IF EXISTS container_number - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1890000000010-AddLastMileAssignmentDistance.ts b/apps/edr-freight-api/src/migrations/1890000000010-AddLastMileAssignmentDistance.ts deleted file mode 100644 index fc2d30552..000000000 --- a/apps/edr-freight-api/src/migrations/1890000000010-AddLastMileAssignmentDistance.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Per-vehicle actual distance on a last-mile delivery. A booking served by - * several trucks records each truck's km; the record's total (last_mile.exact_km) - * is their sum and drives the invoice. - */ -export class AddLastMileAssignmentDistance1890000000010 - implements MigrationInterface -{ - name = "AddLastMileAssignmentDistance1890000000010"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.last_mile_vehicle_assignments - ADD COLUMN IF NOT EXISTS distance_km numeric(10,2) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.last_mile_vehicle_assignments - DROP COLUMN IF EXISTS distance_km - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1900000000000-AddEmailToOtpVerifications.ts b/apps/edr-freight-api/src/migrations/1900000000000-AddEmailToOtpVerifications.ts deleted file mode 100644 index 55a6568c3..000000000 --- a/apps/edr-freight-api/src/migrations/1900000000000-AddEmailToOtpVerifications.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Support email as a second OTP channel alongside phone (e.g. signup lets the - * user choose which one to verify). `phone` becomes nullable since an - * email-channel row has none, and `email` is added as a nullable unique column - * mirroring `phone`'s shape. - */ -export class AddEmailToOtpVerifications1900000000000 - implements MigrationInterface -{ - name = "AddEmailToOtpVerifications1900000000000"; - - public async up(queryRunner: QueryRunner): Promise { - // The table lives in the `freight` schema (the OtpVerification entity pins - // schema: "freight"). An earlier version of this migration targeted - // `public.otp_verifications`, which does not exist there — leaving the real - // freight table without an `email` column and OTP send failing with - // `column OtpVerification.email does not exist`. Target `freight` explicitly. - await queryRunner.query(` - ALTER TABLE freight.otp_verifications - ALTER COLUMN phone DROP NOT NULL - `); - await queryRunner.query(` - ALTER TABLE freight.otp_verifications - ADD COLUMN IF NOT EXISTS email varchar UNIQUE - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.otp_verifications - DROP COLUMN IF EXISTS email - `); - await queryRunner.query(` - ALTER TABLE freight.otp_verifications - ALTER COLUMN phone SET NOT NULL - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1900000000000-AddLoadingStatusToTrainScheduleBookings.ts b/apps/edr-freight-api/src/migrations/1900000000000-AddLoadingStatusToTrainScheduleBookings.ts deleted file mode 100644 index 6c7235e3b..000000000 --- a/apps/edr-freight-api/src/migrations/1900000000000-AddLoadingStatusToTrainScheduleBookings.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Track per-booking loading confirmation (LOADED/UNLOADED) on train_schedule_bookings. - * Tracking only — does not gate dispatch. - */ -export class AddLoadingStatusToTrainScheduleBookings1900000000000 - implements MigrationInterface -{ - name = "AddLoadingStatusToTrainScheduleBookings1900000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedule_bookings - ADD COLUMN IF NOT EXISTS loading_status varchar(20) NOT NULL DEFAULT 'UNLOADED' - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedule_bookings - DROP COLUMN IF EXISTS loading_status - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1900000000000-SimplifyRatesAndWeightLimitRules.ts b/apps/edr-freight-api/src/migrations/1900000000000-SimplifyRatesAndWeightLimitRules.ts deleted file mode 100644 index 7a661bc7c..000000000 --- a/apps/edr-freight-api/src/migrations/1900000000000-SimplifyRatesAndWeightLimitRules.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Simplify the rate + weight-limit configuration model: - * - * 1. Drop the effective_from / effective_to validity window from both - * `rates` and `weight_limit_rules`. Rates are now activated purely by - * the approval workflow (status = LIVE) and weight limits are always - * active for their container + direction. No time-travel scheduling. - * - * 2. Enforce "one rate per pattern" with partial unique indexes so the same - * configuration (e.g. FIRST_MILE for a given container type) cannot be - * duplicated. NULL scope columns are COALESCE-normalised because Postgres - * treats NULLs as distinct in a plain unique index. - * - * This migration is destructive on the date columns — existing effective_* - * values are dropped. - */ -export class SimplifyRatesAndWeightLimitRules1900000000000 implements MigrationInterface { - name = 'SimplifyRatesAndWeightLimitRules1900000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // ── 1. De-duplicate existing data so the unique indexes can be created ── - // Keep the most recently-created row per pattern, soft-delete the rest. - await queryRunner.query(` - WITH ranked AS ( - SELECT id, - row_number() OVER ( - PARTITION BY rate_type, - COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid), - COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid), - COALESCE(trade_direction, ''), - rate_unit - ORDER BY created_at DESC, id DESC - ) AS rn - FROM freight.rates - WHERE deleted_at IS NULL AND status <> 'SUPERSEDED' - ) - UPDATE freight.rates r - SET deleted_at = now() - FROM ranked - WHERE r.id = ranked.id AND ranked.rn > 1; - `); - - await queryRunner.query(` - WITH ranked AS ( - SELECT id, - row_number() OVER ( - PARTITION BY container_type_id, trade_direction - ORDER BY created_at DESC, id DESC - ) AS rn - FROM freight.weight_limit_rules - WHERE deleted_at IS NULL - ) - UPDATE freight.weight_limit_rules w - SET deleted_at = now() - FROM ranked - WHERE w.id = ranked.id AND ranked.rn > 1; - `); - - // ── 2. Drop the effective-date indexes + columns ─────────────────────── - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_rates_effective_from";`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_weight_limit_rules_effective_from";`); - // Indexes created by TypeORM's @Index carry generated hashed names — drop - // any index that references the effective_from column defensively. - await queryRunner.query(` - DO $$ - DECLARE idx record; - BEGIN - FOR idx IN - SELECT indexname FROM pg_indexes - WHERE schemaname = 'freight' - AND tablename IN ('rates', 'weight_limit_rules') - AND indexdef ILIKE '%effective_from%' - LOOP - EXECUTE format('DROP INDEX IF EXISTS freight.%I', idx.indexname); - END LOOP; - END $$; - `); - - await queryRunner.query(`ALTER TABLE freight.rates DROP COLUMN IF EXISTS effective_from;`); - await queryRunner.query(`ALTER TABLE freight.rates DROP COLUMN IF EXISTS effective_to;`); - await queryRunner.query(`ALTER TABLE freight.weight_limit_rules DROP COLUMN IF EXISTS effective_from;`); - await queryRunner.query(`ALTER TABLE freight.weight_limit_rules DROP COLUMN IF EXISTS effective_to;`); - - // ── 3. One-rate-per-pattern partial unique indexes ───────────────────── - // The unit is part of the identity so a surcharge can legitimately carry two - // rows that bill different ways (e.g. reefer PER_CONTAINER + reefer PER_TON), - // while still blocking a true duplicate (same rateType + scope + unit). - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_rates_pattern" - ON freight.rates ( - rate_type, - COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid), - COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid), - COALESCE(trade_direction, ''), - rate_unit - ) - WHERE deleted_at IS NULL AND status <> 'SUPERSEDED'; - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_weight_limit_rules_pattern" - ON freight.weight_limit_rules (container_type_id, trade_direction) - WHERE deleted_at IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern";`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_weight_limit_rules_pattern";`); - - await queryRunner.query(`ALTER TABLE freight.rates ADD COLUMN IF NOT EXISTS effective_from date;`); - await queryRunner.query(`UPDATE freight.rates SET effective_from = COALESCE(effective_from, created_at::date);`); - await queryRunner.query(`ALTER TABLE freight.rates ALTER COLUMN effective_from SET NOT NULL;`); - await queryRunner.query(`ALTER TABLE freight.rates ADD COLUMN IF NOT EXISTS effective_to date;`); - - await queryRunner.query(`ALTER TABLE freight.weight_limit_rules ADD COLUMN IF NOT EXISTS effective_from date;`); - await queryRunner.query(`ALTER TABLE freight.weight_limit_rules ADD COLUMN IF NOT EXISTS effective_to date;`); - - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_rates_effective_from" ON freight.rates (effective_from);`); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_weight_limit_rules_effective_from" ON freight.weight_limit_rules (effective_from);`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/1910000000000-WidenWindowDurationHoursPrecision.ts b/apps/edr-freight-api/src/migrations/1910000000000-WidenWindowDurationHoursPrecision.ts deleted file mode 100644 index 194c0d056..000000000 --- a/apps/edr-freight-api/src/migrations/1910000000000-WidenWindowDurationHoursPrecision.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Widen train_scheduling_global_rules.window_duration_hours from numeric(4,2) - * to numeric(6,4). The UI now lets staff enter the booking-window duration in - * minutes / hours / days and converts to the column's native hours unit; a - * 4-minute window is 0.0667h, which numeric(4,2) rounds to 0.07 (≈3.96 min). - * Four decimals store sub-minute durations exactly (0.0667h → 4.00 min). - */ -export class WidenWindowDurationHoursPrecision1910000000000 - implements MigrationInterface -{ - name = "WidenWindowDurationHoursPrecision1910000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - ALTER COLUMN window_duration_hours TYPE numeric(6, 4); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - ALTER COLUMN window_duration_hours TYPE numeric(4, 2); - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1920000000000-AddScheduleWindowRuleSnapshot.ts b/apps/edr-freight-api/src/migrations/1920000000000-AddScheduleWindowRuleSnapshot.ts deleted file mode 100644 index d48e127c3..000000000 --- a/apps/edr-freight-api/src/migrations/1920000000000-AddScheduleWindowRuleSnapshot.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Snapshot the booking-window rule onto each train schedule. - * - * A schedule's window (open time + reopen cycles) must be frozen to the rule it - * was created with: a later global-rules edit applies only to FUTURE schedules, - * while an already-open schedule keeps its base rule. Previously the batch board - * recomputed windows from the LIVE global config, so editing the rule redrew the - * board for open schedules (a synthetic grid that no longer matched the window - * the customer was shown). These columns give the board a per-schedule rule to - * derive its display windows from. - * - * Existing rows are backfilled from the current global-rules singleton — the best - * available base, since they never stored one. Their stamped windowOpensAt/ - * windowClosesAt are still real, so only projected reopen cycles rely on the - * backfill. - */ -export class AddScheduleWindowRuleSnapshot1920000000000 - implements MigrationInterface -{ - name = "AddScheduleWindowRuleSnapshot1920000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS rule_window_open_hour integer, - ADD COLUMN IF NOT EXISTS rule_window_duration_hours numeric(6, 4), - ADD COLUMN IF NOT EXISTS rule_reopen_delay_minutes integer, - ADD COLUMN IF NOT EXISTS rule_import_window_lead_days integer, - ADD COLUMN IF NOT EXISTS rule_export_booking_lead_hours integer; - `); - - // Backfill from the global-rules singleton so pre-existing schedules render. - await queryRunner.query(` - UPDATE freight.train_schedules ts - SET - rule_window_open_hour = COALESCE(ts.rule_window_open_hour, r.window_open_hour), - rule_window_duration_hours = COALESCE(ts.rule_window_duration_hours, r.window_duration_hours), - rule_reopen_delay_minutes = COALESCE(ts.rule_reopen_delay_minutes, r.reopen_delay_minutes), - rule_import_window_lead_days = COALESCE(ts.rule_import_window_lead_days, r.import_window_lead_days), - rule_export_booking_lead_hours = COALESCE(ts.rule_export_booking_lead_hours, r.export_booking_lead_hours) - FROM freight.train_scheduling_global_rules r - WHERE ts.rule_window_open_hour IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - DROP COLUMN IF EXISTS rule_window_open_hour, - DROP COLUMN IF EXISTS rule_window_duration_hours, - DROP COLUMN IF EXISTS rule_reopen_delay_minutes, - DROP COLUMN IF EXISTS rule_import_window_lead_days, - DROP COLUMN IF EXISTS rule_export_booking_lead_hours; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1930000000000-AddMaxCapacityToWeightLimitRules.ts b/apps/edr-freight-api/src/migrations/1930000000000-AddMaxCapacityToWeightLimitRules.ts deleted file mode 100644 index b1218a9b3..000000000 --- a/apps/edr-freight-api/src/migrations/1930000000000-AddMaxCapacityToWeightLimitRules.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add a hard per-unit weight ceiling to weight limit rules. - * - * maxVgmTons stays the soft "overweight" threshold (surcharge + warning); - * max_capacity_tons is the absolute ceiling above which a booking cannot be - * created at all. Null means no ceiling (existing behavior). - */ -export class AddMaxCapacityToWeightLimitRules1930000000000 - implements MigrationInterface -{ - name = "AddMaxCapacityToWeightLimitRules1930000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.weight_limit_rules - ADD COLUMN IF NOT EXISTS max_capacity_tons numeric(8, 3); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.weight_limit_rules - DROP COLUMN IF EXISTS max_capacity_tons; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1940000000000-AddFirstMileVehicleAssignments.ts b/apps/edr-freight-api/src/migrations/1940000000000-AddFirstMileVehicleAssignments.ts deleted file mode 100644 index 42f2c4eba..000000000 --- a/apps/edr-freight-api/src/migrations/1940000000000-AddFirstMileVehicleAssignments.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Allow more than one vehicle per first-mile pickup. Junction table joins - * first_mile ⇄ vehicles, with each truck's container number + actual distance; - * existing single vehicle_id values are backfilled as the first assignment so - * nothing is lost. Mirrors the last-mile vehicle-assignment schema. - */ -export class AddFirstMileVehicleAssignments1940000000000 implements MigrationInterface { - name = "AddFirstMileVehicleAssignments1940000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.first_mile_vehicle_assignments ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - first_mile_id uuid NOT NULL REFERENCES freight.first_mile(id) ON DELETE CASCADE, - vehicle_id uuid NOT NULL REFERENCES freight.vehicles(id), - container_number varchar, - distance_km numeric(10,2), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT "UQ_FIRST_MILE_VEHICLE" UNIQUE (first_mile_id, vehicle_id) - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_FM_VEHICLE_ASSIGNMENTS_VEHICLE" - ON freight.first_mile_vehicle_assignments (vehicle_id) - `); - // Backfill: existing single-vehicle assignments become the first row - await queryRunner.query(` - INSERT INTO freight.first_mile_vehicle_assignments (first_mile_id, vehicle_id) - SELECT id, vehicle_id FROM freight.first_mile - WHERE vehicle_id IS NOT NULL AND deleted_at IS NULL - ON CONFLICT (first_mile_id, vehicle_id) DO NOTHING - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.first_mile_vehicle_assignments`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1940000000000-AddWagonTypeFkToCargoAndContainerTypes.ts b/apps/edr-freight-api/src/migrations/1940000000000-AddWagonTypeFkToCargoAndContainerTypes.ts deleted file mode 100644 index c7ab60577..000000000 --- a/apps/edr-freight-api/src/migrations/1940000000000-AddWagonTypeFkToCargoAndContainerTypes.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Replace load-type string matching with a real wagon-type foreign key. - * - * Before this migration, train scheduling picked a wagon type by matching - * strings — a hardcoded cargo-code → wagon-code map for bulk (COFFEE→KW2, …) - * and a fixed NW5 default for every container. This adds `wagon_type_id` FKs on - * `cargo_types` and `container_types` so scheduling resolves the wagon type - * through the relation instead. - * - * The columns are NULLABLE: cargo grouping rows and container/legacy cargo that - * never ship in bulk have no wagon type, and forcing one onto them is - * meaningless. Scheduling enforces the requirement at run time (it throws when a - * scheduled bulk cargo type or a container type in the batch has no wagon type). - * - * Backfill reproduces the old hardcoded resolution one final time so existing - * bulk cargo + container rows are not left unset. After this, the runtime map is - * removed — the FK is the single source of truth. - */ -export class AddWagonTypeFkToCargoAndContainerTypes1940000000000 - implements MigrationInterface -{ - name = "AddWagonTypeFkToCargoAndContainerTypes1940000000000"; - - public async up(queryRunner: QueryRunner): Promise { - // ── Columns + FKs ──────────────────────────────────────────────────────── - await queryRunner.query(` - ALTER TABLE freight.cargo_types - ADD COLUMN IF NOT EXISTS wagon_type_id uuid; - `); - await queryRunner.query(` - ALTER TABLE freight.container_types - ADD COLUMN IF NOT EXISTS wagon_type_id uuid; - `); - - await queryRunner.query(` - ALTER TABLE freight.cargo_types - ADD CONSTRAINT fk_cargo_types_wagon_type - FOREIGN KEY (wagon_type_id) - REFERENCES freight.wagon_types(id) - ON DELETE RESTRICT; - `); - await queryRunner.query(` - ALTER TABLE freight.container_types - ADD CONSTRAINT fk_container_types_wagon_type - FOREIGN KEY (wagon_type_id) - REFERENCES freight.wagon_types(id) - ON DELETE RESTRICT; - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_cargo_types_wagon_type_id - ON freight.cargo_types (wagon_type_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_container_types_wagon_type_id - ON freight.container_types (wagon_type_id); - `); - - // ── Backfill: old cargo-code → wagon-code map (one last time) ───────────── - // COFFEE/GRAIN/WHEAT/SORGHUM/CORN → KW2, FERTILIZER/SUGAR → PW2, - // COAL → KW3, STEEL/ORE → CW3. Unmapped bulk cargo → CW3 (old default). - const cargoCodeToWagon: Record = { - COFFEE: "KW2", - GRAIN: "KW2", - WHEAT: "KW2", - SORGHUM: "KW2", - CORN: "KW2", - FERTILIZER: "PW2", - SUGAR: "PW2", - COAL: "KW3", - STEEL: "CW3", - ORE: "CW3", - }; - - for (const [cargoCode, wagonCode] of Object.entries(cargoCodeToWagon)) { - await queryRunner.query( - ` - UPDATE freight.cargo_types ct - SET wagon_type_id = wt.id - FROM freight.wagon_types wt - WHERE wt.code = $1 - AND UPPER(TRIM(ct.code)) = $2 - AND ct.wagon_type_id IS NULL; - `, - [wagonCode, cargoCode], - ); - } - - // Remaining bulk cargo (PER_TON) without a mapped code → default bulk wagon CW3. - await queryRunner.query(` - UPDATE freight.cargo_types ct - SET wagon_type_id = wt.id - FROM freight.wagon_types wt - WHERE wt.code = 'CW3' - AND ct.wagon_type_id IS NULL - AND ct.unit_of_measure = 'PER_TON'; - `); - - // All container types → the old container default wagon NW5. - await queryRunner.query(` - UPDATE freight.container_types ct - SET wagon_type_id = wt.id - FROM freight.wagon_types wt - WHERE wt.code = 'NW5' - AND ct.wagon_type_id IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DROP INDEX IF EXISTS freight.idx_container_types_wagon_type_id; - `); - await queryRunner.query(` - DROP INDEX IF EXISTS freight.idx_cargo_types_wagon_type_id; - `); - await queryRunner.query(` - ALTER TABLE freight.container_types - DROP CONSTRAINT IF EXISTS fk_container_types_wagon_type; - `); - await queryRunner.query(` - ALTER TABLE freight.cargo_types - DROP CONSTRAINT IF EXISTS fk_cargo_types_wagon_type; - `); - await queryRunner.query(` - ALTER TABLE freight.container_types DROP COLUMN IF EXISTS wagon_type_id; - `); - await queryRunner.query(` - ALTER TABLE freight.cargo_types DROP COLUMN IF EXISTS wagon_type_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1950000000000-AddCustomerTruckAssignments.ts b/apps/edr-freight-api/src/migrations/1950000000000-AddCustomerTruckAssignments.ts deleted file mode 100644 index 7c95199b1..000000000 --- a/apps/edr-freight-api/src/migrations/1950000000000-AddCustomerTruckAssignments.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Multi-truck customer (self-haul) assignment. Replaces the single - * booking.customer_truck_* fields with a per-booking list of trucks, each - * carrying 1–2 containers and tracking its own arrival. The legacy - * booking.customer_truck_* columns are kept as a synced booking-level flag - * (any truck assigned / all trucks arrived) so the warehouse exit-gate and - * delivery-approval logic keep working. - */ -export class AddCustomerTruckAssignments1950000000000 implements MigrationInterface { - name = 'AddCustomerTruckAssignments1950000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.customer_truck_assignments ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - booking_id uuid NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE, - plate_number varchar(32) NOT NULL, - driver_name varchar(120) NOT NULL, - truck_type varchar(60) NOT NULL, - assigned_at timestamptz NOT NULL DEFAULT now(), - arrived_at timestamptz, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ); - `); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_customer_truck_assignments_booking" ON freight.customer_truck_assignments (booking_id);`, - ); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.customer_truck_containers ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - assignment_id uuid NOT NULL REFERENCES freight.customer_truck_assignments(id) ON DELETE CASCADE, - booking_id uuid NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE, - container_number varchar(64) NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ); - `); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_customer_truck_containers_assignment" ON freight.customer_truck_containers (assignment_id);`, - ); - // One container number can be loaded onto exactly one truck per booking. - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_customer_truck_containers_booking_number" - ON freight.customer_truck_containers (booking_id, container_number) - WHERE deleted_at IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.customer_truck_containers;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.customer_truck_assignments;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1950000000000-AddVehicleCompliance.ts b/apps/edr-freight-api/src/migrations/1950000000000-AddVehicleCompliance.ts deleted file mode 100644 index f83f0a236..000000000 --- a/apps/edr-freight-api/src/migrations/1950000000000-AddVehicleCompliance.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Vehicle Compliance & Expiry Alerts. - * - Adds expiry-tracking columns to freight.vehicles. - * - Creates freight.compliance_records for per-document compliance tracking. - */ -export class AddVehicleCompliance1950000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - // Vehicle expiry / compliance columns. - await queryRunner.query(` - ALTER TABLE freight.vehicles - ADD COLUMN IF NOT EXISTS vin VARCHAR, - ADD COLUMN IF NOT EXISTS ownership VARCHAR, - ADD COLUMN IF NOT EXISTS insurance_expiry DATE, - ADD COLUMN IF NOT EXISTS registration_expiry DATE, - ADD COLUMN IF NOT EXISTS next_inspection_date DATE; - `); - - // Compliance records table. - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.compliance_records ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - vehicle_id UUID NOT NULL REFERENCES freight.vehicles(id), - type VARCHAR NOT NULL, - document_number VARCHAR, - issued_date DATE, - expiry_date DATE NOT NULL, - status VARCHAR NOT NULL DEFAULT 'VALID', - notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_compliance_records_vehicle_id ON freight.compliance_records(vehicle_id);`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_compliance_records_expiry_date ON freight.compliance_records(expiry_date);`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_compliance_records_type ON freight.compliance_records(type);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.compliance_records CASCADE;`); - await queryRunner.query(` - ALTER TABLE freight.vehicles - DROP COLUMN IF EXISTS vin, - DROP COLUMN IF EXISTS ownership, - DROP COLUMN IF EXISTS insurance_expiry, - DROP COLUMN IF EXISTS registration_expiry, - DROP COLUMN IF EXISTS next_inspection_date; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1950000000000-AddWindowCloseHour.ts b/apps/edr-freight-api/src/migrations/1950000000000-AddWindowCloseHour.ts deleted file mode 100644 index 6ff9bdc12..000000000 --- a/apps/edr-freight-api/src/migrations/1950000000000-AddWindowCloseHour.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add the daily booking-desk close hour. - * - * The import booking window used to reopen only within the same EAT calendar day - * as its close; a cycle whose reopen crossed midnight died at CLOSED_FOR_DAY with - * capacity still free. The window now runs a daily office range [openHour, - * closeHour): a not-yet-full train pauses at closeHour and resumes the next - * morning at openHour, every day until it fills or departs. openHour === closeHour - * means a 24-hour desk. - * - * `window_close_hour` on the global-rules singleton is the live config; the - * matching `rule_window_close_hour` snapshot on each schedule freezes it at - * creation so the batch board keeps drawing the window the customer was shown. - * Both default/backfill to 17:00 (5 PM), the previous implicit office close. - */ -export class AddWindowCloseHour1950000000000 implements MigrationInterface { - name = "AddWindowCloseHour1950000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - ADD COLUMN IF NOT EXISTS window_close_hour integer NOT NULL DEFAULT 17; - `); - - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS rule_window_close_hour integer; - `); - - // Backfill the snapshot from the global-rules singleton so pre-existing - // schedules keep projecting reopen cycles. - await queryRunner.query(` - UPDATE freight.train_schedules ts - SET rule_window_close_hour = COALESCE(ts.rule_window_close_hour, r.window_close_hour) - FROM freight.train_scheduling_global_rules r - WHERE ts.rule_window_close_hour IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - DROP COLUMN IF EXISTS rule_window_close_hour; - `); - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - DROP COLUMN IF EXISTS window_close_hour; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1950000000000-CreateNotifications.ts b/apps/edr-freight-api/src/migrations/1950000000000-CreateNotifications.ts deleted file mode 100644 index c5c608a95..000000000 --- a/apps/edr-freight-api/src/migrations/1950000000000-CreateNotifications.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * In-app notification inbox. One row per recipient per logical notification; - * producers fan out by inserting many rows. Indexed for the two hot queries: - * unread-count (recipient + is_read) and the newest-first list (recipient + - * created_at). Enum-like columns are stored as varchar to avoid PG enum churn. - */ -export class CreateNotifications1950000000000 implements MigrationInterface { - name = "CreateNotifications1950000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.notifications ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - recipient_user_id uuid NOT NULL, - audience varchar(20) NOT NULL, - type varchar(48) NOT NULL DEFAULT 'GENERIC', - title varchar(200) NOT NULL, - body text NOT NULL, - link varchar, - data jsonb, - priority varchar(12) NOT NULL DEFAULT 'NORMAL', - is_read boolean NOT NULL DEFAULT false, - read_at timestamptz, - channels_sent jsonb, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_NOTIFICATIONS_RECIPIENT_UNREAD" - ON freight.notifications (recipient_user_id, is_read) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_NOTIFICATIONS_RECIPIENT_CREATED" - ON freight.notifications (recipient_user_id, created_at) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_NOTIFICATIONS_RECIPIENT_CREATED"`, - ); - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_NOTIFICATIONS_RECIPIENT_UNREAD"`, - ); - await queryRunner.query(`DROP TABLE IF EXISTS freight.notifications`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1960000000000-AddContainerReceiptToBookingContainerUnits.ts b/apps/edr-freight-api/src/migrations/1960000000000-AddContainerReceiptToBookingContainerUnits.ts deleted file mode 100644 index 59a0441c5..000000000 --- a/apps/edr-freight-api/src/migrations/1960000000000-AddContainerReceiptToBookingContainerUnits.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Per-container receive tracking. A booking's containers arrive individually - * (on separate self-haul trucks), so each container unit tracks whether it has - * been received into the port and, once staff confirm it, the GRN it belongs to. - * A single GRN covers the containers received together — so if the whole booking - * arrives at once, all its units share one GRN (per-booking GRN). - */ -export class AddContainerReceiptToBookingContainerUnits1960000000000 - implements MigrationInterface -{ - name = 'AddContainerReceiptToBookingContainerUnits1960000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_container_units - ADD COLUMN IF NOT EXISTS received_to_port boolean NOT NULL DEFAULT false, - ADD COLUMN IF NOT EXISTS received_at timestamptz, - ADD COLUMN IF NOT EXISTS grn_number varchar(100) - `); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_booking_container_units_grn" ON freight.booking_container_units (grn_number);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_booking_container_units_grn";`); - await queryRunner.query(` - ALTER TABLE freight.booking_container_units - DROP COLUMN IF EXISTS received_to_port, - DROP COLUMN IF EXISTS received_at, - DROP COLUMN IF EXISTS grn_number - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1960000000000-AddIncidents.ts b/apps/edr-freight-api/src/migrations/1960000000000-AddIncidents.ts deleted file mode 100644 index dbb2994c3..000000000 --- a/apps/edr-freight-api/src/migrations/1960000000000-AddIncidents.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Accident & Incident register for the fleet. Tracks accidents, breakdowns, - * traffic violations, thefts and other incidents against a vehicle, driver - * and/or booking, with severity, damage estimate, insurance claim tracking and - * a lifecycle status. Queried by driver_id for per-driver incident history. - */ -export class AddIncidents1960000000000 implements MigrationInterface { - name = 'AddIncidents1960000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.incidents ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - vehicle_id uuid, - driver_id uuid, - booking_id uuid, - type varchar NOT NULL, - severity varchar NOT NULL, - occurred_at timestamptz NOT NULL, - location varchar, - description text NOT NULL, - damage_estimate numeric(14,2), - status varchar NOT NULL DEFAULT 'REPORTED', - insurance_claim_number varchar, - reported_by varchar - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_INCIDENTS_DRIVER" - ON freight.incidents (driver_id, occurred_at) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_INCIDENTS_VEHICLE" - ON freight.incidents (vehicle_id, occurred_at) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.incidents`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1970000000000-AddCustomerTruckDeparture.ts b/apps/edr-freight-api/src/migrations/1970000000000-AddCustomerTruckDeparture.ts deleted file mode 100644 index e36420751..000000000 --- a/apps/edr-freight-api/src/migrations/1970000000000-AddCustomerTruckDeparture.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Import self-haul trucks are weighed on leaving. The customer does not - * pre-specify what an import truck takes — staff register the containers loaded - * and the weighed gross when the truck departs. These columns capture that. - */ -export class AddCustomerTruckDeparture1970000000000 implements MigrationInterface { - name = 'AddCustomerTruckDeparture1970000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.customer_truck_assignments - ADD COLUMN IF NOT EXISTS gross_weight_kg numeric(14, 2), - ADD COLUMN IF NOT EXISTS departed_at timestamptz - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.customer_truck_assignments - DROP COLUMN IF EXISTS gross_weight_kg, - DROP COLUMN IF EXISTS departed_at - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1970000000000-AddMaintenanceDepth.ts b/apps/edr-freight-api/src/migrations/1970000000000-AddMaintenanceDepth.ts deleted file mode 100644 index 87b52ceff..000000000 --- a/apps/edr-freight-api/src/migrations/1970000000000-AddMaintenanceDepth.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddMaintenanceDepth1970000000000 implements MigrationInterface { - name = 'AddMaintenanceDepth1970000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS freight`); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.work_orders ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - vehicle_id UUID NOT NULL, - title VARCHAR NOT NULL, - description TEXT, - status VARCHAR NOT NULL DEFAULT 'OPEN', - priority VARCHAR NOT NULL DEFAULT 'MEDIUM', - assigned_to VARCHAR, - opened_at TIMESTAMPTZ NOT NULL DEFAULT now(), - closed_at TIMESTAMPTZ, - labor_cost NUMERIC(14, 2), - parts_cost NUMERIC(14, 2), - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.parts ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR NOT NULL, - sku VARCHAR, - category VARCHAR, - quantity_in_stock INT NOT NULL DEFAULT 0, - reorder_level INT NOT NULL DEFAULT 0, - unit_cost NUMERIC(14, 2), - location VARCHAR, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warranties ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - vehicle_id UUID NOT NULL, - component VARCHAR NOT NULL, - provider VARCHAR, - start_date DATE, - expiry_date DATE NOT NULL, - coverage_notes TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ - ); - `); - - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_work_orders_vehicle_id_status" ON freight.work_orders (vehicle_id, status)`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_parts_category" ON freight.parts (category)`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_warranties_vehicle_id_expiry_date" ON freight.warranties (vehicle_id, expiry_date)`, - ); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.work_orders - ADD CONSTRAINT "FK_work_orders_vehicle_id" - FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE CASCADE; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - - await queryRunner.query(` - DO $$ BEGIN - ALTER TABLE freight.warranties - ADD CONSTRAINT "FK_warranties_vehicle_id" - FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE CASCADE; - EXCEPTION WHEN duplicate_object THEN NULL; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.warranties CASCADE`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.parts CASCADE`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.work_orders CASCADE`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1980000000000-AddBookingHandovers.ts b/apps/edr-freight-api/src/migrations/1980000000000-AddBookingHandovers.ts deleted file mode 100644 index a47ff6297..000000000 --- a/apps/edr-freight-api/src/migrations/1980000000000-AddBookingHandovers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Structured import handover records. Replaces the ad-hoc handover notes so a - * booking can carry one handover (single truck) or several (one per truck when - * multiple trucks are used). Timing differs by mile type: - * - SELF_HAUL: generated on first truck arrival, signed before the truck leaves. - * - EDR_LAST_MILE: generated at delivery (after exit); signed on delivery. - */ -export class AddBookingHandovers1980000000000 implements MigrationInterface { - name = 'AddBookingHandovers1980000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.booking_handovers ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - booking_id uuid NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE, - truck_assignment_id uuid REFERENCES freight.customer_truck_assignments(id) ON DELETE SET NULL, - truck_plate varchar(32), - mile_type varchar(20) NOT NULL, - reference varchar(100) NOT NULL, - generated_at timestamptz NOT NULL DEFAULT now(), - signed_at timestamptz, - signed_by_user_id uuid, - delivered_at timestamptz, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ); - `); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_booking_handovers_booking" ON freight.booking_handovers (booking_id);`, - ); - // At most one live handover per (booking, customer truck). EDR trucks (which - // aren't customer_truck_assignments) and per-booking handovers are de-duped - // in the service, since a NULL truck_assignment_id can't be uniquely indexed. - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_booking_handovers_booking_truck" - ON freight.booking_handovers (booking_id, truck_assignment_id) - WHERE deleted_at IS NULL AND truck_assignment_id IS NOT NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_handovers;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1980000000000-AddProcurement.ts b/apps/edr-freight-api/src/migrations/1980000000000-AddProcurement.ts deleted file mode 100644 index 6d4304aaf..000000000 --- a/apps/edr-freight-api/src/migrations/1980000000000-AddProcurement.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddProcurement1980000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.vendors ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - name varchar NOT NULL, - type varchar, - contact_person varchar, - phone varchar, - email varchar, - address varchar, - is_active boolean NOT NULL DEFAULT true - ); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.asset_acquisitions ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - vehicle_id uuid, - vendor_id uuid, - acquisition_type varchar NOT NULL, - acquisition_date date NOT NULL, - cost numeric(14,2), - useful_life_months integer, - salvage_value numeric(14,2), - lease_start date, - lease_end date, - monthly_payment numeric(14,2), - status varchar NOT NULL DEFAULT 'ACTIVE', - notes text - ); - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_asset_acquisitions_vehicle_date - ON freight.asset_acquisitions(vehicle_id, acquisition_date); - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.asset_disposals ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - vehicle_id uuid NOT NULL, - disposal_date date NOT NULL, - method varchar NOT NULL, - sale_price numeric(14,2), - buyer varchar, - notes text - ); - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_asset_disposals_vehicle_date - ON freight.asset_disposals(vehicle_id, disposal_date); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.asset_disposals CASCADE;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.asset_acquisitions CASCADE;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.vendors CASCADE;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1980000000000-YardCountryEnumAndRouteDirection.ts b/apps/edr-freight-api/src/migrations/1980000000000-YardCountryEnumAndRouteDirection.ts deleted file mode 100644 index 3274b4d1c..000000000 --- a/apps/edr-freight-api/src/migrations/1980000000000-YardCountryEnumAndRouteDirection.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Yard country becomes a two-value enum (Ethiopia | Djibouti) and every route - * freezes its trade direction from the yard countries: - * Ethiopia → Djibouti = EXPORT, Djibouti → Ethiopia = IMPORT, - * same country = DOMESTIC (shown as "Intercity"; disabled for scheduling - * and contracts for now). - * - * Existing yard rows are normalized case-insensitively; anything mentioning - * Djibouti maps there, everything else maps to Ethiopia (the line only serves - * these two countries). A CHECK constraint keeps future writes honest. - */ -export class YardCountryEnumAndRouteDirection1980000000000 implements MigrationInterface { - name = 'YardCountryEnumAndRouteDirection1980000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.yards - SET country = CASE - WHEN lower(trim(country)) LIKE '%djib%' THEN 'Djibouti' - ELSE 'Ethiopia' - END - `); - await queryRunner.query(` - ALTER TABLE freight.yards - DROP CONSTRAINT IF EXISTS chk_yards_country, - ADD CONSTRAINT chk_yards_country CHECK (country IN ('Ethiopia', 'Djibouti')) - `); - - await queryRunner.query(` - ALTER TABLE freight.routes - ADD COLUMN IF NOT EXISTS direction varchar(10) - `); - await queryRunner.query(` - UPDATE freight.routes r - SET direction = CASE - WHEN o.country = 'Djibouti' AND d.country = 'Ethiopia' THEN 'IMPORT' - WHEN o.country = 'Ethiopia' AND d.country = 'Djibouti' THEN 'EXPORT' - ELSE 'DOMESTIC' - END - FROM freight.yards o, freight.yards d - WHERE o.id = r.origin_yard_id - AND d.id = r.destination_yard_id - `); - // Orphan origin/destination (deleted yard) — no way to classify; park as - // DOMESTIC, which is blocked everywhere, so nothing can schedule on it. - await queryRunner.query(` - UPDATE freight.routes SET direction = 'DOMESTIC' WHERE direction IS NULL - `); - await queryRunner.query(` - ALTER TABLE freight.routes - ALTER COLUMN direction SET NOT NULL, - DROP CONSTRAINT IF EXISTS chk_routes_direction, - ADD CONSTRAINT chk_routes_direction CHECK (direction IN ('IMPORT', 'EXPORT', 'DOMESTIC')) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.routes - DROP CONSTRAINT IF EXISTS chk_routes_direction, - DROP COLUMN IF EXISTS direction - `); - await queryRunner.query(` - ALTER TABLE freight.yards DROP CONSTRAINT IF EXISTS chk_yards_country - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1990000000000-AddDoubleHandlingBasisAndMachinery.ts b/apps/edr-freight-api/src/migrations/1990000000000-AddDoubleHandlingBasisAndMachinery.ts deleted file mode 100644 index be4ed060a..000000000 --- a/apps/edr-freight-api/src/migrations/1990000000000-AddDoubleHandlingBasisAndMachinery.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Double-handling fee support. warehouse_fee_rules.basis: how a - * DOUBLE_HANDLING_FEE rule is charged — PER_CONTAINER | PER_TON | PER_ITEM - * (null for the day-based fee types). The PER_TON / PER_ITEM quantity comes from - * the booking's cargo total (cargo_total_weight_vgm, expressed in the cargo's - * unit of measure), so no new booking column is needed. - */ -export class AddDoubleHandlingBasisAndMachinery1990000000000 implements MigrationInterface { - name = 'AddDoubleHandlingBasisAndMachinery1990000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.warehouse_fee_rules ADD COLUMN IF NOT EXISTS basis varchar(20)`, - ); - // machinery_units is not used (PER_ITEM reads cargo_total_weight_vgm); drop it - // if a prior version of this migration added it. - await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS machinery_units`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE freight.warehouse_fee_rules DROP COLUMN IF EXISTS basis`); - } -} diff --git a/apps/edr-freight-api/src/migrations/1990000000000-AddVehiclePricePerKm.ts b/apps/edr-freight-api/src/migrations/1990000000000-AddVehiclePricePerKm.ts deleted file mode 100644 index 7767cd796..000000000 --- a/apps/edr-freight-api/src/migrations/1990000000000-AddVehiclePricePerKm.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Per-km haulage rate on a vehicle (mainly trucks) plus the currency it's - * quoted in (ETB | USD, default ETB). - */ -export class AddVehiclePricePerKm1990000000000 implements MigrationInterface { - name = "AddVehiclePricePerKm1990000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - ADD COLUMN IF NOT EXISTS price_per_km numeric(14,2), - ADD COLUMN IF NOT EXISTS currency varchar(8) NOT NULL DEFAULT 'ETB' - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - DROP COLUMN IF EXISTS price_per_km, - DROP COLUMN IF EXISTS currency - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/1990000000000-SegmentCorridorBookings.ts b/apps/edr-freight-api/src/migrations/1990000000000-SegmentCorridorBookings.ts deleted file mode 100644 index ca88ef7cd..000000000 --- a/apps/edr-freight-api/src/migrations/1990000000000-SegmentCorridorBookings.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Segment corridor bookings: a booking may ride only part of a train's route - * (its own origin→destination leg), so dispatch/arrival become per-booking - * facts and wagon capacity is consumed per leg instead of per whole route. - * - * - bookings.loaded_at / arrived_at (+ by-user): operator-confirmed load at - * the booking's origin yard and unload at its destination yard. Clearance - * gates read arrived_at, not the train's actual_arrival_at. - * - train_set_wagons.board_yard_id / alight_yard_id: the leg a consist slot - * occupies; NULL/NULL = whole route (legacy). Non-overlapping legs coexist - * without consuming each other's capacity. - * - wagon_movements: auditable ledger of every physical wagon relocation - * (loaded leg / empty reposition / manual correction) with the acting user. - */ -export class SegmentCorridorBookings1990000000000 implements MigrationInterface { - name = 'SegmentCorridorBookings1990000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS loaded_at timestamptz, - ADD COLUMN IF NOT EXISTS loaded_by_user_id uuid, - ADD COLUMN IF NOT EXISTS arrived_at timestamptz, - ADD COLUMN IF NOT EXISTS arrived_by_user_id uuid; - `); - - await queryRunner.query(` - ALTER TABLE freight.train_set_wagons - ADD COLUMN IF NOT EXISTS board_yard_id uuid REFERENCES freight.yards(id) ON DELETE SET NULL, - ADD COLUMN IF NOT EXISTS alight_yard_id uuid REFERENCES freight.yards(id) ON DELETE SET NULL; - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.wagon_movements ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - wagon_id uuid NOT NULL REFERENCES freight.wagons(id) ON DELETE CASCADE, - from_yard_id uuid REFERENCES freight.yards(id), - to_yard_id uuid NOT NULL REFERENCES freight.yards(id), - train_schedule_id uuid REFERENCES freight.train_schedules(id) ON DELETE SET NULL, - booking_id uuid REFERENCES freight.bookings(id) ON DELETE SET NULL, - kind varchar(30) NOT NULL, - moved_by_user_id uuid, - occurred_at timestamptz NOT NULL DEFAULT now(), - note text, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ); - `); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_wagon_movements_wagon_occurred" ON freight.wagon_movements (wagon_id, occurred_at);`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_wagon_movements_schedule" ON freight.wagon_movements (train_schedule_id);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_movements;`); - await queryRunner.query(` - ALTER TABLE freight.train_set_wagons - DROP COLUMN IF EXISTS board_yard_id, - DROP COLUMN IF EXISTS alight_yard_id; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS loaded_at, - DROP COLUMN IF EXISTS loaded_by_user_id, - DROP COLUMN IF EXISTS arrived_at, - DROP COLUMN IF EXISTS arrived_by_user_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2000000000000-AddCustomsPriorityConfig.ts b/apps/edr-freight-api/src/migrations/2000000000000-AddCustomsPriorityConfig.ts deleted file mode 100644 index c7009c303..000000000 --- a/apps/edr-freight-api/src/migrations/2000000000000-AddCustomsPriorityConfig.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Moves service-level priority off the service_types table and onto the - * admin-managed priority_configs table as a new CUSTOMS rule type. - * - * - Drops service_types.priority_bonus_points (replaced by CUSTOMS configs). - * - Widens priority_configs.type CHECK to allow 'CUSTOMS' (currency must be - * null, same as WAGON). - * - Seeds the two customs wagon-count tiers: 1–10 → 7 pts, 11–53 → 15 pts. - * CUSTOMS rules apply only when the booking's service type includesCustoms. - */ -export class AddCustomsPriorityConfig2000000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.service_types DROP COLUMN IF EXISTS priority_bonus_points; - `); - - await queryRunner.query(` - ALTER TABLE freight.priority_configs - DROP CONSTRAINT IF EXISTS priority_configs_type_check; - `); - await queryRunner.query(` - ALTER TABLE freight.priority_configs - ADD CONSTRAINT priority_configs_type_check - CHECK (type IN ('WAGON', 'CURRENCY', 'CUSTOMS')); - `); - - await queryRunner.query(` - ALTER TABLE freight.priority_configs - DROP CONSTRAINT IF EXISTS chk_currency_for_type; - `); - await queryRunner.query(` - ALTER TABLE freight.priority_configs - ADD CONSTRAINT chk_currency_for_type CHECK ( - (type = 'WAGON' AND currency IS NULL) OR - (type = 'CURRENCY' AND currency IS NOT NULL) OR - (type = 'CUSTOMS' AND currency IS NULL) - ); - `); - - await queryRunner.query(` - INSERT INTO freight.priority_configs - (type, label, currency, min_wagon_count, max_wagon_count, score_points, is_active, display_order) - VALUES - ('CUSTOMS', 'With customs 1–10 wagons', NULL, 1, 10, 7, true, 1), - ('CUSTOMS', 'With customs 11–53 wagons', NULL, 11, 53, 15, true, 2); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DELETE FROM freight.priority_configs WHERE type = 'CUSTOMS'; - `); - - await queryRunner.query(` - ALTER TABLE freight.priority_configs - DROP CONSTRAINT IF EXISTS chk_currency_for_type; - `); - await queryRunner.query(` - ALTER TABLE freight.priority_configs - ADD CONSTRAINT chk_currency_for_type CHECK ( - (type = 'WAGON' AND currency IS NULL) OR - (type = 'CURRENCY' AND currency IS NOT NULL) - ); - `); - - await queryRunner.query(` - ALTER TABLE freight.priority_configs - DROP CONSTRAINT IF EXISTS priority_configs_type_check; - `); - await queryRunner.query(` - ALTER TABLE freight.priority_configs - ADD CONSTRAINT priority_configs_type_check - CHECK (type IN ('WAGON', 'CURRENCY')); - `); - - await queryRunner.query(` - ALTER TABLE freight.service_types - ADD COLUMN IF NOT EXISTS priority_bonus_points INT NOT NULL DEFAULT 0; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2000000000000-AddGpsTracking.ts b/apps/edr-freight-api/src/migrations/2000000000000-AddGpsTracking.ts deleted file mode 100644 index 3d440c678..000000000 --- a/apps/edr-freight-api/src/migrations/2000000000000-AddGpsTracking.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * GPS tracking: physical trackers (gps_devices, one denormalized latest fix per - * device for the live map) + append-only fix history (gps_positions). - */ -export class AddGpsTracking2000000000000 implements MigrationInterface { - name = "AddGpsTracking2000000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.gps_devices ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - imei varchar(20) NOT NULL UNIQUE, - name varchar, - vehicle_id uuid REFERENCES freight.vehicles(id), - status varchar(16) NOT NULL DEFAULT 'REGISTERED', - last_seen_at timestamptz, - last_lat numeric(10,6), - last_lng numeric(10,6), - last_speed numeric(6,2), - last_course int, - last_fix_at timestamptz, - voltage_level int, - gsm_level int, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_GPS_DEVICES_VEHICLE" - ON freight.gps_devices (vehicle_id) - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.gps_positions ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - device_id uuid NOT NULL, - imei varchar(20) NOT NULL, - vehicle_id uuid, - lat numeric(10,6) NOT NULL, - lng numeric(10,6) NOT NULL, - speed numeric(6,2) NOT NULL DEFAULT 0, - course int NOT NULL DEFAULT 0, - satellites int NOT NULL DEFAULT 0, - positioned boolean NOT NULL DEFAULT false, - gps_time timestamptz NOT NULL, - alarm int NOT NULL DEFAULT 0, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_GPS_POSITIONS_DEVICE_TIME" - ON freight.gps_positions (device_id, gps_time) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_GPS_POSITIONS_VEHICLE_TIME" - ON freight.gps_positions (vehicle_id, gps_time) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.gps_positions`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.gps_devices`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2000000000000-AddTruckDetentionTiming.ts b/apps/edr-freight-api/src/migrations/2000000000000-AddTruckDetentionTiming.ts deleted file mode 100644 index ce7bfd326..000000000 --- a/apps/edr-freight-api/src/migrations/2000000000000-AddTruckDetentionTiming.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Truck detention support. - * - last_mile.arrived_at / delivered_at: the detention window for an EDR - * last-mile vehicle. The clock runs from arrival at destination; the customer - * has a grace period (default 3h) to clear/return, after which detention - * accrues per truck per day until delivered_at (or now, if still out). - * - warehouse_fee_rules.free_hours: configurable grace window (hours) for a - * TRUCK_DETENTION_FEE rule; null/0 falls back to the 3-hour default. - */ -export class AddTruckDetentionTiming2000000000000 implements MigrationInterface { - name = 'AddTruckDetentionTiming2000000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.last_mile ADD COLUMN IF NOT EXISTS arrived_at timestamptz`, - ); - await queryRunner.query( - `ALTER TABLE freight.last_mile ADD COLUMN IF NOT EXISTS delivered_at timestamptz`, - ); - await queryRunner.query( - `ALTER TABLE freight.warehouse_fee_rules ADD COLUMN IF NOT EXISTS free_hours int`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE freight.warehouse_fee_rules DROP COLUMN IF EXISTS free_hours`); - await queryRunner.query(`ALTER TABLE freight.last_mile DROP COLUMN IF EXISTS delivered_at`); - await queryRunner.query(`ALTER TABLE freight.last_mile DROP COLUMN IF EXISTS arrived_at`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2000000000000-CreateCompanyChangeRequest.ts b/apps/edr-freight-api/src/migrations/2000000000000-CreateCompanyChangeRequest.ts deleted file mode 100644 index 9d8d2b5c9..000000000 --- a/apps/edr-freight-api/src/migrations/2000000000000-CreateCompanyChangeRequest.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -/** - * Staging table for customer profile edits that require backoffice review. An - * already-approved company's settings edits are snapshotted here (Pending) - * instead of being written to the live `companies` row; a reviewer approves - * (snapshot applied) or rejects with a note (customer amends & resubmits). - */ -export class CreateCompanyChangeRequest2000000000000 - implements MigrationInterface -{ - name = 'CreateCompanyChangeRequest2000000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'company_change_request', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'company_id', type: 'uuid' }, - { name: 'snapshot', type: 'jsonb' }, - { name: 'documents', type: 'jsonb', isNullable: true }, - { name: 'status', type: 'varchar', length: '20', default: "'pending'" }, - { name: 'note', type: 'text', isNullable: true }, - { name: 'submitted_by', type: 'uuid', isNullable: true }, - { name: 'submitted_at', type: 'timestamptz', isNullable: true }, - { name: 'reviewed_by', type: 'uuid', isNullable: true }, - { name: 'reviewed_at', type: 'timestamptz', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['company_id'], - referencedSchema: 'freight', - referencedTableName: 'companies', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.company_change_request', - new TableIndex({ name: 'idx_company_change_request_company', columnNames: ['company_id'] }), - ); - await queryRunner.createIndex( - 'freight.company_change_request', - new TableIndex({ name: 'idx_company_change_request_status', columnNames: ['status'] }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.company_change_request', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/2000000000001-AddCompanyProfileReview.ts b/apps/edr-freight-api/src/migrations/2000000000001-AddCompanyProfileReview.ts deleted file mode 100644 index 2c2fdf3e1..000000000 --- a/apps/edr-freight-api/src/migrations/2000000000001-AddCompanyProfileReview.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; - -/** - * Adds reviewer note/id/timestamp to company_profiles so a rejected operational - * role (new ProfileStatus 'rejected') can carry the reason back to the customer, - * who can then amend and reapply. - */ -export class AddCompanyProfileReview2000000000001 - implements MigrationInterface -{ - name = 'AddCompanyProfileReview2000000000001'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.addColumns('freight.company_profiles', [ - new TableColumn({ name: 'review_note', type: 'text', isNullable: true }), - new TableColumn({ name: 'reviewed_by', type: 'uuid', isNullable: true }), - new TableColumn({ name: 'reviewed_at', type: 'timestamptz', isNullable: true }), - ]); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumns('freight.company_profiles', [ - 'review_note', - 'reviewed_by', - 'reviewed_at', - ]); - } -} diff --git a/apps/edr-freight-api/src/migrations/2010000000000-AddConsolidationResumeStatus.ts b/apps/edr-freight-api/src/migrations/2010000000000-AddConsolidationResumeStatus.ts deleted file mode 100644 index cb6f7dc91..000000000 --- a/apps/edr-freight-api/src/migrations/2010000000000-AddConsolidationResumeStatus.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Adds bookings.consolidation_resume_status: the status a booking parked in - * PENDING_CONSOLIDATION returns to once it pairs with a wagon partner. - * - * Direct customer bookings leave it NULL (they resume to SUBMITTED, unchanged). - * Contract-drawdown bookings (GL shipments) set it to the status - * createUnderContract would otherwise have used (OPERATION_REQUEST_PENDING or - * AWAITING_DOCUMENTS), so pairing resumes them into the contract-booking flow - * instead of wrongly moving them to SUBMITTED. - */ -export class AddConsolidationResumeStatus2010000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS consolidation_resume_status VARCHAR(40); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS consolidation_resume_status; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2010000000000-AddFeeRuleVehicleType.ts b/apps/edr-freight-api/src/migrations/2010000000000-AddFeeRuleVehicleType.ts deleted file mode 100644 index a71f3cb5b..000000000 --- a/apps/edr-freight-api/src/migrations/2010000000000-AddFeeRuleVehicleType.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Truck-detention rules can be scoped by vehicle type (TRUCK / VAN / TRAILER / - * TANKER / FLATBED / …), so different truck types carry different detention - * rates. Null = applies to any truck type. - */ -export class AddFeeRuleVehicleType2010000000000 implements MigrationInterface { - name = 'AddFeeRuleVehicleType2010000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.warehouse_fee_rules ADD COLUMN IF NOT EXISTS vehicle_type varchar(20)`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE freight.warehouse_fee_rules DROP COLUMN IF EXISTS vehicle_type`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2020000000000-RepairOtpEmailSchema.ts b/apps/edr-freight-api/src/migrations/2020000000000-RepairOtpEmailSchema.ts deleted file mode 100644 index e11923a4d..000000000 --- a/apps/edr-freight-api/src/migrations/2020000000000-RepairOtpEmailSchema.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Repair: AddEmailToOtpVerifications1900000000000 originally altered - * `public.otp_verifications`, but the OtpVerification entity pins - * schema: "freight". On any DB where that migration already ran (and is recorded - * as executed, so it won't run again), the real `freight.otp_verifications` table - * never got the `email` column and `phone` was never made nullable — so OTP send - * dies with `column OtpVerification.email does not exist`. - * - * This migration re-applies the change against the correct schema. Idempotent - * (IF NOT EXISTS / no-op DROP NOT NULL), and guarded so it's a no-op when the - * freight table is absent. - */ -export class RepairOtpEmailSchema2020000000000 implements MigrationInterface { - name = "RepairOtpEmailSchema2020000000000"; - - public async up(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable("freight.otp_verifications"); - if (!exists) return; - - await queryRunner.query(` - ALTER TABLE freight.otp_verifications - ALTER COLUMN phone DROP NOT NULL - `); - await queryRunner.query(` - ALTER TABLE freight.otp_verifications - ADD COLUMN IF NOT EXISTS email varchar UNIQUE - `); - } - - public async down(queryRunner: QueryRunner): Promise { - const exists = await queryRunner.hasTable("freight.otp_verifications"); - if (!exists) return; - - await queryRunner.query(` - ALTER TABLE freight.otp_verifications - DROP COLUMN IF EXISTS email - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2020000000000-WarehouseCapacityKgToTons.ts b/apps/edr-freight-api/src/migrations/2020000000000-WarehouseCapacityKgToTons.ts deleted file mode 100644 index 333a0f541..000000000 --- a/apps/edr-freight-api/src/migrations/2020000000000-WarehouseCapacityKgToTons.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Weights are tonnes everywhere. Warehouse / yard / zone capacity was stored in - * kg (e.g. 25000, 5000, 2500) — convert existing rows to tonnes (÷1000). Cargo - * weight (warehouse_inventory.weight ← cargo_total_weight_vgm) is already tonnes - * and is NOT touched; truck gross weight has no data yet. Runs exactly once - * (tracked by TypeORM) — re-running would divide again. - */ -export class WarehouseCapacityKgToTons2020000000000 implements MigrationInterface { - name = 'WarehouseCapacityKgToTons2020000000000'; - - private readonly tables = ['warehouses', 'warehouse_yards', 'warehouse_zones']; - private readonly columns = ['capacity_weight', 'current_weight', 'max_weight']; - - public async up(queryRunner: QueryRunner): Promise { - for (const table of this.tables) { - for (const column of this.columns) { - await queryRunner.query( - `UPDATE freight.${table} SET ${column} = ${column} / 1000.0 WHERE ${column} IS NOT NULL`, - ); - } - } - } - - public async down(queryRunner: QueryRunner): Promise { - for (const table of this.tables) { - for (const column of this.columns) { - await queryRunner.query( - `UPDATE freight.${table} SET ${column} = ${column} * 1000.0 WHERE ${column} IS NOT NULL`, - ); - } - } - } -} diff --git a/apps/edr-freight-api/src/migrations/2030000000000-AddTrainScheduleReference.ts b/apps/edr-freight-api/src/migrations/2030000000000-AddTrainScheduleReference.ts deleted file mode 100644 index a05465c53..000000000 --- a/apps/edr-freight-api/src/migrations/2030000000000-AddTrainScheduleReference.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Adds train_schedules.reference: a human-facing unique schedule number - * S-YYYY-NNNNN (per-year sequence, like bookings' BK-YYYY-NNNNNN). - * - * - Adds the nullable column. - * - Backfills existing rows: within each created-at year, numbers rows by - * created_at ascending (oldest → S--00001). Deterministic order. - * - Adds a partial unique index (NULLs allowed so a future insert can stage - * the row before the app stamps its reference). - */ -export class AddTrainScheduleReference2030000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS reference VARCHAR(20); - `); - - // Backfill per-year, ordered by created_at (oldest = 00001). Uses the row's - // own created-at year as the reference year so historical rows keep a - // sensible number. - await queryRunner.query(` - WITH numbered AS ( - SELECT - id, - EXTRACT(YEAR FROM created_at)::int AS yr, - ROW_NUMBER() OVER ( - PARTITION BY EXTRACT(YEAR FROM created_at) - ORDER BY created_at ASC, id ASC - ) AS seq - FROM freight.train_schedules - WHERE reference IS NULL - ) - UPDATE freight.train_schedules ts - SET reference = 'S-' || numbered.yr || '-' || LPAD(numbered.seq::text, 5, '0') - FROM numbered - WHERE ts.id = numbered.id; - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS ux_train_schedules_reference - ON freight.train_schedules (reference) - WHERE reference IS NOT NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DROP INDEX IF EXISTS freight.ux_train_schedules_reference; - `); - await queryRunner.query(` - ALTER TABLE freight.train_schedules DROP COLUMN IF EXISTS reference; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2040000000000-AddLocomotiveOverageTolerance.ts b/apps/edr-freight-api/src/migrations/2040000000000-AddLocomotiveOverageTolerance.ts deleted file mode 100644 index 4bda8dde1..000000000 --- a/apps/edr-freight-api/src/migrations/2040000000000-AddLocomotiveOverageTolerance.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Adds locomotives.overage_tolerance_tons / overage_tolerance_meters: an - * optional per-locomotive deviation allowance above max_pull_weight_tons / - * max_train_length_meters. Nullable, defaults to no tolerance so existing - * strict-cap behavior is unchanged until staff sets a value. - */ -export class AddLocomotiveOverageTolerance2040000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.locomotives - ADD COLUMN IF NOT EXISTS overage_tolerance_tons NUMERIC(10, 3), - ADD COLUMN IF NOT EXISTS overage_tolerance_meters NUMERIC(10, 3); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.locomotives - DROP COLUMN IF EXISTS overage_tolerance_tons, - DROP COLUMN IF EXISTS overage_tolerance_meters; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2040000000000-MigrateLicenseFilesToFileRecords.ts b/apps/edr-freight-api/src/migrations/2040000000000-MigrateLicenseFilesToFileRecords.ts deleted file mode 100644 index d3720de33..000000000 --- a/apps/edr-freight-api/src/migrations/2040000000000-MigrateLicenseFilesToFileRecords.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Business-license files used to live inline as a jsonb array on - * `company_profiles.business_license_files`. They now belong to the FileRecord - * model (`freight.files`, resource `company_profiles`, code `business_license`) - * so they get stable ids and stream through `GET /api/files/:id` — the same - * proxy path regular documents use — instead of broken direct-MinIO URLs. - * - * This copies each existing inline entry into `freight.files` by reference - * (keeping the stored object URL — no bytes are re-uploaded). The original jsonb - * column is left intact for rollback safety. - */ -export class MigrateLicenseFilesToFileRecords2040000000000 - implements MigrationInterface -{ - name = "MigrateLicenseFilesToFileRecords2040000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - INSERT INTO freight.files - (id, resource_id, resource, code, name, url, size, mime_type, created_at, updated_at) - SELECT - gen_random_uuid(), - cp.id, - 'company_profiles', - 'business_license', - COALESCE(elem->>'name', 'license'), - elem->>'url', - COALESCE(NULLIF(elem->>'size', '')::int, 0), - COALESCE(NULLIF(elem->>'mimeType', ''), 'application/octet-stream'), - now(), - now() - FROM freight.company_profiles cp - CROSS JOIN LATERAL jsonb_array_elements(cp.business_license_files) AS elem - WHERE cp.business_license_files IS NOT NULL - AND jsonb_typeof(cp.business_license_files) = 'array' - AND elem->>'url' IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM freight.files f - WHERE f.resource_id = cp.id - AND f.resource = 'company_profiles' - AND f.code = 'business_license' - AND f.url = elem->>'url' - AND f.deleted_at IS NULL - ); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - // Reverse the model migration by dropping the license FileRecords. The - // original jsonb column was never cleared, so the data still exists there. - await queryRunner.query(` - DELETE FROM freight.files - WHERE resource = 'company_profiles' - AND code = 'business_license'; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2050000000000-AddCustomerTruckContainerLoadedAt.ts b/apps/edr-freight-api/src/migrations/2050000000000-AddCustomerTruckContainerLoadedAt.ts deleted file mode 100644 index 332df2423..000000000 --- a/apps/edr-freight-api/src/migrations/2050000000000-AddCustomerTruckContainerLoadedAt.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Adds customer_truck_containers.loaded_at so an assignment (customer planning - * which containers ride which truck) is distinct from the container actually - * being loaded. Stage LOADED now requires loaded_at; customer assignment alone - * keeps the container at its prior stage (RECEIVED/GRN) with its planned truck - * shown. Backfills containers on already-departed trucks (they left loaded). - */ -export class AddCustomerTruckContainerLoadedAt2050000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.customer_truck_containers - ADD COLUMN IF NOT EXISTS loaded_at TIMESTAMPTZ; - `); - - await queryRunner.query(` - UPDATE freight.customer_truck_containers ctc - SET loaded_at = a.departed_at - FROM freight.customer_truck_assignments a - WHERE a.id = ctc.assignment_id - AND a.departed_at IS NOT NULL - AND ctc.deleted_at IS NULL - AND ctc.loaded_at IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.customer_truck_containers DROP COLUMN IF EXISTS loaded_at; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2050000000000-DropWagonTypeMaxWagonsPerTrain.ts b/apps/edr-freight-api/src/migrations/2050000000000-DropWagonTypeMaxWagonsPerTrain.ts deleted file mode 100644 index 927504ee6..000000000 --- a/apps/edr-freight-api/src/migrations/2050000000000-DropWagonTypeMaxWagonsPerTrain.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Drops wagon_types.max_wagons_per_train. Train wagon-count caps are already - * derived from locomotive + wagon length/weight (train-capacity.util.ts) and - * the global train_scheduling_global_rules row — this per-wagon-type override - * was unused by that derivation and only added a confusing "Max / train" - * field to the wagon type form. - */ -export class DropWagonTypeMaxWagonsPerTrain2050000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagon_types - DROP COLUMN IF EXISTS max_wagons_per_train; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagon_types - ADD COLUMN IF NOT EXISTS max_wagons_per_train INT; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2060000000000-CreateYardDistances.ts b/apps/edr-freight-api/src/migrations/2060000000000-CreateYardDistances.ts deleted file mode 100644 index 42352237d..000000000 --- a/apps/edr-freight-api/src/migrations/2060000000000-CreateYardDistances.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Configured rail distance between two yards (Configuration → Yard Distances). - * Route creation resolves each segment's km from here (symmetric lookup: - * one A↔B row serves both directions) instead of accepting free-text km, - * and snapshots the value onto route_milestones.distance_km. - * - * Uniqueness is a partial index (deleted_at IS NULL) so a soft-deleted pair - * can be re-created. - */ -export class CreateYardDistances2060000000000 implements MigrationInterface { - name = 'CreateYardDistances2060000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.yard_distances ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - from_yard_id uuid NOT NULL REFERENCES freight.yards(id), - to_yard_id uuid NOT NULL REFERENCES freight.yards(id), - distance_km numeric(10,2) NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_yard_distances_from_yard - ON freight.yard_distances (from_yard_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_yard_distances_to_yard - ON freight.yard_distances (to_yard_id); - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS uq_yard_distances_pair - ON freight.yard_distances (from_yard_id, to_yard_id) - WHERE deleted_at IS NULL; - `); - // Backfill from segments already stored on existing routes so editing them - // does not immediately fail the "pair not configured" check. One row per - // unordered pair; where routes disagree the longest segment wins. - await queryRunner.query(` - INSERT INTO freight.yard_distances (from_yard_id, to_yard_id, distance_km) - SELECT DISTINCT ON (LEAST(prev_yard_id, yard_id), GREATEST(prev_yard_id, yard_id)) - prev_yard_id, yard_id, distance_km - FROM ( - SELECT - yard_id, - distance_km, - LAG(yard_id) OVER (PARTITION BY route_id ORDER BY sequence_no) AS prev_yard_id - FROM freight.route_milestones - WHERE deleted_at IS NULL - ) segments - WHERE prev_yard_id IS NOT NULL - AND distance_km IS NOT NULL - AND distance_km > 0 - ORDER BY LEAST(prev_yard_id, yard_id), GREATEST(prev_yard_id, yard_id), distance_km DESC - ON CONFLICT DO NOTHING; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.yard_distances;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2060000000000-SeedRailWagonTypes.ts b/apps/edr-freight-api/src/migrations/2060000000000-SeedRailWagonTypes.ts deleted file mode 100644 index ed9e8fae2..000000000 --- a/apps/edr-freight-api/src/migrations/2060000000000-SeedRailWagonTypes.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Upserts the 10 real EDR wagon types (code, name, capacity, length, tare - * weight) by code. Overwrites any existing row with the same code so - * previously-seeded demo values (e.g. NW5/PW2/CW3 from demo-bookings.seeder) - * are replaced with the real spec. - */ -export class SeedRailWagonTypes2060000000000 implements MigrationInterface { - private readonly wagonTypes = [ - { code: 'NW7', name: 'Double deck sedan wagon', capacityTons: 22, lengthMeters: 26.066, tareWeightTons: 37.1 }, - { code: 'NW5', name: 'Flat wagon', capacityTons: 70, lengthMeters: 13.966, tareWeightTons: 22.4 }, - { code: 'PW2', name: 'Box wagon', capacityTons: 70, lengthMeters: 17.066, tareWeightTons: 25.2 }, - { code: 'GW2', name: 'Tank wagon', capacityTons: 70, lengthMeters: 12.228, tareWeightTons: 23 }, - { code: 'CW4', name: 'Gondola covered wagon', capacityTons: 70, lengthMeters: 13.976, tareWeightTons: 24.8 }, - { code: 'CW3', name: 'Gondola open wagon', capacityTons: 70, lengthMeters: 13.976, tareWeightTons: 23.4 }, - { code: 'KW2', name: 'Hopper covered wagon', capacityTons: 69, lengthMeters: 16.466, tareWeightTons: 25.2 }, - { code: 'KW3', name: 'Hopper wagon open', capacityTons: 70, lengthMeters: 14.4, tareWeightTons: 24 }, - { code: 'NW6', name: 'Flat wagon (long)', capacityTons: 70, lengthMeters: 18.56, tareWeightTons: 25.3 }, - { code: 'BW1', name: 'Refrigerated wagon', capacityTons: 38, lengthMeters: 21.996, tareWeightTons: 32.1 }, - ]; - - public async up(queryRunner: QueryRunner): Promise { - for (const wt of this.wagonTypes) { - await queryRunner.query( - ` - INSERT INTO freight.wagon_types (code, name, capacity_tons, length_meters, tare_weight_tons, is_active) - VALUES ($1, $2, $3, $4, $5, true) - ON CONFLICT (code) DO UPDATE SET - name = EXCLUDED.name, - capacity_tons = EXCLUDED.capacity_tons, - length_meters = EXCLUDED.length_meters, - tare_weight_tons = EXCLUDED.tare_weight_tons; - `, - [wt.code, wt.name, wt.capacityTons, wt.lengthMeters, wt.tareWeightTons], - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DELETE FROM freight.wagon_types WHERE code = ANY($1);`, - [this.wagonTypes.map((wt) => wt.code)], - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2070000000000-MakeWagonTypeTareWeightRequired.ts b/apps/edr-freight-api/src/migrations/2070000000000-MakeWagonTypeTareWeightRequired.ts deleted file mode 100644 index ff85f3f24..000000000 --- a/apps/edr-freight-api/src/migrations/2070000000000-MakeWagonTypeTareWeightRequired.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Tare weight becomes mandatory on a wagon type. - * - * The locomotive's pull limit is a GROSS limit — it drags the wagon as well as - * the cargo — so capacity math cannot run without a tare. A NULL tare silently - * read as zero and let trains overbook by the tare fraction (~27% on a PW2 - * consist), so the column is now NOT NULL. - * - * Any row still missing a tare predates 2060000000000-SeedRailWagonTypes (which - * upserts the ten real EDR types). Backfill those by code first, and give any - * remaining custom/demo type the NW5 flat-wagon tare rather than fail the - * migration — a wrong-but-plausible tare is recoverable in the admin UI; a - * blocked deploy is not. - */ -export class MakeWagonTypeTareWeightRequired2070000000000 implements MigrationInterface { - private readonly tareByCode: Array<[string, number]> = [ - ['NW7', 37.1], - ['NW5', 22.4], - ['PW2', 25.2], - ['GW2', 23], - ['CW4', 24.8], - ['CW3', 23.4], - ['KW2', 25.2], - ['KW3', 24], - ['NW6', 25.3], - ['BW1', 32.1], - ]; - - /** NW5 flat wagon — the commonest type in the fleet (550 of 1100). */ - private readonly fallbackTareTons = 22.4; - - public async up(queryRunner: QueryRunner): Promise { - for (const [code, tareWeightTons] of this.tareByCode) { - await queryRunner.query( - `UPDATE freight.wagon_types - SET tare_weight_tons = $2 - WHERE code = $1 AND tare_weight_tons IS NULL;`, - [code, tareWeightTons], - ); - } - - await queryRunner.query( - `UPDATE freight.wagon_types - SET tare_weight_tons = $1 - WHERE tare_weight_tons IS NULL;`, - [this.fallbackTareTons], - ); - - await queryRunner.query( - `ALTER TABLE freight.wagon_types - ALTER COLUMN tare_weight_tons SET NOT NULL;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.wagon_types - ALTER COLUMN tare_weight_tons DROP NOT NULL;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2080000000000-DropWagonSpecColumns.ts b/apps/edr-freight-api/src/migrations/2080000000000-DropWagonSpecColumns.ts deleted file mode 100644 index 0fe3569f4..000000000 --- a/apps/edr-freight-api/src/migrations/2080000000000-DropWagonSpecColumns.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Wagon spec belongs to the wagon TYPE, not to each physical wagon. - * - * `wagons.tare_weight` and `wagons.max_payload_weight` duplicated - * `wagon_types.tare_weight_tons` / `wagon_types.capacity_tons` on all 1100 rows, - * with nothing keeping them in step. They had drifted completely: every wagon - * disagreed with its type's tare (seeded ~20T against a real 22.4T NW5), and a - * third disagreed on payload (NW5 wagons claiming 22T–70T against a flat 70T). - * None of those numbers came from the railway. - * - * Nothing reads them for capacity — that math resolves tare and capacity through - * `wagon_type_id` — so dropping them removes a source of fiction rather than a - * source of truth. `wagon_type_id` is NOT NULL with no orphans, so the type is - * always reachable. - * - * A wagon re-tared after repair would need a nullable override column on - * `wagons` falling back to the type; deliberately not added, since no such - * per-wagon value exists today. - */ -export class DropWagonSpecColumns2080000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagons - DROP COLUMN IF EXISTS tare_weight, - DROP COLUMN IF EXISTS max_payload_weight; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - // Re-add nullable, backfill from the owning type, then restore NOT NULL. - // The pre-drop values were drifted seed data and are not recoverable — the - // type's spec is what they should always have held. - await queryRunner.query(` - ALTER TABLE freight.wagons - ADD COLUMN IF NOT EXISTS tare_weight NUMERIC(10, 2), - ADD COLUMN IF NOT EXISTS max_payload_weight NUMERIC(10, 2); - `); - await queryRunner.query(` - UPDATE freight.wagons w - SET tare_weight = t.tare_weight_tons, - max_payload_weight = t.capacity_tons - FROM freight.wagon_types t - WHERE t.id = w.wagon_type_id; - `); - await queryRunner.query(` - ALTER TABLE freight.wagons - ALTER COLUMN tare_weight SET NOT NULL, - ALTER COLUMN max_payload_weight SET NOT NULL; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2090000000000-CreateContractTemplates.ts b/apps/edr-freight-api/src/migrations/2090000000000-CreateContractTemplates.ts deleted file mode 100644 index a553319cc..000000000 --- a/apps/edr-freight-api/src/migrations/2090000000000-CreateContractTemplates.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -import { CONTRACT_TEMPLATE_DEFAULTS } from '../seed/data/contract-template-defaults'; - -/** - * Creates freight.contract_templates — the six editable contract document - * templates (direction × freight type) whose dynamic articles drive the - * generated contract PDF — and seeds them from the EDR reference contract - * documents. Seeding is idempotent (ON CONFLICT (code) DO NOTHING) so admin - * edits are never overwritten by redeploys. - */ -export class CreateContractTemplates2090000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_templates ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - code VARCHAR(40) NOT NULL, - name VARCHAR(200) NOT NULL, - description TEXT, - document_title VARCHAR(300) NOT NULL, - whereas_clauses JSONB NOT NULL DEFAULT '[]', - articles JSONB NOT NULL DEFAULT '[]', - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - deleted_at TIMESTAMPTZ, - CONSTRAINT uq_contract_templates_code UNIQUE (code) - ); - `); - - for (const seed of CONTRACT_TEMPLATE_DEFAULTS) { - const articles = seed.articles.map((article, index) => ({ - ...article, - order: index + 1, - })); - await queryRunner.query( - ` - INSERT INTO freight.contract_templates - (code, name, description, document_title, whereas_clauses, articles) - VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb) - ON CONFLICT (code) DO NOTHING; - `, - [ - seed.code, - seed.name, - seed.description, - seed.documentTitle, - JSON.stringify(seed.whereasClauses), - JSON.stringify(articles), - ], - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_templates;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2090000000000-RepairGrnNumberColumn.ts b/apps/edr-freight-api/src/migrations/2090000000000-RepairGrnNumberColumn.ts deleted file mode 100644 index aa2f998a2..000000000 --- a/apps/edr-freight-api/src/migrations/2090000000000-RepairGrnNumberColumn.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Repairs `freight.warehouse_inventory.grn_number`. - * - * AddGrnNumberToWarehouseInventory1828000000000 is recorded in `migrations` but - * the column is absent on at least one environment - it was added, then dropped - * out-of-band (a stray `synchronize: true`, same class of damage that - * RepairSynchronizeDrift1870000000000 already had to undo). Because TypeORM has - * the original recorded, it will never re-run it. - * - * Without the column, everything that reads or writes a GRN fails with - * `column ... grn_number does not exist`: - * - bulkReceive() -> INSERT names grn_number (receive to warehouse) - * - importQueueByStatuses() -> Unloaded + Dispatch queues - * - exportInventoryByStatus() -> Received / Ready-To-Load / Loaded tabs - * - grnDocument() -> GRN PDF - * - * Idempotent: a no-op on environments where the column survived. - */ -export class RepairGrnNumberColumn2090000000000 implements MigrationInterface { - name = 'RepairGrnNumberColumn2090000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.warehouse_inventory - ADD COLUMN IF NOT EXISTS grn_number VARCHAR(100) NULL - `); - - // Recover the GRN for rows received before the column existed: it was also - // written into the receive note as "GRN Number: ". - await queryRunner.query(` - UPDATE freight.warehouse_inventory - SET grn_number = substring(notes FROM 'GRN Number: ([^\\n\\r]+)') - WHERE grn_number IS NULL - AND notes IS NOT NULL - AND notes ~ 'GRN Number: ' - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_inventory_grn_number - ON freight.warehouse_inventory(grn_number) - WHERE grn_number IS NOT NULL - `); - } - - /** - * Deliberately a no-op. Dropping the column is what broke these environments - * in the first place, and the original 1828 migration already owns its own - * down(). Reverting this repair must not re-introduce the outage. - */ - public async down(): Promise { - // intentionally empty - } -} diff --git a/apps/edr-freight-api/src/migrations/2100000000000-CompanyProfileDefaultPending.ts b/apps/edr-freight-api/src/migrations/2100000000000-CompanyProfileDefaultPending.ts deleted file mode 100644 index aafa9715f..000000000 --- a/apps/edr-freight-api/src/migrations/2100000000000-CompanyProfileDefaultPending.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * `company_profiles.status` defaulted to 'active', so any insert that omitted - * the column produced an operational role that was approved without ever being - * reviewed. Every live write path already passes 'pending' explicitly; this - * closes the hole at the schema level. - * - * Deliberately no data backfill. A role approved through setCompanyProfileStatus - * always stamps `reviewed_at`, so `status = 'active' AND reviewed_at IS NULL` - * flags a role that skipped review — but it also matches rows approved before - * `reviewed_at` existed (migration 2000000000001). Auditing that set is a - * judgement call about real customers, not something to automate here. - */ -export class CompanyProfileDefaultPending2100000000000 - implements MigrationInterface -{ - name = 'CompanyProfileDefaultPending2100000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "freight"."company_profiles" ALTER COLUMN "status" SET DEFAULT 'pending'`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "freight"."company_profiles" ALTER COLUMN "status" SET DEFAULT 'active'`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2100000000000-WarehouseLoadingTrainAssociation.ts b/apps/edr-freight-api/src/migrations/2100000000000-WarehouseLoadingTrainAssociation.ts deleted file mode 100644 index 26afdf82a..000000000 --- a/apps/edr-freight-api/src/migrations/2100000000000-WarehouseLoadingTrainAssociation.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Auto-load onto a selected train: a warehouse_loadings row now records WHICH - * train the item was loaded onto (train_schedule_id), and wagon_id becomes - * nullable because a schedule-level load may not resolve to a single wagon. - */ -export class WarehouseLoadingTrainAssociation2100000000000 implements MigrationInterface { - name = 'WarehouseLoadingTrainAssociation2100000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.warehouse_loadings - ADD COLUMN IF NOT EXISTS train_schedule_id UUID NULL - `); - await queryRunner.query(` - ALTER TABLE freight.warehouse_loadings - ALTER COLUMN wagon_id DROP NOT NULL - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_warehouse_loadings_train_schedule - ON freight.warehouse_loadings(train_schedule_id) - WHERE train_schedule_id IS NOT NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_warehouse_loadings_train_schedule`); - await queryRunner.query(` - ALTER TABLE freight.warehouse_loadings DROP COLUMN IF EXISTS train_schedule_id - `); - // wagon_id stays nullable on revert: restoring NOT NULL would fail on rows - // recorded without a wagon and re-introduce the outage this fixes. - } -} diff --git a/apps/edr-freight-api/src/migrations/2110000000000-AddBookingIsSplit.ts b/apps/edr-freight-api/src/migrations/2110000000000-AddBookingIsSplit.ts deleted file mode 100644 index 8e5ded189..000000000 --- a/apps/edr-freight-api/src/migrations/2110000000000-AddBookingIsSplit.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Partial-batch splits no longer promote a ONE_TIME contract to GENERAL. - * Instead the reduced booking is flagged is_split, and the booking gate lets - * the customer book exactly the remainder under the still-ONE_TIME contract. - */ -export class AddBookingIsSplit2110000000000 implements MigrationInterface { - name = 'AddBookingIsSplit2110000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS is_split BOOLEAN NOT NULL DEFAULT FALSE - `); - // Quantities the booking carried before the split — the remainder ledger - // for ONE_TIME contracts, which have no quantity cap to derive it from. - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS pre_split_quantities JSONB NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings DROP COLUMN IF EXISTS pre_split_quantities - `); - await queryRunner.query(` - ALTER TABLE freight.bookings DROP COLUMN IF EXISTS is_split - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2110000000000-RepairVehicleAvailabilityColumn.ts b/apps/edr-freight-api/src/migrations/2110000000000-RepairVehicleAvailabilityColumn.ts deleted file mode 100644 index c68a6c720..000000000 --- a/apps/edr-freight-api/src/migrations/2110000000000-RepairVehicleAvailabilityColumn.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Repair migration. `SeparateVehicleAvailability1890000000000` is recorded in - * public.migrations but the `availability` column is absent on some databases - * (recorded-but-not-applied drift). Because the original is already recorded, - * TypeORM will not re-run it, so `vehiclesService.findAll` (a query builder that - * selects every entity column) 500s with `column "availability" does not exist`. - * - * This re-adds the column idempotently and backfills. Safe to run everywhere: - * `IF NOT EXISTS` makes it a no-op where the column already exists. - */ -export class RepairVehicleAvailabilityColumn2110000000000 - implements MigrationInterface -{ - name = "RepairVehicleAvailabilityColumn2110000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.vehicles - ADD COLUMN IF NOT EXISTS availability varchar DEFAULT 'FREE' - `); - await queryRunner.query(` - UPDATE freight.vehicles SET availability = 'FREE' WHERE availability IS NULL - `); - } - - public async down(): Promise { - // No-op: dropping a column other code now depends on would reintroduce the - // drift. The original SeparateVehicleAvailability migration owns the column. - } -} diff --git a/apps/edr-freight-api/src/migrations/2120000000000-AddLastMileProofOfDelivery.ts b/apps/edr-freight-api/src/migrations/2120000000000-AddLastMileProofOfDelivery.ts deleted file mode 100644 index ed5054e87..000000000 --- a/apps/edr-freight-api/src/migrations/2120000000000-AddLastMileProofOfDelivery.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Proof of delivery for EDR last-mile: recipient name, a captured signature - * (stored as a file), delivery photos (file ids), notes, and the capture time. - * Recorded when the driver completes the delivery. - */ -export class AddLastMileProofOfDelivery2120000000000 - implements MigrationInterface -{ - name = "AddLastMileProofOfDelivery2120000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.last_mile - ADD COLUMN IF NOT EXISTS pod_recipient_name varchar(160), - ADD COLUMN IF NOT EXISTS pod_signature_file_id uuid, - ADD COLUMN IF NOT EXISTS pod_photo_file_ids text[] NOT NULL DEFAULT '{}', - ADD COLUMN IF NOT EXISTS pod_notes text, - ADD COLUMN IF NOT EXISTS pod_captured_at timestamptz - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.last_mile - DROP COLUMN IF EXISTS pod_recipient_name, - DROP COLUMN IF EXISTS pod_signature_file_id, - DROP COLUMN IF EXISTS pod_photo_file_ids, - DROP COLUMN IF EXISTS pod_notes, - DROP COLUMN IF EXISTS pod_captured_at - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2120000000000-AddScheduleWagonAllocationSnapshot.ts b/apps/edr-freight-api/src/migrations/2120000000000-AddScheduleWagonAllocationSnapshot.ts deleted file mode 100644 index 651c4ba13..000000000 --- a/apps/edr-freight-api/src/migrations/2120000000000-AddScheduleWagonAllocationSnapshot.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add a frozen wagon-allocation snapshot to each train schedule. - * - * Once a schedule leaves the editable DRAFT/SCHEDULED phase (dispatch / arrive / - * cancel), the same physical wagons get released and re-pinned onto later trains. - * The live wagon↔slot joins then no longer describe THIS train's plan, so an - * admin viewing a past schedule saw a mangled or "unavailable" allocation. - * - * This jsonb column stores a one-shot frozen copy of the wagon plan (per-slot - * physical wagon + booking allocations) captured at the transition. Non-editable - * schedules render from the snapshot; DRAFT/SCHEDULED still read live. NULL on - * legacy rows and while editable — the read path falls back to the live joins. - */ -export class AddScheduleWagonAllocationSnapshot2120000000000 - implements MigrationInterface -{ - name = "AddScheduleWagonAllocationSnapshot2120000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS wagon_allocation_snapshot jsonb; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - DROP COLUMN IF EXISTS wagon_allocation_snapshot; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2130000000000-AddHandoverSignerName.ts b/apps/edr-freight-api/src/migrations/2130000000000-AddHandoverSignerName.ts deleted file mode 100644 index 4e4c5f5fc..000000000 --- a/apps/edr-freight-api/src/migrations/2130000000000-AddHandoverSignerName.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * The person who signs off a handover must record their full name (a signature - * is optional, especially for self-haul). Stored per handover record. - */ -export class AddHandoverSignerName2130000000000 implements MigrationInterface { - name = "AddHandoverSignerName2130000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_handovers - ADD COLUMN IF NOT EXISTS signer_name varchar(160) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_handovers - DROP COLUMN IF EXISTS signer_name - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2140000000000-CreateAccrualAcks.ts b/apps/edr-freight-api/src/migrations/2140000000000-CreateAccrualAcks.ts deleted file mode 100644 index 5e13a6e3d..000000000 --- a/apps/edr-freight-api/src/migrations/2140000000000-CreateAccrualAcks.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Accrual alert acknowledgements: ops can mark an in-warehouse item's fee - * accrual as reviewed (optionally snoozed until a date) so it stops nudging and - * drops down the accrual dashboard. One row per inventory item. - */ -export class CreateAccrualAcks2140000000000 implements MigrationInterface { - name = "CreateAccrualAcks2140000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_accrual_acks ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - inventory_id uuid NOT NULL UNIQUE, - acknowledged_by uuid, - acknowledged_at timestamptz NOT NULL DEFAULT now(), - snooze_until timestamptz, - note text, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now() - ) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_accrual_acks`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2150000000000-TrainBuilder.ts b/apps/edr-freight-api/src/migrations/2150000000000-TrainBuilder.ts deleted file mode 100644 index 24fa50feb..000000000 --- a/apps/edr-freight-api/src/migrations/2150000000000-TrainBuilder.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Train Builder: a `Train` becomes a first-class buildable consist — a coded - * train (e.g. 81001) assembled in one yard from 2+ locomotives and ordered - * wagons, then reused by scheduling ("schedule the train" instead of picking - * locomotives per departure). - * - * - `freight.train_locomotives` — link table train ⇄ locomotive with an order - * index (mirrors `train_set_locomotives`). - * - `trains.current_yard_id` — yard the train sits in; wagons/locomotives may - * only be attached from this yard. - * - `train_sets.train_id` — which built train an operational set was formed - * from, so schedules can surface the train code and the lifecycle can sync - * the train's status/yard on dispatch/arrival/cancel. - * - * NOTE: the shared dev DB has no applied migration history, so this is also - * hand-applied there. IF NOT EXISTS keeps that idempotent. - */ -export class TrainBuilder2150000000000 implements MigrationInterface { - name = 'TrainBuilder2150000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.train_locomotives ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - train_id uuid NOT NULL, - locomotive_id uuid NOT NULL, - sequence_no int NOT NULL DEFAULT 0, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT "PK_train_locomotives" PRIMARY KEY (id), - CONSTRAINT "FK_train_locomotives_train" FOREIGN KEY (train_id) - REFERENCES freight.trains (id) ON DELETE CASCADE, - CONSTRAINT "FK_train_locomotives_locomotive" FOREIGN KEY (locomotive_id) - REFERENCES freight.locomotives (id) - ); - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_train_locomotives_train_loco" - ON freight.train_locomotives (train_id, locomotive_id); - `); - - await queryRunner.query(` - ALTER TABLE freight.trains - ADD COLUMN IF NOT EXISTS current_yard_id uuid; - `); - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'FK_trains_current_yard' - ) THEN - ALTER TABLE freight.trains - ADD CONSTRAINT "FK_trains_current_yard" FOREIGN KEY (current_yard_id) - REFERENCES freight.yards (id) ON DELETE SET NULL; - END IF; - END $$; - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_trains_current_yard_id" - ON freight.trains (current_yard_id); - `); - - await queryRunner.query(` - ALTER TABLE freight.train_sets - ADD COLUMN IF NOT EXISTS train_id uuid; - `); - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'FK_train_sets_train' - ) THEN - ALTER TABLE freight.train_sets - ADD CONSTRAINT "FK_train_sets_train" FOREIGN KEY (train_id) - REFERENCES freight.trains (id) ON DELETE SET NULL; - END IF; - END $$; - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_train_sets_train_id" - ON freight.train_sets (train_id); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_train_sets_train_id";`); - await queryRunner.query(` - ALTER TABLE freight.train_sets - DROP CONSTRAINT IF EXISTS "FK_train_sets_train", - DROP COLUMN IF EXISTS train_id; - `); - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_trains_current_yard_id";`); - await queryRunner.query(` - ALTER TABLE freight.trains - DROP CONSTRAINT IF EXISTS "FK_trains_current_yard", - DROP COLUMN IF EXISTS current_yard_id; - `); - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_train_locomotives_train_loco";`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.train_locomotives;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2160000000000-MultiWagonTypePerCargoAndContainer.ts b/apps/edr-freight-api/src/migrations/2160000000000-MultiWagonTypePerCargoAndContainer.ts deleted file mode 100644 index 0fc391904..000000000 --- a/apps/edr-freight-api/src/migrations/2160000000000-MultiWagonTypePerCargoAndContainer.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * A container type / cargo type can now be carried by SEVERAL wagon types - * (e.g. a 20ft container rides NX70 or NW5). Replaces the single - * `wagon_type_id` FK on both tables with proper link tables; train scheduling - * resolves the wagon type from the list, picking whichever type the schedule's - * built train (or the yard) actually has. - * - * Backfills one link row from each existing `wagon_type_id`, then drops the - * old column — the single-FK field is removed from the API and UI entirely. - * - * NOTE: the shared dev DB has no applied migration history, so this is also - * hand-applied there. IF NOT EXISTS keeps that idempotent. - */ -export class MultiWagonTypePerCargoAndContainer2160000000000 implements MigrationInterface { - name = 'MultiWagonTypePerCargoAndContainer2160000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.container_type_wagon_types ( - container_type_id uuid NOT NULL, - wagon_type_id uuid NOT NULL, - CONSTRAINT "PK_container_type_wagon_types" PRIMARY KEY (container_type_id, wagon_type_id), - CONSTRAINT "FK_ctwt_container_type" FOREIGN KEY (container_type_id) - REFERENCES freight.container_types (id) ON DELETE CASCADE, - CONSTRAINT "FK_ctwt_wagon_type" FOREIGN KEY (wagon_type_id) - REFERENCES freight.wagon_types (id) ON DELETE RESTRICT - ); - `); - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.cargo_type_wagon_types ( - cargo_type_id uuid NOT NULL, - wagon_type_id uuid NOT NULL, - CONSTRAINT "PK_cargo_type_wagon_types" PRIMARY KEY (cargo_type_id, wagon_type_id), - CONSTRAINT "FK_cgwt_cargo_type" FOREIGN KEY (cargo_type_id) - REFERENCES freight.cargo_types (id) ON DELETE CASCADE, - CONSTRAINT "FK_cgwt_wagon_type" FOREIGN KEY (wagon_type_id) - REFERENCES freight.wagon_types (id) ON DELETE RESTRICT - ); - `); - - // Backfill from the old single FK (column may already be gone on re-run). - await queryRunner.query(` - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'freight' AND table_name = 'container_types' - AND column_name = 'wagon_type_id' - ) THEN - INSERT INTO freight.container_type_wagon_types (container_type_id, wagon_type_id) - SELECT ct.id, ct.wagon_type_id - FROM freight.container_types ct - WHERE ct.wagon_type_id IS NOT NULL - ON CONFLICT DO NOTHING; - END IF; - END $$; - `); - await queryRunner.query(` - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'freight' AND table_name = 'cargo_types' - AND column_name = 'wagon_type_id' - ) THEN - INSERT INTO freight.cargo_type_wagon_types (cargo_type_id, wagon_type_id) - SELECT cg.id, cg.wagon_type_id - FROM freight.cargo_types cg - WHERE cg.wagon_type_id IS NOT NULL - ON CONFLICT DO NOTHING; - END IF; - END $$; - `); - - // Old single-FK column is fully retired (API + UI now use the lists). - await queryRunner.query(` - ALTER TABLE freight.container_types DROP COLUMN IF EXISTS wagon_type_id; - `); - await queryRunner.query(` - ALTER TABLE freight.cargo_types DROP COLUMN IF EXISTS wagon_type_id; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.container_types ADD COLUMN IF NOT EXISTS wagon_type_id uuid - REFERENCES freight.wagon_types (id) ON DELETE RESTRICT; - `); - await queryRunner.query(` - ALTER TABLE freight.cargo_types ADD COLUMN IF NOT EXISTS wagon_type_id uuid - REFERENCES freight.wagon_types (id) ON DELETE RESTRICT; - `); - // Restore the first linked wagon type per row, then drop the link tables. - await queryRunner.query(` - UPDATE freight.container_types ct - SET wagon_type_id = link.wagon_type_id - FROM ( - SELECT DISTINCT ON (container_type_id) container_type_id, wagon_type_id - FROM freight.container_type_wagon_types - ORDER BY container_type_id, wagon_type_id - ) link - WHERE link.container_type_id = ct.id; - `); - await queryRunner.query(` - UPDATE freight.cargo_types cg - SET wagon_type_id = link.wagon_type_id - FROM ( - SELECT DISTINCT ON (cargo_type_id) cargo_type_id, wagon_type_id - FROM freight.cargo_type_wagon_types - ORDER BY cargo_type_id, wagon_type_id - ) link - WHERE link.cargo_type_id = cg.id; - `); - await queryRunner.query(`DROP TABLE IF EXISTS freight.container_type_wagon_types;`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.cargo_type_wagon_types;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2170000000000-CreateWagonTransferRequests.ts b/apps/edr-freight-api/src/migrations/2170000000000-CreateWagonTransferRequests.ts deleted file mode 100644 index 2b73eaa0c..000000000 --- a/apps/edr-freight-api/src/migrations/2170000000000-CreateWagonTransferRequests.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Two-person wagon-transfer request queue. A requester records a count-only - * request (N wagons of a type, from yard → to yard); OCC staff later pick the - * physical wagons and execute the move. Replaces the single-step instant - * bulk-transfer as the customer-facing yard-to-yard relocation path. - */ -export class CreateWagonTransferRequests2170000000000 - implements MigrationInterface -{ - name = 'CreateWagonTransferRequests2170000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.wagon_transfer_requests ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - from_yard_id uuid NOT NULL, - to_yard_id uuid NOT NULL, - wagon_type_id uuid NOT NULL, - quantity integer NOT NULL, - status varchar(20) NOT NULL DEFAULT 'PENDING', - requested_by_user_id uuid NULL, - fulfilled_by_user_id uuid NULL, - fulfilled_at timestamptz NULL, - note text NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL, - CONSTRAINT pk_wagon_transfer_requests PRIMARY KEY (id), - CONSTRAINT fk_wtr_from_yard FOREIGN KEY (from_yard_id) REFERENCES freight.yards (id), - CONSTRAINT fk_wtr_to_yard FOREIGN KEY (to_yard_id) REFERENCES freight.yards (id), - CONSTRAINT fk_wtr_wagon_type FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types (id), - CONSTRAINT chk_wtr_quantity CHECK (quantity > 0) - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_wtr_status_from_yard - ON freight.wagon_transfer_requests (status, from_yard_id) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight.idx_wtr_status_from_yard`, - ); - await queryRunner.query( - `DROP TABLE IF EXISTS freight.wagon_transfer_requests`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2170000000000-ScheduleWagonAdjustmentLogs.ts b/apps/edr-freight-api/src/migrations/2170000000000-ScheduleWagonAdjustmentLogs.ts deleted file mode 100644 index 7fecfccbb..000000000 --- a/apps/edr-freight-api/src/migrations/2170000000000-ScheduleWagonAdjustmentLogs.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Consist adjustments from a schedule: staff can trim free wagons off a built - * train when their tare pushes gross weight over the locomotives' pull limit - * (incl. overage tolerance), or couple extra yard wagons on while weight and - * length headroom remain. Each add/remove is logged here so the schedule keeps - * an auditable history; the built train itself is updated in place. - * - * Plain columns (no FKs) so the history survives wagon/train deletion. - * - * NOTE: the shared dev DB has no applied migration history, so this is also - * hand-applied there. IF NOT EXISTS keeps that idempotent. - */ -export class ScheduleWagonAdjustmentLogs2170000000000 implements MigrationInterface { - name = 'ScheduleWagonAdjustmentLogs2170000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.schedule_wagon_adjustment_logs ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - train_schedule_id uuid NOT NULL, - train_id uuid NOT NULL, - action varchar(10) NOT NULL, - wagon_id uuid NOT NULL, - wagon_number varchar(50) NOT NULL, - adjusted_by_user_id uuid, - occurred_at timestamptz NOT NULL DEFAULT now(), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT "PK_schedule_wagon_adjustment_logs" PRIMARY KEY (id) - ); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_swal_train_schedule_id" - ON freight.schedule_wagon_adjustment_logs (train_schedule_id); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_swal_train_id" - ON freight.schedule_wagon_adjustment_logs (train_id); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_swal_train_id";`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_swal_train_schedule_id";`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.schedule_wagon_adjustment_logs;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2180000000000-LinkWagonMovementToTransferRequest.ts b/apps/edr-freight-api/src/migrations/2180000000000-LinkWagonMovementToTransferRequest.ts deleted file mode 100644 index 3f10fa874..000000000 --- a/apps/edr-freight-api/src/migrations/2180000000000-LinkWagonMovementToTransferRequest.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Link each physical wagon move back to the transfer request that drove it, so - * the history can show "Request S→K, 3× NX70 → wagons W101, W102, W103". - * Nullable — legacy moves and non-request manual corrections carry no request. - * Also indexes `moved_by_user_id` for the per-user history queries. - */ -export class LinkWagonMovementToTransferRequest2180000000000 - implements MigrationInterface -{ - name = 'LinkWagonMovementToTransferRequest2180000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagon_movements - ADD COLUMN IF NOT EXISTS transfer_request_id uuid NULL - `); - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'fk_wm_transfer_request' - ) THEN - ALTER TABLE freight.wagon_movements - ADD CONSTRAINT fk_wm_transfer_request - FOREIGN KEY (transfer_request_id) - REFERENCES freight.wagon_transfer_requests (id) ON DELETE SET NULL; - END IF; - END $$; - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_wm_transfer_request - ON freight.wagon_movements (transfer_request_id) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_wm_moved_by - ON freight.wagon_movements (moved_by_user_id) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_wm_moved_by`); - await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_wm_transfer_request`); - await queryRunner.query(` - ALTER TABLE freight.wagon_movements - DROP CONSTRAINT IF EXISTS fk_wm_transfer_request - `); - await queryRunner.query(` - ALTER TABLE freight.wagon_movements - DROP COLUMN IF EXISTS transfer_request_id - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2190000000000-DropReopenDelayMinutes.ts b/apps/edr-freight-api/src/migrations/2190000000000-DropReopenDelayMinutes.ts deleted file mode 100644 index 5a667e2e4..000000000 --- a/apps/edr-freight-api/src/migrations/2190000000000-DropReopenDelayMinutes.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Drop the unused reopen-delay knob from the global rules. - * - * The window engine never honoured `reopen_delay_minutes`: a not-yet-full train - * reopens as soon as its payment phase settles, so the real gap between a cycle - * closing and reopening is doc review + payment — nothing else. The per-schedule - * `rule_reopen_delay_minutes` snapshot stays: it freezes that derived gap at - * creation so the batch board keeps projecting the cycles the customer was shown. - */ -export class DropReopenDelayMinutes2190000000000 implements MigrationInterface { - name = "DropReopenDelayMinutes2190000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - DROP COLUMN IF EXISTS reopen_delay_minutes; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - ADD COLUMN IF NOT EXISTS reopen_delay_minutes integer NOT NULL DEFAULT 90; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2200000000000-TrainNumberPair.ts b/apps/edr-freight-api/src/migrations/2200000000000-TrainNumberPair.ts deleted file mode 100644 index 65a3d3cda..000000000 --- a/apps/edr-freight-api/src/migrations/2200000000000-TrainNumberPair.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Every built train owns a fixed pair of run numbers, typed at build time: - * an EXPORT number (odd, e.g. 8001) and an IMPORT number (even, e.g. 8002). - * Scheduling copies the route-direction-matched number onto the schedule at - * creation; legacy trains with a null pair keep dispatch-time pool assignment. - * - * NOTE: the shared dev DB has no applied migration history, so this is also - * hand-applied there. IF NOT EXISTS keeps that idempotent. - */ -export class TrainNumberPair2200000000000 implements MigrationInterface { - name = 'TrainNumberPair2200000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.trains - ADD COLUMN IF NOT EXISTS import_train_number varchar(20), - ADD COLUMN IF NOT EXISTS export_train_number varchar(20); - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_trains_import_train_number" - ON freight.trains (import_train_number) - WHERE import_train_number IS NOT NULL; - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_trains_export_train_number" - ON freight.trains (export_train_number) - WHERE export_train_number IS NOT NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_trains_export_train_number";`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_trains_import_train_number";`); - await queryRunner.query(` - ALTER TABLE freight.trains - DROP COLUMN IF EXISTS export_train_number, - DROP COLUMN IF EXISTS import_train_number; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2210000000000-ScheduleScopedWagonPins.ts b/apps/edr-freight-api/src/migrations/2210000000000-ScheduleScopedWagonPins.ts deleted file mode 100644 index 42e8f1dc8..000000000 --- a/apps/edr-freight-api/src/migrations/2210000000000-ScheduleScopedWagonPins.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Schedule-scoped wagon pins. - * - * Wagon occupancy now lives ONLY on each schedule's own train_set_wagons slots - * (the per-schedule snapshot): pinning/releasing a wagon no longer mutates the - * Wagon entity, so the same physical wagon can serve many schedules (the July 17 - * and July 20 runs of one train both use its 50 wagons). The Wagon columns - * `current_train_schedule_id` / `train_set_wagon_id` keep only their physical - * meaning — "out on this DISPATCHED train right now" (stamped at dispatch, - * cleared at arrive/unload/cancel). - * - * This migration erases the legacy pin-time stamps left by the old flow: any - * wagon pointing at a schedule that is not currently DISPATCHED (or that no - * longer exists) gets its pointers cleared, and — when the old flow had parked - * it in ASSIGNED — its status returns to the pool semantics (ASSIGNED only - * while coupled to a built train, otherwise AVAILABLE). - */ -export class ScheduleScopedWagonPins2210000000000 implements MigrationInterface { - name = "ScheduleScopedWagonPins2210000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.wagons w - SET current_train_schedule_id = NULL, - train_set_wagon_id = NULL, - status = CASE - WHEN w.status = 'ASSIGNED' AND w.train_id IS NULL THEN 'AVAILABLE' - ELSE w.status - END - WHERE w.deleted_at IS NULL - AND w.current_train_schedule_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM freight.train_schedules ts - WHERE ts.id = w.current_train_schedule_id - AND ts.deleted_at IS NULL - AND ts.status = 'DISPATCHED' - ); - `); - } - - public async down(_queryRunner: QueryRunner): Promise { - // Pin-time stamps cannot be reconstructed (the data was the bug); the - // slots on train_set_wagons still hold every live pin, so down is a no-op. - } -} diff --git a/apps/edr-freight-api/src/migrations/2220000000000-AddContractDocumentSnapshot.ts b/apps/edr-freight-api/src/migrations/2220000000000-AddContractDocumentSnapshot.ts deleted file mode 100644 index 5a8cdd035..000000000 --- a/apps/edr-freight-api/src/migrations/2220000000000-AddContractDocumentSnapshot.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Adds freight.contracts.document_snapshot — a per-contract frozen copy of the - * contract-document template (articles + WHEREAS recitals) captured at staff - * accept. Staff can edit these articles for a single contract before generating - * its PDF; the edit never touches the shared six freight.contract_templates - * rows. Null on existing contracts → the PDF keeps rendering from the live - * template, so this is backward compatible. - */ -export class AddContractDocumentSnapshot2220000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.contracts - ADD COLUMN IF NOT EXISTS document_snapshot JSONB; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.contracts - DROP COLUMN IF EXISTS document_snapshot; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2230000000000-RenameWagonStatusRetiredToDetained.ts b/apps/edr-freight-api/src/migrations/2230000000000-RenameWagonStatusRetiredToDetained.ts deleted file mode 100644 index 2fe7c726d..000000000 --- a/apps/edr-freight-api/src/migrations/2230000000000-RenameWagonStatusRetiredToDetained.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Wagon status RETIRED is renamed DETAINED (wagons pulled from circulation). - * The column is a plain varchar, so this is a data-only rename. Vehicles keep - * their own RETIRED status — only freight.wagons rows are touched. - */ -export class RenameWagonStatusRetiredToDetained2230000000000 - implements MigrationInterface -{ - name = 'RenameWagonStatusRetiredToDetained2230000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.wagons SET status = 'DETAINED' WHERE status = 'RETIRED' - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.wagons SET status = 'RETIRED' WHERE status = 'DETAINED' - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2240000000000-AddTransferRequestReason.ts b/apps/edr-freight-api/src/migrations/2240000000000-AddTransferRequestReason.ts deleted file mode 100644 index 76a59b0f7..000000000 --- a/apps/edr-freight-api/src/migrations/2240000000000-AddTransferRequestReason.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Every new wagon-transfer request must state WHY the wagons are needed; the - * reason is shown on the OCC request queue. Nullable in the DB — legacy rows - * predate the requirement; the DTO enforces it for new requests. - */ -export class AddTransferRequestReason2240000000000 implements MigrationInterface { - name = 'AddTransferRequestReason2240000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagon_transfer_requests - ADD COLUMN IF NOT EXISTS reason text NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagon_transfer_requests - DROP COLUMN IF EXISTS reason - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2250000000000-CreatePriorityRuleChangeRequests.ts b/apps/edr-freight-api/src/migrations/2250000000000-CreatePriorityRuleChangeRequests.ts deleted file mode 100644 index f93fc7c95..000000000 --- a/apps/edr-freight-api/src/migrations/2250000000000-CreatePriorityRuleChangeRequests.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Approval workflow for priority-rule changes: every create/update/delete of a - * priority config is filed here as a PENDING change request; an approver - * applies or rejects it. `payload` carries the proposed field values (null for - * DELETE), `priority_config_id` the target row (null for CREATE). - */ -export class CreatePriorityRuleChangeRequests2250000000000 - implements MigrationInterface -{ - name = 'CreatePriorityRuleChangeRequests2250000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.priority_rule_change_requests ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - action varchar(10) NOT NULL, - priority_config_id uuid NULL REFERENCES freight.priority_configs (id), - payload jsonb NULL, - status varchar(10) NOT NULL DEFAULT 'PENDING', - requested_by_user_id uuid NULL, - decided_by_user_id uuid NULL, - decided_at timestamptz NULL, - decision_note text NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_prcr_status - ON freight.priority_rule_change_requests (status) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP TABLE IF EXISTS freight.priority_rule_change_requests`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2260000000000-AddClearanceFeePayment.ts b/apps/edr-freight-api/src/migrations/2260000000000-AddClearanceFeePayment.ts deleted file mode 100644 index c0dc2c818..000000000 --- a/apps/edr-freight-api/src/migrations/2260000000000-AddClearanceFeePayment.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Prepaid customs clearance service fee (Path B): - * - contract_rate_snapshots.is_clearance — flags the frozen CUSTOMS_CLEARANCE - * fee line so it is billed via its own clearance invoice and excluded from - * shipment booking totals; - * - contracts.clearance_fee_paid_at — when the ONE_TIME contract-level fee - * settled (gate: AWAITING_CLEARANCE_PAYMENT → AWAITING_CLEARANCE_DOCUMENTS); - * - bookings.clearance_fee_paid_at — when a GENERAL shipment-request instance's - * fee settled (gate: AWAITING_CLEARANCE_PAYMENT → AWAITING_DOCUMENTS). - * All nullable/defaulted — existing rows are untouched and keep today's flow. - */ -export class AddClearanceFeePayment2260000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.contract_rate_snapshots - ADD COLUMN IF NOT EXISTS is_clearance BOOLEAN NOT NULL DEFAULT FALSE; - `); - await queryRunner.query(` - ALTER TABLE freight.contracts - ADD COLUMN IF NOT EXISTS clearance_fee_paid_at TIMESTAMPTZ; - `); - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS clearance_fee_paid_at TIMESTAMPTZ; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS clearance_fee_paid_at; - `); - await queryRunner.query(` - ALTER TABLE freight.contracts - DROP COLUMN IF EXISTS clearance_fee_paid_at; - `); - await queryRunner.query(` - ALTER TABLE freight.contract_rate_snapshots - DROP COLUMN IF EXISTS is_clearance; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2260000000000-AddLastMileTruckArrivalDeparture.ts b/apps/edr-freight-api/src/migrations/2260000000000-AddLastMileTruckArrivalDeparture.ts deleted file mode 100644 index 052d46340..000000000 --- a/apps/edr-freight-api/src/migrations/2260000000000-AddLastMileTruckArrivalDeparture.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * EDR last-mile is multi-truck: a booking can be served by as many trucks as it - * has containers (bulk hauls until the tonnage is drawn down). Arrival/delivery - * were stamped once per `last_mile` record, so every truck shared one timestamp. - * These per-vehicle columns give each EDR truck its own arrival, leaving and - * weighed load — the same granularity self-haul trucks already have. - * - * Weights are TONNES (matching bookings.cargo_total_weight_vgm and the exit - * weighing UI). Named `*_tons` deliberately: the older - * customer_truck_assignments.gross_weight_kg is named kg but stores tonnes. - * All nullable — legacy rows predate per-truck tracking. - */ -export class AddLastMileTruckArrivalDeparture2260000000000 implements MigrationInterface { - name = 'AddLastMileTruckArrivalDeparture2260000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.last_mile_vehicle_assignments - ADD COLUMN IF NOT EXISTS arrived_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS departed_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS gross_weight_tons numeric(14, 3) NULL, - ADD COLUMN IF NOT EXISTS net_weight_tons numeric(14, 3) NULL - `); - - // A truck carries 1x40ft OR 2x20ft, so an EDR truck needs MORE than the one - // container the legacy scalar `container_number` can hold. Mirrors the - // self-haul customer_truck_containers child table. The scalar stays in place - // (synced to the first container) for backward compatibility. - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.last_mile_vehicle_containers ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - assignment_id uuid NOT NULL REFERENCES freight.last_mile_vehicle_assignments(id) ON DELETE CASCADE, - last_mile_id uuid NOT NULL, - container_number varchar(32) NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_last_mile_vehicle_containers_assignment" - ON freight.last_mile_vehicle_containers (assignment_id) - `); - // A container rides exactly one truck per delivery. - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_last_mile_vehicle_container" - ON freight.last_mile_vehicle_containers (last_mile_id, container_number) - WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.last_mile_vehicle_containers`); - await queryRunner.query(` - ALTER TABLE freight.last_mile_vehicle_assignments - DROP COLUMN IF EXISTS arrived_at, - DROP COLUMN IF EXISTS departed_at, - DROP COLUMN IF EXISTS gross_weight_tons, - DROP COLUMN IF EXISTS net_weight_tons - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2260000000000-SeedEdrWagonFleetErNumbering.ts b/apps/edr-freight-api/src/migrations/2260000000000-SeedEdrWagonFleetErNumbering.ts deleted file mode 100644 index a651b8f60..000000000 --- a/apps/edr-freight-api/src/migrations/2260000000000-SeedEdrWagonFleetErNumbering.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Re-seed the EDR wagon fleet onto the official ER numbering. - * - * Supersedes SeedWagonsWithYardAssignment1784000000001, which seeded 500 wagons - * on a `-NNNN` scheme and wrote the status as 'Available' — mixed case - * that never matches WagonStatus.Available ('AVAILABLE'), so status filters - * silently returned nothing. This seed uses the enum value. - * - * Every wagon lands unassigned: current_yard_id NULL, status AVAILABLE. Wagon - * specs (capacity/length/tare) stay owned by wagon_types and are not touched — - * the types already exist and only the wagon↔type link is (re)established here. - */ -type FleetRow = { - code: string; - start: number; - end: number; - count: number; -}; - -/** Official fleet: 1100 wagons, ER0001–ER1100, contiguous across 10 types. */ -const FLEET: FleetRow[] = [ - { code: 'PW2', start: 1, end: 220, count: 220 }, - { code: 'CW4', start: 221, end: 330, count: 110 }, - { code: 'CW3', start: 331, end: 350, count: 20 }, - { code: 'KW2', start: 351, end: 370, count: 20 }, - { code: 'KW3', start: 371, end: 390, count: 20 }, - { code: 'NW5', start: 391, end: 940, count: 550 }, - { code: 'BW1', start: 941, end: 950, count: 10 }, - { code: 'GW2', start: 951, end: 1060, count: 110 }, - { code: 'NW6', start: 1061, end: 1080, count: 20 }, - { code: 'NW7', start: 1081, end: 1100, count: 20 }, -]; - -const wagonNumber = (sequence: number) => `ER${String(sequence).padStart(4, '0')}`; - -export class SeedEdrWagonFleetErNumbering2260000000000 implements MigrationInterface { - name = 'SeedEdrWagonFleetErNumbering2260000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // Full replacement: the ER range is the fleet of record, so any wagon - // outside it is stale seed data. Safe to hard-delete — containers and - // train_set_wagons null their link, wagon_movements cascade. - await queryRunner.query(`DELETE FROM freight.wagons;`); - - // Deliberately does NOT create a unique index on wagon_number. It once did, - // to satisfy an ON CONFLICT clause that no longer exists (the DELETE above - // makes collisions impossible). Recreating the plain index here would undo - // WagonNumberPartialUnique2280000000000, which replaces it with a PARTIAL - // unique index so soft-deleted wagons stop reserving their number — this - // seeder is run directly by scripts/seed-edr-wagons.ts, which would - // otherwise resurrect the plain index on an already-migrated database. - - for (const row of FLEET) { - if (row.end - row.start + 1 !== row.count) { - throw new Error(`wagon_range_mismatch:${row.code}`); - } - - const [typeRecord] = await queryRunner.query( - `SELECT id FROM freight.wagon_types WHERE code = $1 AND deleted_at IS NULL LIMIT 1;`, - [row.code], - ); - - if (!typeRecord?.id) { - throw new Error(`wagon_type_missing:${row.code}`); - } - - // generate_series builds the range server-side — one round trip per type - // instead of 1100 individual INSERTs. No ON CONFLICT clause: every wagon - // was deleted above, so a plain INSERT cannot collide, and the clause would - // otherwise hard-require a unique index this table lacks on some envs. - await queryRunner.query( - ` - INSERT INTO freight.wagons ( - wagon_number, - wagon_type_id, - status, - current_yard_id, - train_id, - sequence_number, - notes, - train_set_wagon_id, - current_train_schedule_id - ) - SELECT - 'ER' || LPAD(seq::text, 4, '0'), - $1::uuid, - 'AVAILABLE', - NULL, - NULL, - NULL, - NULL, - NULL, - NULL - FROM generate_series($2::int, $3::int) AS seq; - `, - [typeRecord.id, row.start, row.end], - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DELETE FROM freight.wagons WHERE wagon_number BETWEEN $1 AND $2;`, - [wagonNumber(FLEET[0].start), wagonNumber(FLEET[FLEET.length - 1].end)], - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2270000000000-AddContainerReturnQuantity.ts b/apps/edr-freight-api/src/migrations/2270000000000-AddContainerReturnQuantity.ts deleted file mode 100644 index 4a37b5e98..000000000 --- a/apps/edr-freight-api/src/migrations/2270000000000-AddContainerReturnQuantity.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Adds freight.booking_container.return_quantity — how many units of a - * container line ship with the empty-container-return service (≤ quantity). - * Mirrors hazardous_quantity / reefer_quantity: captured per line at booking - * creation when the contract enables WITH_RETURN (container freight only) and - * drives the booking-level equipment_return flag that fires the WITH_RETURN - * pricing surcharge. - */ -export class AddContainerReturnQuantity2270000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_container - ADD COLUMN IF NOT EXISTS return_quantity SMALLINT NOT NULL DEFAULT 0; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_container - DROP COLUMN IF EXISTS return_quantity; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2270000000000-AddWagonTrainNumbers.ts b/apps/edr-freight-api/src/migrations/2270000000000-AddWagonTrainNumbers.ts deleted file mode 100644 index 7050c1c40..000000000 --- a/apps/edr-freight-api/src/migrations/2270000000000-AddWagonTrainNumbers.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Per-wagon EXPORT/IMPORT run numbers, editable from the wagon form. - * - * Nullable with no default: a wagon is not on a run until an operator says so. - * Mirrors the width of trains.export_train_number / trains.import_train_number - * (varchar 20) so the two stay comparable. - */ -export class AddWagonTrainNumbers2270000000000 implements MigrationInterface { - name = 'AddWagonTrainNumbers2270000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagons - ADD COLUMN IF NOT EXISTS export_train_number varchar(20), - ADD COLUMN IF NOT EXISTS import_train_number varchar(20); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagons - DROP COLUMN IF EXISTS export_train_number, - DROP COLUMN IF EXISTS import_train_number; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2280000000000-SeedWagonRunNumbers.ts b/apps/edr-freight-api/src/migrations/2280000000000-SeedWagonRunNumbers.ts deleted file mode 100644 index dae700883..000000000 --- a/apps/edr-freight-api/src/migrations/2280000000000-SeedWagonRunNumbers.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Assign EDR export/import run numbers to the wagon fleet. - * - * Runs AFTER SeedEdrWagonFleetErNumbering2260000000000, which recreates every - * wagon with NULL run numbers — so this must stay later in timestamp order. - * - * Source data below is the operator-supplied roster, kept verbatim rather than - * pre-resolved so its quirks stay visible: - * - ER0697 is listed twice under run 8101 (deduped here -> 49, not 50). - * - Four wagons are claimed by two runs each. A wagon holds a single run, so - * FIRST-LISTED WINS, which is why four runs land one short of their listed - * count: - * ER0484 8301 over 8401 - * ER0451 8401 over 8701 - * ER0887 8701 over 9001 - * ER0936 8801 over 8901 - * - * Wagons outside this roster (PW2 ER0001-0220 and ER0941-1100) keep NULL runs. - */ - -/** Odd EXPORT run (Ethiopia -> Djibouti) -> the wagons rostered to it. */ -const RUN_WAGONS: Record = { - '8001': [ - 'ER0744', 'ER0734', 'ER0791', 'ER0885', 'ER0410', 'ER0901', - 'ER0692', 'ER0784', 'ER0663', 'ER0547', 'ER0635', 'ER0840', - 'ER0660', 'ER0541', 'ER0850', 'ER0764', 'ER0786', 'ER0694', - 'ER0656', 'ER0432', 'ER0666', 'ER0879', 'ER0724', 'ER0868', - 'ER0835', 'ER0650', 'ER0926', 'ER0915', 'ER0858', 'ER0826', - 'ER0474', 'ER0539', 'ER0419', 'ER0695', 'ER0462', 'ER0825', - 'ER0820', 'ER0790', 'ER0905', 'ER0557', 'ER0712', 'ER0782', - 'ER0816', 'ER0447', 'ER0674', 'ER0424', 'ER0544', 'ER0519', - 'ER0479', 'ER0440', - ], - '8101': [ - 'ER0458', 'ER0600', 'ER0521', 'ER0559', 'ER0846', 'ER0459', - 'ER0863', 'ER0925', 'ER0746', 'ER0821', 'ER0914', 'ER0768', - 'ER0676', 'ER0470', 'ER0697', 'ER0697', 'ER0923', 'ER0937', - 'ER0431', 'ER0412', 'ER0254', 'ER0555', 'ER0527', 'ER0590', - 'ER0480', 'ER0723', 'ER0316', 'ER0800', 'ER0648', 'ER0435', - 'ER0844', 'ER0939', 'ER0747', 'ER0654', 'ER0752', 'ER0633', - 'ER0725', 'ER0567', 'ER0838', 'ER0920', 'ER0843', 'ER0520', - 'ER0646', 'ER0407', 'ER0515', 'ER0760', 'ER0703', 'ER0880', - 'ER0422', 'ER0852', - ], - '8201': [ - 'ER0322', 'ER0314', 'ER0274', 'ER0514', 'ER0505', 'ER0618', - 'ER0812', 'ER0776', 'ER0698', 'ER0662', 'ER0888', 'ER0625', - 'ER0568', 'ER0596', 'ER0918', 'ER0524', 'ER0684', 'ER0231', - 'ER0907', 'ER0445', 'ER0839', 'ER0430', 'ER0799', 'ER0464', - 'ER0491', 'ER0833', 'ER0855', 'ER0571', 'ER0452', 'ER0733', - 'ER0606', 'ER0822', 'ER0845', 'ER0771', 'ER0542', 'ER0588', - 'ER0443', 'ER0585', 'ER0624', 'ER0538', 'ER0642', 'ER0928', - 'ER0411', 'ER0794', 'ER0564', 'ER0906', 'ER0348', 'ER0236', - 'ER0933', 'ER0456', - ], - '8301': [ - 'ER0264', 'ER0691', 'ER0562', 'ER0686', 'ER0881', 'ER0780', - 'ER0400', 'ER0420', 'ER0475', 'ER0425', 'ER0396', 'ER0818', - 'ER0537', 'ER0917', 'ER0421', 'ER0766', 'ER0728', 'ER0485', - 'ER0830', 'ER0804', 'ER0935', 'ER0898', 'ER0577', 'ER0762', - 'ER0558', 'ER0612', 'ER0484', 'ER0566', 'ER0876', 'ER0528', - 'ER0292', 'ER0630', 'ER0761', 'ER0849', 'ER0578', 'ER0232', - 'ER0673', 'ER0870', 'ER0575', 'ER0250', 'ER0599', 'ER0622', - 'ER0801', 'ER0806', 'ER0594', 'ER0831', 'ER0513', - ], - '8401': [ - 'ER0616', 'ER0730', 'ER0415', 'ER0522', 'ER0454', 'ER0758', - 'ER0715', 'ER0658', 'ER0602', 'ER0649', 'ER0540', 'ER0434', - 'ER0678', 'ER0550', 'ER0402', 'ER0636', 'ER0500', 'ER0740', - 'ER0664', 'ER0397', 'ER0565', 'ER0704', 'ER0720', 'ER0787', - 'ER0884', 'ER0573', 'ER0755', 'ER0392', 'ER0739', 'ER0530', - 'ER0437', 'ER0484', 'ER0653', 'ER0502', 'ER0615', 'ER0563', - 'ER0641', 'ER0391', 'ER0789', 'ER0451', 'ER0819', 'ER0442', - 'ER0798', 'ER0729', 'ER0772', 'ER0940', 'ER0682', 'ER0614', - 'ER0561', 'ER0393', - ], - '8501': [ - 'ER0807', 'ER0289', 'ER0587', 'ER0902', 'ER0877', 'ER0748', - 'ER0837', 'ER0408', 'ER0307', 'ER0759', 'ER0847', 'ER0433', - 'ER0498', 'ER0492', 'ER0735', 'ER0503', 'ER0461', 'ER0508', - 'ER0243', 'ER0583', 'ER0924', 'ER0395', 'ER0707', 'ER0572', - 'ER0536', 'ER0796', 'ER0929', 'ER0713', 'ER0603', 'ER0814', - 'ER0756', 'ER0398', 'ER0853', 'ER0276', 'ER0405', 'ER0418', - 'ER0517', 'ER0919', 'ER0781', 'ER0516', 'ER0417', 'ER0702', - 'ER0857', 'ER0486', 'ER0637', 'ER0736', 'ER0859', 'ER0483', - 'ER0824', 'ER0640', 'ER0714', - ], - '8601': [ - 'ER0455', 'ER0930', 'ER0293', 'ER0294', 'ER0677', 'ER0808', - 'ER0785', 'ER0628', 'ER0545', 'ER0551', 'ER0644', 'ER0922', - 'ER0670', 'ER0864', 'ER0629', 'ER0306', 'ER0494', 'ER0496', - 'ER0679', 'ER0874', 'ER0921', 'ER0910', 'ER0621', 'ER0667', - 'ER0262', 'ER0774', 'ER0488', 'ER0300', 'ER0234', 'ER0711', - 'ER0605', 'ER0897', 'ER0841', 'ER0778', 'ER0769', 'ER0487', - 'ER0556', 'ER0526', 'ER0795', 'ER0268', 'ER0266', 'ER0257', - ], - '8701': [ - 'ER0263', 'ER0661', 'ER0282', 'ER0394', 'ER0423', 'ER0665', - 'ER0598', 'ER0909', 'ER0481', 'ER0854', 'ER0471', 'ER0582', - 'ER0671', 'ER0466', 'ER0788', 'ER0934', 'ER0683', 'ER0680', - 'ER0890', 'ER0531', 'ER0647', 'ER0823', 'ER0608', 'ER0900', - 'ER0467', 'ER0607', 'ER0554', 'ER0233', 'ER0911', 'ER0726', - 'ER0675', 'ER0291', 'ER0313', 'ER0619', 'ER0775', 'ER0705', - 'ER0548', 'ER0891', 'ER0560', 'ER0904', 'ER0429', 'ER0655', - 'ER0224', 'ER0700', 'ER0797', 'ER0706', 'ER0533', 'ER0861', - 'ER0580', 'ER0449', 'ER0409', 'ER0613', 'ER0645', 'ER0315', - 'ER0718', 'ER0553', 'ER0444', 'ER0593', 'ER0499', 'ER0693', - 'ER0525', 'ER0451', 'ER0634', 'ER0689', 'ER0878', 'ER0518', - 'ER0887', - ], - '8801': [ - 'ER0811', 'ER0652', 'ER0889', 'ER0886', 'ER0936', 'ER0476', - 'ER0832', 'ER0626', 'ER0669', 'ER0404', 'ER0546', 'ER0501', - 'ER0894', 'ER0460', 'ER0805', 'ER0465', 'ER0717', 'ER0601', - 'ER0751', 'ER0777', 'ER0504', 'ER0749', 'ER0827', 'ER0896', - 'ER0903', 'ER0591', 'ER0436', 'ER0552', 'ER0716', 'ER0895', - 'ER0463', 'ER0809', 'ER0473', 'ER0883', 'ER0569', 'ER0610', - 'ER0275', 'ER0333', 'ER0344', 'ER0469', - ], - '8901': [ - 'ER0913', 'ER0310', 'ER0873', 'ER0448', 'ER0763', 'ER0441', - 'ER0936', 'ER0767', 'ER0416', 'ER0413', 'ER0589', 'ER0453', - 'ER0507', 'ER0287', 'ER0414', 'ER0406', 'ER0584', 'ER0866', - 'ER0893', 'ER0627', 'ER0227', 'ER0403', 'ER0428', 'ER0908', - 'ER0349', 'ER0221', 'ER0271', 'ER0659', 'ER0765', 'ER0478', - 'ER0511', 'ER0506', 'ER0743', 'ER0512', 'ER0916', 'ER0497', - 'ER0643', 'ER0638', 'ER0468', 'ER0597', - ], - '9001': [ - 'ER0446', 'ER0802', 'ER0570', 'ER0836', 'ER0576', 'ER0672', - 'ER0631', 'ER0490', 'ER0851', 'ER0450', 'ER0872', 'ER0912', - 'ER0815', 'ER0882', 'ER0738', 'ER0899', 'ER0620', 'ER0399', - 'ER0685', 'ER0477', 'ER0842', 'ER0529', 'ER0617', 'ER0865', - 'ER0754', 'ER0737', 'ER0753', 'ER0732', 'ER0623', 'ER0574', - 'ER0803', 'ER0651', 'ER0489', 'ER0668', 'ER0741', 'ER0699', - 'ER0592', 'ER0225', 'ER0229', 'ER0298', 'ER0270', 'ER0259', - 'ER0337', 'ER0770', 'ER0327', 'ER0251', 'ER0285', 'ER0927', - 'ER0810', 'ER0681', 'ER0887', - ], -}; - -/** - * Even IMPORT run (Djibouti -> Ethiopia) for each export run. Listed rather - * than computed as export+1 so a run that ever breaks the convention stays - * correct. Run numbers are always 4 digits (8401, never 84001). - */ -const IMPORT_RUN: Record = { - '8001': '8002', - '8101': '8102', - '8201': '8202', - '8301': '8302', - '8401': '8402', - '8501': '8502', - '8601': '8602', - '8701': '8702', - '8801': '8802', - '8901': '8902', - '9001': '9002', -}; - -export class SeedWagonRunNumbers2280000000000 implements MigrationInterface { - name = 'SeedWagonRunNumbers2280000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // Idempotent: clear the roster's runs first so a re-run cannot leave a - // wagon on a run it was since moved off of. - await queryRunner.query(` - UPDATE freight.wagons - SET export_train_number = NULL, import_train_number = NULL - WHERE export_train_number IS NOT NULL; - `); - - const claimed = new Set(); - - for (const [exportRun, wagons] of Object.entries(RUN_WAGONS)) { - const importRun = IMPORT_RUN[exportRun]; - if (!importRun) throw new Error(`import_run_missing:${exportRun}`); - - // First-listed wins — skip any wagon an earlier run already claimed. - const fresh = wagons.filter((w) => !claimed.has(w)); - fresh.forEach((w) => claimed.add(w)); - if (!fresh.length) continue; - - await queryRunner.query( - ` - UPDATE freight.wagons - SET export_train_number = $1, - import_train_number = $2, - updated_at = now() - WHERE wagon_number = ANY($3::text[]); - `, - [exportRun, importRun, fresh], - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.wagons - SET export_train_number = NULL, import_train_number = NULL - WHERE export_train_number IS NOT NULL; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2280000000000-WagonNumberPartialUnique.ts b/apps/edr-freight-api/src/migrations/2280000000000-WagonNumberPartialUnique.ts deleted file mode 100644 index f28c81317..000000000 --- a/apps/edr-freight-api/src/migrations/2280000000000-WagonNumberPartialUnique.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Wagons are now soft-deleted (deleted_at) instead of hard-deleted. The plain - * UNIQUE on wagon_number would keep a retired wagon's number reserved forever - * and block ever re-registering that number. Swap it for a PARTIAL unique index - * that only constrains live rows (deleted_at IS NULL); soft-deleted wagons no - * longer occupy their number. - * - * NOTE: the shared dev DB has no applied migration history, so this is also - * hand-applied there. The DO blocks + IF EXISTS/IF NOT EXISTS keep it - * idempotent whether the original uniqueness is the auto-named column - * constraint (wagons_wagon_number_key) or a TypeORM-named UQ_* constraint/index. - */ -export class WagonNumberPartialUnique2280000000000 implements MigrationInterface { - name = 'WagonNumberPartialUnique2280000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // Drop any UNIQUE constraint on freight.wagons(wagon_number), whatever it is - // named (dropping the constraint also drops its backing index). - await queryRunner.query(` - DO $$ - DECLARE con_name text; - BEGIN - FOR con_name IN - SELECT conname - FROM pg_constraint - WHERE conrelid = 'freight.wagons'::regclass - AND contype = 'u' - AND pg_get_constraintdef(oid) ILIKE '%(wagon_number)%' - LOOP - EXECUTE format('ALTER TABLE freight.wagons DROP CONSTRAINT IF EXISTS %I', con_name); - END LOOP; - END $$; - `); - - // Drop any standalone (non-partial) unique index on wagon_number too. - await queryRunner.query(` - DO $$ - DECLARE idx_name text; - BEGIN - FOR idx_name IN - SELECT c.relname - FROM pg_index i - JOIN pg_class c ON c.oid = i.indexrelid - WHERE i.indrelid = 'freight.wagons'::regclass - AND i.indisunique - AND i.indpred IS NULL - AND c.relname <> 'UQ_wagons_wagon_number_active' - AND pg_get_indexdef(i.indexrelid) ILIKE '%(wagon_number)%' - LOOP - EXECUTE format('DROP INDEX IF EXISTS freight.%I', idx_name); - END LOOP; - END $$; - `); - - // Live wagon numbers stay unique; soft-deleted rows are exempt. - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_wagons_wagon_number_active" - ON freight.wagons (wagon_number) - WHERE deleted_at IS NULL; - `); - } - - public async down(): Promise { - // No-op: re-adding a plain UNIQUE would fail whenever two soft-deleted - // wagons share a number, and the partial index is strictly safer. Left in - // place intentionally. - } -} diff --git a/apps/edr-freight-api/src/migrations/2290000000000-DropContainerWagonsPerUnit.ts b/apps/edr-freight-api/src/migrations/2290000000000-DropContainerWagonsPerUnit.ts deleted file mode 100644 index ed66c37d3..000000000 --- a/apps/edr-freight-api/src/migrations/2290000000000-DropContainerWagonsPerUnit.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * container_types.wagons_per_unit is no longer stored: the wagon fraction is - * derived from size_ft everywhere (40ft = 1.00 wagon, 20ft = 0.50 — two per - * wagon; see rule-engine/container-type.util.ts). The stored value duplicated - * that rule and could silently drift from it. - */ -export class DropContainerWagonsPerUnit2290000000000 implements MigrationInterface { - name = 'DropContainerWagonsPerUnit2290000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.container_types DROP COLUMN IF EXISTS wagons_per_unit; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.container_types - ADD COLUMN IF NOT EXISTS wagons_per_unit numeric(4,2); - `); - // Backfill from the same size rule the code now derives from. - await queryRunner.query(` - UPDATE freight.container_types - SET wagons_per_unit = CASE WHEN size_ft >= 40 THEN 1.00 ELSE 0.50 END; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2290000000000-SeedWagonYardDoraleh.ts b/apps/edr-freight-api/src/migrations/2290000000000-SeedWagonYardDoraleh.ts deleted file mode 100644 index 7e12efdbb..000000000 --- a/apps/edr-freight-api/src/migrations/2290000000000-SeedWagonYardDoraleh.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Stand the whole wagon fleet in Doraleh. - * - * Runs AFTER SeedEdrWagonFleetErNumbering2260000000000, which recreates every - * wagon with a NULL yard — so this must stay later in timestamp order. - * - * A wagon with no yard cannot be coupled to a train (the train builder only - * offers AVAILABLE wagons standing in the train's own yard), which left the - * seeded fleet unusable. Doraleh is the Djibouti-side port yard the import runs - * originate from. - * - * The yard is created when absent: environments disagree about which yards - * exist, so this cannot assume one is there. - */ -const YARD_CODE = 'DORALEH'; - -export class SeedWagonYardDoraleh2290000000000 implements MigrationInterface { - name = 'SeedWagonYardDoraleh2290000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // Ensure the yard exists and is usable. Deliberately does NOT overwrite an - // existing label/country — a deployment that already calls this yard - // something else keeps its own naming. - await queryRunner.query( - ` - INSERT INTO freight.yards (code, label, country, is_active, display_order) - VALUES ($1, 'Doraleh', 'Djibouti', true, 12) - ON CONFLICT (code) DO UPDATE SET - is_active = true, - deleted_at = NULL, - updated_at = now(); - `, - [YARD_CODE], - ); - - const [yard] = await queryRunner.query( - `SELECT id FROM freight.yards WHERE code = $1 AND deleted_at IS NULL LIMIT 1;`, - [YARD_CODE], - ); - - if (!yard?.id) { - throw new Error(`yard_missing:${YARD_CODE}`); - } - - // Whole fleet — a wagon already coupled to a built train follows the train, - // so leave those where they stand. - await queryRunner.query( - ` - UPDATE freight.wagons - SET current_yard_id = $1::uuid, - updated_at = now() - WHERE train_id IS NULL; - `, - [yard.id], - ); - } - - public async down(queryRunner: QueryRunner): Promise { - // Back to the state SeedEdrWagonFleetErNumbering leaves them in. - await queryRunner.query(` - UPDATE freight.wagons - SET current_yard_id = NULL - WHERE train_id IS NULL; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2290000000000-YardFacilities.ts b/apps/edr-freight-api/src/migrations/2290000000000-YardFacilities.ts deleted file mode 100644 index 620eebc14..000000000 --- a/apps/edr-freight-api/src/migrations/2290000000000-YardFacilities.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Intercity (DOMESTIC) cargo is loaded at its origin yard and unloaded at its - * destination yard, but only some yards have the equipment to do it. EDR's - * load/unload facilities are Indode, Sebeta, Modjo, Adama, Dire Dawa and Negad — - * and the set grows, so it must be data, not a constant. - * - * `yards.has_facility` marks a yard as a load/unload point; `yard_facilities` - * holds what that facility can do. Only a facility with `has_warehouse` (Indode - * today) stores cargo, and therefore accrues storage/demurrage — the rest just - * move it on and off the train. - * - * `facility_handling_events` records each load/unload and carries its GRN. - * warehouse_inventory can't do that job: its warehouse/yard/zone are NOT NULL, so - * a facility with no warehouse could never have a row. `inventory_id` links to the - * storage record when the facility does have a warehouse. - */ -export class YardFacilities2290000000000 implements MigrationInterface { - name = 'YardFacilities2290000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.yards - ADD COLUMN IF NOT EXISTS has_facility boolean NOT NULL DEFAULT false - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.yard_facilities ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - yard_id uuid NOT NULL REFERENCES freight.yards(id) ON DELETE CASCADE, - has_warehouse boolean NOT NULL DEFAULT false, - equipment_notes text NULL, - is_active boolean NOT NULL DEFAULT true, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL - ) - `); - // One facility record per yard. - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_yard_facility_yard" - ON freight.yard_facilities (yard_id) WHERE deleted_at IS NULL - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.facility_handling_events ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - booking_id uuid NOT NULL REFERENCES freight.bookings(id), - yard_id uuid NOT NULL REFERENCES freight.yards(id), - train_schedule_id uuid NULL REFERENCES freight.train_schedules(id), - event_type varchar(10) NOT NULL, - grn_number varchar(60) NULL, - quantity numeric(14, 3) NULL, - weight_tons numeric(14, 3) NULL, - inventory_id uuid NULL REFERENCES freight.warehouse_inventory(id), - performed_by varchar(120) NULL, - occurred_at timestamptz NOT NULL DEFAULT now(), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_facility_handling_events_booking" - ON freight.facility_handling_events (booking_id) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_facility_handling_events_yard" - ON freight.facility_handling_events (yard_id) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_facility_handling_events_grn" - ON freight.facility_handling_events (grn_number) WHERE grn_number IS NOT NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.facility_handling_events`); - await queryRunner.query(`DROP TABLE IF EXISTS freight.yard_facilities`); - await queryRunner.query(` - ALTER TABLE freight.yards DROP COLUMN IF EXISTS has_facility - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2300000000000-CreateRateChangeRequests.ts b/apps/edr-freight-api/src/migrations/2300000000000-CreateRateChangeRequests.ts deleted file mode 100644 index a3e36f0b9..000000000 --- a/apps/edr-freight-api/src/migrations/2300000000000-CreateRateChangeRequests.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Approval workflow for edits to LIVE rates. A LIVE rate is what pricing - * charges, so it is never edited in place: the edit is filed here as PENDING - * and the live row keeps its value until an approver applies it. - * - * `payload` holds the changed fields only; `previous_values` snapshots what - * they were at submit time so the approver sees a real before→after diff. - */ -export class CreateRateChangeRequests2300000000000 implements MigrationInterface { - name = 'CreateRateChangeRequests2300000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.rate_change_requests ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - rate_id uuid NOT NULL REFERENCES freight.rates (id), - payload jsonb NOT NULL, - previous_values jsonb NOT NULL, - status varchar(10) NOT NULL DEFAULT 'PENDING', - requested_by_user_id uuid NULL, - decided_by_user_id uuid NULL, - decided_at timestamptz NULL, - decision_note text NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz NULL - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_rcr_status - ON freight.rate_change_requests (status) - `); - // At most one pending edit per rate — two racing requests would both pass - // validation and the second would silently overwrite the first on approval. - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS uq_rcr_one_pending_per_rate - ON freight.rate_change_requests (rate_id) - WHERE status = 'PENDING' AND deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.rate_change_requests`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2300000000000-RepairGpsTrackingTables.ts b/apps/edr-freight-api/src/migrations/2300000000000-RepairGpsTrackingTables.ts deleted file mode 100644 index f4628db36..000000000 --- a/apps/edr-freight-api/src/migrations/2300000000000-RepairGpsTrackingTables.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Repair for environments missing the GPS tracking tables. - * - * AddGpsTracking2000000000000 creates freight.gps_devices / gps_positions, but - * some databases have it RECORDED in public.migrations without the tables ever - * landing. TypeORM never re-runs a recorded migration, so those environments - * stay broken through any number of restarts — the GT06 listener accepts tracker - * packets on its TCP port regardless of schema state and fails per packet with - * `relation "freight.gps_devices" does not exist`, dropping position fixes. - * - * This re-issues the same DDL under a new name so it is applied afresh. Every - * statement is IF NOT EXISTS, so it is a no-op where the tables already exist - * and safe on every environment. - * - * Kept byte-identical to the original DDL on purpose: this must converge on the - * schema the entities expect, not a variant of it. - */ -export class RepairGpsTrackingTables2300000000000 implements MigrationInterface { - name = "RepairGpsTrackingTables2300000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.gps_devices ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - imei varchar(20) NOT NULL UNIQUE, - name varchar, - vehicle_id uuid REFERENCES freight.vehicles(id), - status varchar(16) NOT NULL DEFAULT 'REGISTERED', - last_seen_at timestamptz, - last_lat numeric(10,6), - last_lng numeric(10,6), - last_speed numeric(6,2), - last_course int, - last_fix_at timestamptz, - voltage_level int, - gsm_level int, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_GPS_DEVICES_VEHICLE" - ON freight.gps_devices (vehicle_id) - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.gps_positions ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - device_id uuid NOT NULL, - imei varchar(20) NOT NULL, - vehicle_id uuid, - lat numeric(10,6) NOT NULL, - lng numeric(10,6) NOT NULL, - speed numeric(6,2) NOT NULL DEFAULT 0, - course int NOT NULL DEFAULT 0, - satellites int NOT NULL DEFAULT 0, - positioned boolean NOT NULL DEFAULT false, - gps_time timestamptz NOT NULL, - alarm int NOT NULL DEFAULT 0, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_GPS_POSITIONS_DEVICE_TIME" - ON freight.gps_positions (device_id, gps_time) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_GPS_POSITIONS_VEHICLE_TIME" - ON freight.gps_positions (vehicle_id, gps_time) - `); - } - - public async down(): Promise { - // No-op: dropping the tables would discard tracker history on environments - // where this migration was the one that created them. AddGpsTracking owns - // the teardown. - } -} diff --git a/apps/edr-freight-api/src/migrations/2310000000000-CreateSupportChat.ts b/apps/edr-freight-api/src/migrations/2310000000000-CreateSupportChat.ts deleted file mode 100644 index 17db8a4e1..000000000 --- a/apps/edr-freight-api/src/migrations/2310000000000-CreateSupportChat.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Customer-support chat. A `support_conversations` row is the single ongoing - * thread with a company; `support_messages` are its text messages. There is no - * lifecycle column — a thread is opened by whichever side speaks first and - * stays open. Enum-like columns are varchar (no PG enum churn). - * - * The unique index on `company_id` is load-bearing, not just an optimization: - * the get-or-create path depends on it to settle concurrent first-messages. - * It is partial on `deleted_at IS NULL` so a soft-deleted thread doesn't block - * a fresh one. - */ -export class CreateSupportChat2310000000000 implements MigrationInterface { - name = "CreateSupportChat2310000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.support_conversations ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - company_id uuid NOT NULL, - company_name varchar(200), - created_by_user_id uuid, - last_message_at timestamptz, - last_message_preview varchar(280), - last_message_author_role varchar(12), - customer_last_read_at timestamptz, - agent_last_read_at timestamptz, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "IDX_SUPPORT_CONV_COMPANY" - ON freight.support_conversations (company_id) - WHERE deleted_at IS NULL - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_SUPPORT_CONV_LASTMSG" - ON freight.support_conversations (last_message_at) - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.support_messages ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - conversation_id uuid NOT NULL, - author_user_id uuid NOT NULL, - author_role varchar(12) NOT NULL, - author_name varchar(200), - body text NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_SUPPORT_MSG_CONV_CREATED" - ON freight.support_messages (conversation_id, created_at) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_SUPPORT_MSG_CONV_CREATED"`, - ); - await queryRunner.query(`DROP TABLE IF EXISTS freight.support_messages`); - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_SUPPORT_CONV_LASTMSG"`, - ); - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_SUPPORT_CONV_COMPANY"`, - ); - await queryRunner.query( - `DROP TABLE IF EXISTS freight.support_conversations`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2320000000000-AddRateYardScope.ts b/apps/edr-freight-api/src/migrations/2320000000000-AddRateYardScope.ts deleted file mode 100644 index 8e693cf2a..000000000 --- a/apps/edr-freight-api/src/migrations/2320000000000-AddRateYardScope.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Scope base rail freight to a route (origin yard → destination yard). - * - * Until now a base-freight rate was keyed by direction + container/bulk scope - * only, so "container import" cost the same whether the box was railed to Dire - * Dawa or to Mojo. Rates now carry the yard pair the price is quoted for, which - * is what the business actually sells: `container import, Djibouti → Dire Dawa, - * 500 USD`. - * - * Existing base-freight rates predate the yard pair and cannot be backfilled — - * there is no way to know which route each was meant for. They are retired - * (SUPERSEDED + soft-deleted) rather than deleted, because booking_rate_snapshot - * and rate_change_requests hold FKs to them (RESTRICT) and those rows are price - * history. Retiring drops them out of pricing and the admin UI just the same; - * the yard-scoped replacements must be re-entered. - * - * Surcharges, first-mile and last-mile rates are untouched: they are not - * route-scoped and keep NULL yards. - */ -export class AddRateYardScope2320000000000 implements MigrationInterface { - name = 'AddRateYardScope2320000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // ── 1. Yard columns + FKs ────────────────────────────────────────────── - await queryRunner.query(` - ALTER TABLE freight.rates - ADD COLUMN IF NOT EXISTS origin_yard_id uuid NULL, - ADD COLUMN IF NOT EXISTS destination_yard_id uuid NULL; - `); - - await queryRunner.query(` - DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'FK_rates_origin_yard_id') THEN - ALTER TABLE freight.rates - ADD CONSTRAINT "FK_rates_origin_yard_id" - FOREIGN KEY (origin_yard_id) REFERENCES freight.yards(id); - END IF; - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'FK_rates_destination_yard_id') THEN - ALTER TABLE freight.rates - ADD CONSTRAINT "FK_rates_destination_yard_id" - FOREIGN KEY (destination_yard_id) REFERENCES freight.yards(id); - END IF; - END $$; - `); - - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_rates_origin_yard_id" ON freight.rates (origin_yard_id);`, - ); - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "IDX_rates_destination_yard_id" ON freight.rates (destination_yard_id);`, - ); - - // ── 2. Retire route-less base freight ────────────────────────────────── - // Soft-delete, not DELETE: booking_rate_snapshot.rate_id is ON DELETE - // RESTRICT and those snapshots are what past bookings were charged. - await queryRunner.query(` - UPDATE freight.rates - SET status = 'SUPERSEDED', - deleted_at = now(), - updated_at = now() - WHERE deleted_at IS NULL - AND "trigger" = 'ALWAYS' - AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY'); - `); - - // ── 3. Route is part of a rate's identity ────────────────────────────── - // Two rates may now share rateType + scope + unit as long as they price - // different legs, so the yard pair joins the uniqueness tuple. - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern";`); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_rates_pattern" - ON freight.rates ( - rate_type, - COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid), - COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid), - COALESCE(trade_direction, ''), - COALESCE(origin_yard_id, '00000000-0000-0000-0000-000000000000'::uuid), - COALESCE(destination_yard_id, '00000000-0000-0000-0000-000000000000'::uuid), - rate_unit - ) - WHERE deleted_at IS NULL AND status <> 'SUPERSEDED'; - `); - - // ── 4. Base freight must carry a route; nothing else may ─────────────── - // Retired rows are exempt — they are the route-less rates step 2 just - // superseded, and they must stay readable for snapshot history. - await queryRunner.query(` - DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_rates_yard_scope') THEN - ALTER TABLE freight.rates - ADD CONSTRAINT "CK_rates_yard_scope" CHECK ( - deleted_at IS NOT NULL - OR status = 'SUPERSEDED' - OR CASE - WHEN "trigger" = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY') - THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL - ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL - END - ); - END IF; - END $$; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - // The retired rates are not un-superseded: which route each belonged to was - // never recorded, so reviving them would restore rates that price the wrong - // legs. Down only reverses the schema. - await queryRunner.query( - `ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope";`, - ); - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern";`); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_rates_pattern" - ON freight.rates ( - rate_type, - COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid), - COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid), - COALESCE(trade_direction, ''), - rate_unit - ) - WHERE deleted_at IS NULL AND status <> 'SUPERSEDED'; - `); - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_rates_destination_yard_id";`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_rates_origin_yard_id";`); - await queryRunner.query( - `ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "FK_rates_destination_yard_id";`, - ); - await queryRunner.query( - `ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "FK_rates_origin_yard_id";`, - ); - await queryRunner.query(` - ALTER TABLE freight.rates - DROP COLUMN IF EXISTS destination_yard_id, - DROP COLUMN IF EXISTS origin_yard_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2320000000000-SupportChatAttachments.ts b/apps/edr-freight-api/src/migrations/2320000000000-SupportChatAttachments.ts deleted file mode 100644 index 1f383f1da..000000000 --- a/apps/edr-freight-api/src/migrations/2320000000000-SupportChatAttachments.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Let a support message carry files instead of text. - * - * No new table: chat attachments reuse the polymorphic `freight.files` record - * with `resource = 'support_message'` and `resource_id = `, the same - * way bookings/contracts/companies already store theirs. - * - * The only schema change is dropping NOT NULL from `support_messages.body`, so - * an attachment-only message can say "there is no text" rather than smuggling - * that fact through an empty string. DROP NOT NULL is a catalog-only change in - * Postgres — no table rewrite, no long lock — so this is safe on a live table. - * - * The partial index on (resource, resource_id) is what makes hydrating a page of - * messages one indexed lookup instead of a scan of every file row in the system. - */ -export class SupportChatAttachments2320000000000 implements MigrationInterface { - name = "SupportChatAttachments2320000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.support_messages - ALTER COLUMN body DROP NOT NULL - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_FILES_RESOURCE_LOOKUP" - ON freight.files (resource, resource_id) - WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DROP INDEX IF EXISTS freight."IDX_FILES_RESOURCE_LOOKUP" - `); - - // Re-imposing NOT NULL would fail on any attachment-only message written - // while this migration was applied. Backfill those to '' first so the - // rollback is deterministic rather than dependent on production data. - await queryRunner.query(` - UPDATE freight.support_messages SET body = '' WHERE body IS NULL - `); - await queryRunner.query(` - ALTER TABLE freight.support_messages - ALTER COLUMN body SET NOT NULL - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2320000000000-YardFacilityFreightTypes.ts b/apps/edr-freight-api/src/migrations/2320000000000-YardFacilityFreightTypes.ts deleted file mode 100644 index d6e7ae273..000000000 --- a/apps/edr-freight-api/src/migrations/2320000000000-YardFacilityFreightTypes.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * A facility handles what its equipment can handle. Containers need a reach - * stacker or gantry, so only Indode, Modjo and Dire Dawa take them; bulk needs - * far less, so all five facilities load and unload it. - * - * Both default true — a facility handles everything unless someone says - * otherwise, which keeps existing rows working and makes the seeder the place - * where the real capability is stated. - */ -export class YardFacilityFreightTypes2320000000000 implements MigrationInterface { - name = 'YardFacilityFreightTypes2320000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.yard_facilities - ADD COLUMN IF NOT EXISTS handles_container boolean NOT NULL DEFAULT true, - ADD COLUMN IF NOT EXISTS handles_bulk boolean NOT NULL DEFAULT true - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.yard_facilities - DROP COLUMN IF EXISTS handles_container, - DROP COLUMN IF EXISTS handles_bulk - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2330000000000-AddBookingCloseOffset.ts b/apps/edr-freight-api/src/migrations/2330000000000-AddBookingCloseOffset.ts deleted file mode 100644 index b435e55ea..000000000 --- a/apps/edr-freight-api/src/migrations/2330000000000-AddBookingCloseOffset.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add a global "booking close offset" — how long BEFORE departure a schedule's - * booking window shuts — configurable separately for import and export. - * - * When an offset is set, the window's close instant is `departure − offset` - * (e.g. departure 17:00 with a 3-hour import offset closes at 14:00; departure - * Jul-10 16:00 with a 1-day export offset closes Jul-9 16:00). It caps the whole - * booking lifecycle: the first window close, every reopen cycle, and the export - * FCFS close all land at/at-or-before this cutoff instead of at departure. - * - * NULL / 0 preserves the previous behaviour exactly (import closes at - * open+duration clamped to departure; export closes at departure), so existing - * installs are unaffected until an offset is entered. - * - * `*_close_offset_minutes` on the global-rules singleton is the live config; the - * matching `rule_*_close_offset_minutes` snapshot on each schedule freezes it at - * creation so the batch board keeps drawing the window the customer was shown - * even after a later global-rules edit. Both are nullable with no backfill — - * absent means "no offset", the safe default. - */ -export class AddBookingCloseOffset2330000000000 implements MigrationInterface { - name = "AddBookingCloseOffset2330000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - ADD COLUMN IF NOT EXISTS import_close_offset_minutes integer, - ADD COLUMN IF NOT EXISTS export_close_offset_minutes integer; - `); - - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS rule_import_close_offset_minutes integer, - ADD COLUMN IF NOT EXISTS rule_export_close_offset_minutes integer; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - DROP COLUMN IF EXISTS rule_import_close_offset_minutes, - DROP COLUMN IF EXISTS rule_export_close_offset_minutes; - `); - await queryRunner.query(` - ALTER TABLE freight.train_scheduling_global_rules - DROP COLUMN IF EXISTS import_close_offset_minutes, - DROP COLUMN IF EXISTS export_close_offset_minutes; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2340000000000-AddCargoTypeHasLashing.ts b/apps/edr-freight-api/src/migrations/2340000000000-AddCargoTypeHasLashing.ts deleted file mode 100644 index 6585cc842..000000000 --- a/apps/edr-freight-api/src/migrations/2340000000000-AddCargoTypeHasLashing.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add `has_lashing` to cargo types. - * - * When true, every booking of that cargo type incurs the flat LASHING - * surcharge (a rate with trigger = 'LASHING'). Defaults to false so existing - * cargo ships without the fee until the flag is turned on. - */ -export class AddCargoTypeHasLashing2340000000000 implements MigrationInterface { - name = "AddCargoTypeHasLashing2340000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.cargo_types - ADD COLUMN IF NOT EXISTS has_lashing boolean NOT NULL DEFAULT false; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.cargo_types - DROP COLUMN IF EXISTS has_lashing; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2340000000000-AddReverseWagonOrder.ts b/apps/edr-freight-api/src/migrations/2340000000000-AddReverseWagonOrder.ts deleted file mode 100644 index ad389e929..000000000 --- a/apps/edr-freight-api/src/migrations/2340000000000-AddReverseWagonOrder.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Add an opt-in "reverse wagon order" flag to a train schedule. - * - * When true, the built wagon plan is flipped at build time so the physically-last - * wagon sits at position 1. Only the order (sequence_no) changes — composition and - * booking allocations travel with their slot. The flag is frozen on the schedule - * at creation and re-applied every time the wagon plan is rebuilt, so the stored - * train order and the schedule order always match. - * - * Defaults to false; existing schedules keep their as-built order. - */ -export class AddReverseWagonOrder2340000000000 implements MigrationInterface { - name = "AddReverseWagonOrder2340000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - ADD COLUMN IF NOT EXISTS reverse_wagon_order boolean NOT NULL DEFAULT false; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.train_schedules - DROP COLUMN IF EXISTS reverse_wagon_order; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2350000000000-RefreshContractPricingArticles.ts b/apps/edr-freight-api/src/migrations/2350000000000-RefreshContractPricingArticles.ts deleted file mode 100644 index 95e007c8a..000000000 --- a/apps/edr-freight-api/src/migrations/2350000000000-RefreshContractPricingArticles.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -import { CONTRACT_TEMPLATE_DEFAULTS } from '../seed/data/contract-template-defaults'; - -/** - * Refresh the "pricing" article of each seeded contract template so it points - * at the live Rate Schedule instead of hardcoded price figures (USD 400/wagon, - * USD 919/40ft, …). The original CreateContractTemplates migration seeded the - * old prose with ON CONFLICT DO NOTHING, so those figures are frozen in the DB - * rows and would otherwise contradict the rate-config-driven schedule table now - * rendered under the pricing article. - * - * Only the article whose id = 'pricing' is touched, and only when its body - * still matches the originally-seeded prose — so any admin edit to the pricing - * article is left untouched. Idempotent: re-running is a no-op once refreshed. - */ -export class RefreshContractPricingArticles2350000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - for (const seed of CONTRACT_TEMPLATE_DEFAULTS) { - const pricing = seed.articles.find((article) => article.id === 'pricing'); - if (!pricing) continue; - - // jsonb_set the title + body of the element whose id = 'pricing', matched - // by array index. Guarded so admin-edited bodies are never overwritten. - await queryRunner.query( - ` - UPDATE freight.contract_templates ct - SET articles = ( - SELECT jsonb_agg( - CASE - WHEN elem->>'id' = 'pricing' - THEN elem || jsonb_build_object('title', $2::text, 'body', $3::text) - ELSE elem - END - ) - FROM jsonb_array_elements(ct.articles) elem - ) - WHERE ct.code = $1 - AND EXISTS ( - SELECT 1 FROM jsonb_array_elements(ct.articles) e - WHERE e->>'id' = 'pricing' - AND e->>'body' LIKE ANY (ARRAY[ - '%USD 59.4 per metric ton%', - '%USD 696 (six hundred ninety-six) per wagon%', - '%USD 400 (four hundred) per wagon%', - '%From SGTD to Dire Dawa dry port, the rate is USD 919%', - '%Railway transportation charges from GMP to SGTD: USD 819%', - '%prevailing EDR domestic container tariff, as set out in the commercial schedule%' - ]) - ); - `, - [seed.code, pricing.title, pricing.body], - ); - } - } - - public async down(): Promise { - // No-op: the refreshed pricing prose is the correct forward state; reverting - // to hardcoded figures would reintroduce the rate-schedule contradiction. - } -} diff --git a/apps/edr-freight-api/src/migrations/2360000000000-RefreshContractPricingArticles.ts b/apps/edr-freight-api/src/migrations/2360000000000-RefreshContractPricingArticles.ts deleted file mode 100644 index 566d0308e..000000000 --- a/apps/edr-freight-api/src/migrations/2360000000000-RefreshContractPricingArticles.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -import { CONTRACT_TEMPLATE_DEFAULTS } from '../seed/data/contract-template-defaults'; - -/** - * Refresh the `pricing` article body of the six seeded contract templates to - * the live-rate-schedule wording. The per-lane figures (e.g. "USD 400 per - * wagon") are now rendered from the LIVE rate config instead of frozen prose, - * so any template whose pricing article still carries a hardcoded price token - * is rewritten to the current seed text. - * - * The guard `body ~ '(USD|ETB) [0-9]'` identifies the auto-seeded original - * prose (which always quoted a currency + figure) and matches neither an - * already-migrated body nor a hand-edited one that adopted the schedule - * wording — so admin edits are preserved. Idempotent: after the rewrite the - * price token is gone, so a re-run is a no-op. Fresh databases seed the new - * text directly (CreateContractTemplates imports the same seed), making this - * a targeted backfill for databases seeded before the seed changed. - */ -const HARDCODED_PRICE_TOKEN = '(USD|ETB) [0-9]'; - -export class RefreshContractPricingArticles2360000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - for (const seed of CONTRACT_TEMPLATE_DEFAULTS) { - const pricing = seed.articles.find((a) => a.id === 'pricing'); - if (!pricing) continue; - - // Rewrite only the article whose id = 'pricing', in place, and only when - // its body still quotes a hardcoded currency figure. jsonb_agg keeps the - // rest of the article (id/title/order) and every other article intact. - await queryRunner.query( - ` - UPDATE freight.contract_templates AS t - SET articles = ( - SELECT jsonb_agg( - CASE - WHEN elem->>'id' = 'pricing' - THEN jsonb_set(elem, '{body}', to_jsonb($2::text), true) - ELSE elem - END - ORDER BY ord - ) - FROM jsonb_array_elements(t.articles) WITH ORDINALITY AS a(elem, ord) - ), - updated_at = now() - WHERE t.code = $1 - AND EXISTS ( - SELECT 1 - FROM jsonb_array_elements(t.articles) AS x - WHERE x->>'id' = 'pricing' - AND x->>'body' ~ $3 - ); - `, - [seed.code, pricing.body, HARDCODED_PRICE_TOKEN], - ); - } - } - - /** - * Irreversible in practice — the original per-lane figures are not restored. - * A no-op down keeps the migration reversible-by-contract without - * resurrecting stale hardcoded prices. - */ - public async down(): Promise { - // intentionally empty - } -} diff --git a/apps/edr-freight-api/src/migrations/2370000000000-AddContainerUnitReturnFlag.ts b/apps/edr-freight-api/src/migrations/2370000000000-AddContainerUnitReturnFlag.ts deleted file mode 100644 index 1971f6166..000000000 --- a/apps/edr-freight-api/src/migrations/2370000000000-AddContainerUnitReturnFlag.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Per-container handling opt-in: each physical container can now be marked - * hazardous / reefer / with-return individually, next to its VGM. The hazardous - * and reefer flags already existed on the unit row; only the return leg was - * missing, so a booking of 20 containers with 10 returning empty can bill the - * WITH_RETURN surcharge on 10 instead of all 20. - * - * Backfill: existing rows keep false. The line-level counts - * (booking_container.return_quantity etc.) stay authoritative for bookings made - * before this change — the rule engine falls back to them when no unit is flagged. - */ -export class AddContainerUnitReturnFlag2370000000000 implements MigrationInterface { - name = 'AddContainerUnitReturnFlag2370000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "freight"."booking_container_units" ADD COLUMN IF NOT EXISTS "is_return" boolean NOT NULL DEFAULT false`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "freight"."booking_container_units" DROP COLUMN IF EXISTS "is_return"`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2380000000000-AddTrainDeactivatedStatus.ts b/apps/edr-freight-api/src/migrations/2380000000000-AddTrainDeactivatedStatus.ts deleted file mode 100644 index 2c252f127..000000000 --- a/apps/edr-freight-api/src/migrations/2380000000000-AddTrainDeactivatedStatus.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * New built-train lifecycle status DEACTIVATED: staff park a train indefinitely - * (only allowed while it has no DRAFT/SCHEDULED/DISPATCHED schedule). Like - * UNDER_MAINTENANCE / OUT_OF_SERVICE it is staff-owned — the scheduler never - * overwrites it and refuses to schedule a deactivated train. - * - * Postgres cannot drop an enum value, so down() is a no-op. - */ -export class AddTrainDeactivatedStatus2380000000000 implements MigrationInterface { - name = 'AddTrainDeactivatedStatus2380000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TYPE "freight"."train_status" ADD VALUE IF NOT EXISTS 'DEACTIVATED'`, - ); - } - - public async down(): Promise { - // Enum values cannot be removed in Postgres; leaving the label is harmless. - } -} diff --git a/apps/edr-freight-api/src/migrations/2390000000000-SeedImportTrainNumbers.ts b/apps/edr-freight-api/src/migrations/2390000000000-SeedImportTrainNumbers.ts deleted file mode 100644 index 99c764806..000000000 --- a/apps/edr-freight-api/src/migrations/2390000000000-SeedImportTrainNumbers.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Admin-managed catalog of IMPORT run numbers (even, Djibouti → Ethiopia) - * selectable in the Train Builder. The paired EXPORT number is derived - * (import − 1), so only the import side is configured. Seeded with the runs - * historically hardcoded in the backoffice's trainRuns constants; admins add - * new runs from the Dropdown Settings editor. - */ -export class SeedImportTrainNumbers2390000000000 implements MigrationInterface { - name = 'SeedImportTrainNumbers2390000000000'; - private readonly code = 'import_train_numbers'; - private readonly options: string[] = [ - '8002', - '8102', - '8202', - '8302', - '8402', - '8502', - '8602', - '8702', - '8802', - '8902', - '9002', - ]; - - public async up(queryRunner: QueryRunner): Promise { - const existing = await queryRunner.query( - `SELECT id FROM freight.dropdown_settings WHERE code = $1 LIMIT 1;`, - [this.code], - ); - if (existing.length > 0) return; - - const inserted = await queryRunner.query( - `INSERT INTO freight.dropdown_settings (code, label, description, multiple, meta) - VALUES ($1, $2, $3, false, $4::jsonb) - RETURNING id;`, - [ - this.code, - 'Import train numbers', - 'Even IMPORT run numbers (Djibouti → Ethiopia) selectable when building a train. The paired export number is derived automatically (import − 1).', - JSON.stringify({ searchable: true, clearable: true }), - ], - ); - const settingId = inserted[0].id; - - for (let i = 0; i < this.options.length; i++) { - const value = this.options[i]; - await queryRunner.query( - `INSERT INTO freight.dropdown_options (setting_id, value, label, display_order) - VALUES ($1, $2, $3, $4);`, - [settingId, value, value, i], - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DELETE FROM freight.dropdown_settings WHERE code = $1;`, [ - this.code, - ]); - } -} diff --git a/apps/edr-freight-api/src/migrations/2390000000000-WidenYardCodeForSoftDeleteSuffix.ts b/apps/edr-freight-api/src/migrations/2390000000000-WidenYardCodeForSoftDeleteSuffix.ts deleted file mode 100644 index 31263f4b2..000000000 --- a/apps/edr-freight-api/src/migrations/2390000000000-WidenYardCodeForSoftDeleteSuffix.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Yard soft-delete now appends `@` to the unique code (SEBETA → - * SEBETA@1755612345678) so the name can be reused by a new yard while - * UQ_yards_code still spans soft-deleted rows. varchar(20) can't hold long - * codes plus the 14-char suffix, so widen to 40. - */ -export class WidenYardCodeForSoftDeleteSuffix2390000000000 implements MigrationInterface { - name = 'WidenYardCodeForSoftDeleteSuffix2390000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "freight"."yards" ALTER COLUMN "code" TYPE varchar(40)`, - ); - } - - public async down(): Promise { - // Narrowing would fail on suffixed codes; keep 40. - } -} diff --git a/apps/edr-freight-api/src/migrations/2400000000000-AddCustomerTruckExitWeights.ts b/apps/edr-freight-api/src/migrations/2400000000000-AddCustomerTruckExitWeights.ts deleted file mode 100644 index 6cddae23d..000000000 --- a/apps/edr-freight-api/src/migrations/2400000000000-AddCustomerTruckExitWeights.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Per-truck exit weights for customer self-haul, mirroring what - * `last_mile_vehicle_assignments` already carries for EDR trucks. - * - * A bulk booking is hauled away truck by truck until no tonnage is left, and the - * EDR side enforces that by summing `net_weight_tons` of departed trucks. The - * customer side had no net and no tare — only `gross_weight_kg`, which nothing - * in the live flow ever wrote (the release flow updated the EDR table only). So - * a self-haul bulk booking could take unlimited trucks: hauled tonnage always - * summed to zero. - * - * `gross_weight_kg` is left alone but note it holds TONNES despite its name — - * the weighing UI is in tonnes throughout. The new columns are named for the - * unit they actually hold. - */ -export class AddCustomerTruckExitWeights2400000000000 implements MigrationInterface { - name = 'AddCustomerTruckExitWeights2400000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.customer_truck_assignments - ADD COLUMN IF NOT EXISTS tare_weight_tons numeric(14,3) NULL, - ADD COLUMN IF NOT EXISTS net_weight_tons numeric(14,3) NULL - `); - - // Departed trucks are what the drawdown sums, so it reads this index. - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_customer_truck_departed" - ON freight.customer_truck_assignments (booking_id, departed_at) - WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_customer_truck_departed"`, - ); - await queryRunner.query(` - ALTER TABLE freight.customer_truck_assignments - DROP COLUMN IF EXISTS tare_weight_tons, - DROP COLUMN IF EXISTS net_weight_tons - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2400000000000-NormalizeCompanyRegions.ts b/apps/edr-freight-api/src/migrations/2400000000000-NormalizeCompanyRegions.ts deleted file mode 100644 index b3369b82d..000000000 --- a/apps/edr-freight-api/src/migrations/2400000000000-NormalizeCompanyRegions.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * `freight.companies.region` was free text until region became a closed set - * (see ETHIOPIAN_REGIONS in @edr/types). This normalizes the rows written under - * the old rules so they satisfy the new dropdown. - * - * Two classes of bad data exist, handled differently: - * - * - Unambiguous spelling/case drift ("Addis ababa", "oromoia") — rewritten to - * the canonical spelling. - * - Values that are not regions at all ("Arba Minch", a city), and rows whose - * region contradicts their own zone/woreda — set to NULL. These are NOT - * guessed at: inferring "Gurage/Meskan" means Central Ethiopia would silently - * overwrite what the customer actually submitted. NULL surfaces the gap and - * the required dropdown forces a deliberate pick on next edit. - */ -export class NormalizeCompanyRegions2400000000000 implements MigrationInterface { - name = 'NormalizeCompanyRegions2400000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // Canonical spellings — case/whitespace insensitive, safe to re-run. - await queryRunner.query(` - UPDATE freight.companies - SET region = v.canonical - FROM (VALUES - ('addis ababa', 'Addis Ababa'), - ('addis abeba', 'Addis Ababa'), - ('addisababa', 'Addis Ababa'), - ('oromia', 'Oromia'), - ('oromoia', 'Oromia'), - ('oromiya', 'Oromia'), - ('amhara', 'Amhara'), - ('somali', 'Somali'), - ('afar', 'Afar'), - ('tigray', 'Tigray'), - ('tigrai', 'Tigray'), - ('sidama', 'Sidama'), - ('harari', 'Harari'), - ('gambela', 'Gambela'), - ('gambella', 'Gambela'), - ('dire dawa', 'Dire Dawa'), - ('benishangul-gumuz', 'Benishangul-Gumuz'), - ('benishangul gumuz', 'Benishangul-Gumuz'), - ('central ethiopia', 'Central Ethiopia'), - ('south ethiopia', 'South Ethiopia') - ) AS v(variant, canonical) - WHERE freight.companies.region IS NOT NULL - AND lower(regexp_replace(btrim(freight.companies.region), '\\s+', ' ', 'g')) = v.variant - AND freight.companies.region <> v.canonical - `); - - // Anything still outside the canonical set is unresolvable — null it. - await queryRunner.query(` - UPDATE freight.companies - SET region = NULL - WHERE region IS NOT NULL - AND region <> '' - AND region NOT IN ( - 'Addis Ababa','Afar','Amhara','Benishangul-Gumuz','Central Ethiopia', - 'Dire Dawa','Gambela','Harari','Oromia','Sidama','Somali', - 'South Ethiopia','South West Ethiopia Peoples''','Tigray' - ) - `); - - // Normalize empty string to NULL so "unset" has one representation. - await queryRunner.query(` - UPDATE freight.companies SET region = NULL WHERE region = '' - `); - } - - public async down(): Promise { - // Irreversible by design: the original free-text values are not retained - // anywhere, so there is nothing to restore. Rolling back the code is safe — - // the column is still a nullable varchar(100) and accepts free text again. - } -} diff --git a/apps/edr-freight-api/src/migrations/2410000000000-DropBookingApprovalWidenRoles.ts b/apps/edr-freight-api/src/migrations/2410000000000-DropBookingApprovalWidenRoles.ts deleted file mode 100644 index 1b4674b05..000000000 --- a/apps/edr-freight-api/src/migrations/2410000000000-DropBookingApprovalWidenRoles.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Bookings no longer run an approval chain — accepting an intake approves the - * booking outright and generates its contract. The approval chain is now a - * contract-only concern, so `freight.approval_rules` is read by contracts alone. - * - * Also widens the role columns: chain steps now reference IAM position-type - * keys (`iam.position_types.key`), and real keys run past the old varchar(30) - * (e.g. '-marketing-manager-/-general-manager' is 38 chars), which would fail - * on insert. - */ -export class DropBookingApprovalWidenRoles2410000000000 - implements MigrationInterface -{ - name = 'DropBookingApprovalWidenRoles2410000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP TABLE IF EXISTS freight.booking_approval_step;`, - ); - - for (const [table, column] of [ - ['approval_rules', 'required_role'], - ['approval_rules', 'blocks_role'], - ['contract_approval_steps', 'required_role'], - ['contract_approval_steps', 'blocks_role'], - ] as const) { - await queryRunner.query( - `ALTER TABLE freight.${table} ALTER COLUMN ${column} TYPE varchar(64);`, - ); - } - } - - /** - * No-op: the booking approval chain is retired, so re-creating the table - * would leave dead schema behind. Narrowing the role columns again would - * truncate any position-type key already stored. - */ - public async down(): Promise { - // intentionally empty - } -} diff --git a/apps/edr-freight-api/src/migrations/2420000000000-CreateContractDocumentRevisions.ts b/apps/edr-freight-api/src/migrations/2420000000000-CreateContractDocumentRevisions.ts deleted file mode 100644 index db06b2ba2..000000000 --- a/apps/edr-freight-api/src/migrations/2420000000000-CreateContractDocumentRevisions.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Audit trail for contract document edits. The document stays editable through - * the whole approval chain (each approver may edit on their turn), so the - * contract itself only ever holds the current snapshot — this table records who - * changed which article, and when. - */ -export class CreateContractDocumentRevisions2420000000000 - implements MigrationInterface -{ - name = 'CreateContractDocumentRevisions2420000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.contract_document_revisions ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - contract_id uuid NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE, - actor_id uuid, - actor_role varchar(64), - step_id uuid, - summary varchar(255), - changes jsonb NOT NULL DEFAULT '[]'::jsonb - ); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS idx_contract_document_revisions_contract - ON freight.contract_document_revisions (contract_id, created_at DESC); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP TABLE IF EXISTS freight.contract_document_revisions;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2430000000000-AddFileReviewStatus.ts b/apps/edr-freight-api/src/migrations/2430000000000-AddFileReviewStatus.ts deleted file mode 100644 index 833d49fea..000000000 --- a/apps/edr-freight-api/src/migrations/2430000000000-AddFileReviewStatus.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Per-document review state, so a backoffice reviewer can request a correction - * on one specific onboarding document instead of rejecting the whole role. - * - * Until now `freight.files` carried no status at all: the `pending_add` / - * `pending_remove` badges the portal shows are derived by diffing live rows - * against an open company change request, which says nothing about whether a - * reviewer is happy with a given document. `review_status` is that missing - * verdict — NULL means never reviewed, which is the state every existing row - * correctly starts in, so no backfill is needed. - * - * The partial index serves the approval gate, which asks "does this company (or - * profile) still have any document with an open change request?" on every - * role-status write. - */ -export class AddFileReviewStatus2430000000000 implements MigrationInterface { - name = 'AddFileReviewStatus2430000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.files - ADD COLUMN IF NOT EXISTS review_status varchar(32) NULL, - ADD COLUMN IF NOT EXISTS review_note text NULL, - ADD COLUMN IF NOT EXISTS reviewed_by uuid NULL, - ADD COLUMN IF NOT EXISTS reviewed_at timestamptz NULL - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_files_open_change_request" - ON freight.files (resource, resource_id) - WHERE review_status = 'change_requested' AND deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight."IDX_files_open_change_request"`, - ); - await queryRunner.query(` - ALTER TABLE freight.files - DROP COLUMN IF EXISTS review_status, - DROP COLUMN IF EXISTS review_note, - DROP COLUMN IF EXISTS reviewed_by, - DROP COLUMN IF EXISTS reviewed_at - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2430000000000-UniqueLocomotiveName.ts b/apps/edr-freight-api/src/migrations/2430000000000-UniqueLocomotiveName.ts deleted file mode 100644 index 5468e2207..000000000 --- a/apps/edr-freight-api/src/migrations/2430000000000-UniqueLocomotiveName.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Locomotive names must be unique so staff can identify a unit by name alone - * (the card view leads with `name`, falling back to `code`). Uniqueness is: - * - * - case/whitespace-insensitive — "MTL1", "mtl1" and " MTL1 " are one name; - * - scoped to live rows — a decommissioned (soft-deleted) locomotive must not - * hold its name hostage, matching how the fleet reuses yard codes; - * - skipped for blank names — `name` stays optional, and NULL/'' rows are - * excluded rather than colliding with each other. - * - * A partial expression index gives all three; a plain UNIQUE column cannot. - */ -export class UniqueLocomotiveName2430000000000 implements MigrationInterface { - name = 'UniqueLocomotiveName2430000000000'; - - public async up(queryRunner: QueryRunner): Promise { - // Pre-existing duplicates would abort CREATE UNIQUE INDEX. Suffix every - // copy after the oldest (…-2, …-3) so the index can build; the oldest row - // keeps the original name. Deterministic on created_at, then id. - await queryRunner.query(` - WITH ranked AS ( - SELECT - id, - name, - row_number() OVER ( - PARTITION BY lower(btrim(name)) - ORDER BY created_at, id - ) AS rn - FROM "freight"."locomotives" - WHERE deleted_at IS NULL - AND name IS NOT NULL - AND btrim(name) <> '' - ) - UPDATE "freight"."locomotives" AS l - SET name = btrim(ranked.name) || '-' || ranked.rn - FROM ranked - WHERE l.id = ranked.id - AND ranked.rn > 1 - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_locomotives_name_active" - ON "freight"."locomotives" (lower(btrim("name"))) - WHERE "deleted_at" IS NULL - AND "name" IS NOT NULL - AND btrim("name") <> '' - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS "freight"."UQ_locomotives_name_active"`, - ); - // The de-duplicating renames are not reversed: the original names are no - // longer recoverable, and restoring them would re-introduce the conflict. - } -} diff --git a/apps/edr-freight-api/src/migrations/2440000000000-AddHandoverEdrAssignment.ts b/apps/edr-freight-api/src/migrations/2440000000000-AddHandoverEdrAssignment.ts deleted file mode 100644 index c8f48af05..000000000 --- a/apps/edr-freight-api/src/migrations/2440000000000-AddHandoverEdrAssignment.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Per-truck EDR last-mile handovers. `truck_assignment_id` FKs - * customer_truck_assignments (self-haul only), so EDR trucks need their own - * link to the last-mile vehicle assignment that hauled the goods. Generated - * when the EDR truck exits the warehouse (with its exit paper) and signed by - * the customer in the portal — one per truck, or booking-level (both ids null) - * when the truck cannot be resolved. - */ -export class AddHandoverEdrAssignment2440000000000 implements MigrationInterface { - name = 'AddHandoverEdrAssignment2440000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.booking_handovers - ADD COLUMN IF NOT EXISTS edr_assignment_id uuid - REFERENCES freight.last_mile_vehicle_assignments(id) ON DELETE SET NULL; - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_booking_handovers_booking_edr_truck" - ON freight.booking_handovers (booking_id, edr_assignment_id) - WHERE deleted_at IS NULL AND edr_assignment_id IS NOT NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight."UQ_booking_handovers_booking_edr_truck";`, - ); - await queryRunner.query( - `ALTER TABLE freight.booking_handovers DROP COLUMN IF EXISTS edr_assignment_id;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2450000000000-DropActiveProfileTypeFromExternalProfiles.ts b/apps/edr-freight-api/src/migrations/2450000000000-DropActiveProfileTypeFromExternalProfiles.ts deleted file mode 100644 index d8119930f..000000000 --- a/apps/edr-freight-api/src/migrations/2450000000000-DropActiveProfileTypeFromExternalProfiles.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Drop the `active_profile_type` "active mode" column. A booking/contract now - * resolves its company_profile from the trade direction at creation time (with - * a forwarder passing an explicit companyProfileId), so no per-user active mode - * is stored. `onboarding_step` / `onboarding_completed` are unaffected. - */ -export class DropActiveProfileTypeFromExternalProfiles2450000000000 - implements MigrationInterface -{ - name = 'DropActiveProfileTypeFromExternalProfiles2450000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.external_profiles - DROP COLUMN IF EXISTS active_profile_type; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.external_profiles - ADD COLUMN IF NOT EXISTS active_profile_type varchar(32); - `); - // Rebuild the mode the same way the original column was backfilled: - // importer first, then exporter, then whichever profile the company has. - await queryRunner.query(` - UPDATE freight.external_profiles ep - SET active_profile_type = cp.type - FROM ( - SELECT DISTINCT ON (company_id) company_id, type - FROM freight.company_profiles - ORDER BY company_id, - CASE type - WHEN 'importer' THEN 0 - WHEN 'exporter' THEN 1 - ELSE 2 - END - ) cp - WHERE ep.company_id = cp.company_id - AND ep.active_profile_type IS NULL; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2460000000000-AddCacBankPaymentMethod.ts b/apps/edr-freight-api/src/migrations/2460000000000-AddCacBankPaymentMethod.ts deleted file mode 100644 index e6cfe70c3..000000000 --- a/apps/edr-freight-api/src/migrations/2460000000000-AddCacBankPaymentMethod.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddCacBankPaymentMethod2460000000000 implements MigrationInterface { - name = "AddCacBankPaymentMethod2460000000000"; - - public async up(queryRunner: QueryRunner): Promise { - // The entity + frontend already list 'cac-bank' as a valid method, but the - // DB enum was never extended. Filtering payments by 'cac-bank' cast the - // literal to the enum and errored (invalid input value for enum). EDRFREIGHT-301. - await queryRunner.query(`ALTER TYPE freight.payments_method_enum ADD VALUE IF NOT EXISTS 'cac-bank';`); - } - - public async down(_queryRunner: QueryRunner): Promise { - // PostgreSQL does not support removing enum values directly. - // To roll back, recreate the type without the added value and update the column. - } -} diff --git a/apps/edr-freight-api/src/migrations/2470000000000-AddAcquisitionItemName.ts b/apps/edr-freight-api/src/migrations/2470000000000-AddAcquisitionItemName.ts deleted file mode 100644 index fadacda69..000000000 --- a/apps/edr-freight-api/src/migrations/2470000000000-AddAcquisitionItemName.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Acquisitions describe WHAT was acquired (vehicle, parts, equipment…) — the - * vehicle link is optional and only for acquisitions that ARE a fleet vehicle. - */ -export class AddAcquisitionItemName2470000000000 implements MigrationInterface { - name = 'AddAcquisitionItemName2470000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.asset_acquisitions - ADD COLUMN IF NOT EXISTS item_name varchar(200) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.asset_acquisitions - DROP COLUMN IF EXISTS item_name - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2480000000000-AddMaintenanceDueNotifiedAt.ts b/apps/edr-freight-api/src/migrations/2480000000000-AddMaintenanceDueNotifiedAt.ts deleted file mode 100644 index 61431c5bc..000000000 --- a/apps/edr-freight-api/src/migrations/2480000000000-AddMaintenanceDueNotifiedAt.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Dedup stamp for the km/date-due maintenance alert — without it the daily - * cron would re-notify every day a schedule stays due. - */ -export class AddMaintenanceDueNotifiedAt2480000000000 implements MigrationInterface { - name = 'AddMaintenanceDueNotifiedAt2480000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.maintenance_schedules - ADD COLUMN IF NOT EXISTS due_notified_at timestamptz NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.maintenance_schedules DROP COLUMN IF EXISTS due_notified_at - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2800000000000-AddMaintenanceIntervals.ts b/apps/edr-freight-api/src/migrations/2800000000000-AddMaintenanceIntervals.ts deleted file mode 100644 index f4f125eaf..000000000 --- a/apps/edr-freight-api/src/migrations/2800000000000-AddMaintenanceIntervals.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * KM-based maintenance scheduling: per-vehicle service intervals (by km - * and/or days) driving the maintenance due engine. Raw schema-qualified SQL — - * the builder API resolved bare table names against the default schema and - * failed on boot ("Table maintenance_intervals does not exist"). - */ -export class AddMaintenanceIntervals2800000000000 implements MigrationInterface { - name = 'AddMaintenanceIntervals2800000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.maintenance_intervals ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - vehicle_id uuid NOT NULL REFERENCES freight.vehicles(id) ON DELETE CASCADE, - maintenance_type varchar NOT NULL, - interval_km numeric(14,2), - interval_days integer, - description text, - is_active boolean NOT NULL DEFAULT true, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ); - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_maintenance_intervals_vehicle_type" - ON freight.maintenance_intervals (vehicle_id, maintenance_type); - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_maintenance_intervals_vehicle_type" - ON freight.maintenance_intervals (vehicle_id, maintenance_type); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.maintenance_intervals;`); - } -} diff --git a/apps/edr-freight-api/src/migrations/2800000000001-AddSignatureToHandover.ts b/apps/edr-freight-api/src/migrations/2800000000001-AddSignatureToHandover.ts deleted file mode 100644 index 679846484..000000000 --- a/apps/edr-freight-api/src/migrations/2800000000001-AddSignatureToHandover.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Persist the signer's saved-signature image on the handover record, so the - * signed handover document can render the actual signature (not just the - * typed name) — parity with the booking-contract signing flow. - */ -export class AddSignatureToHandover2800000000001 implements MigrationInterface { - name = 'AddSignatureToHandover2800000000001'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.booking_handovers ADD COLUMN IF NOT EXISTS signature_image_url text;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.booking_handovers DROP COLUMN IF EXISTS signature_image_url;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2810000000000-AddMaintenanceServiceItem.ts b/apps/edr-freight-api/src/migrations/2810000000000-AddMaintenanceServiceItem.ts deleted file mode 100644 index 983aa6e64..000000000 --- a/apps/edr-freight-api/src/migrations/2810000000000-AddMaintenanceServiceItem.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Named service items for KM-based maintenance ("oil change", "tires", …). - * The coarse maintenance_type enum (PREVENTIVE/…) allowed only one interval - * per type per vehicle, so oil and tire intervals could not coexist. Interval - * identity becomes (vehicle, maintenance_type, service_item); schedules carry - * the item so completion re-finds the right interval for auto-scheduling. - */ -export class AddMaintenanceServiceItem2810000000000 implements MigrationInterface { - name = 'AddMaintenanceServiceItem2810000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.maintenance_intervals ADD COLUMN IF NOT EXISTS service_item varchar(120);`, - ); - await queryRunner.query( - `ALTER TABLE freight.maintenance_schedules ADD COLUMN IF NOT EXISTS service_item varchar(120);`, - ); - // Re-key interval uniqueness on (vehicle, type, item). COALESCE folds the - // item-less legacy rows into one slot; soft-deleted rows are ignored. - await queryRunner.query( - `DROP INDEX IF EXISTS freight."UQ_maintenance_intervals_vehicle_type";`, - ); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_maintenance_intervals_vehicle_type_item" - ON freight.maintenance_intervals (vehicle_id, maintenance_type, COALESCE(service_item, '')) - WHERE deleted_at IS NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight."UQ_maintenance_intervals_vehicle_type_item";`, - ); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_maintenance_intervals_vehicle_type" - ON freight.maintenance_intervals (vehicle_id, maintenance_type); - `); - await queryRunner.query( - `ALTER TABLE freight.maintenance_schedules DROP COLUMN IF EXISTS service_item;`, - ); - await queryRunner.query( - `ALTER TABLE freight.maintenance_intervals DROP COLUMN IF EXISTS service_item;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2820000000000-AddMileTonsQuantity.ts b/apps/edr-freight-api/src/migrations/2820000000000-AddMileTonsQuantity.ts deleted file mode 100644 index e1e02c418..000000000 --- a/apps/edr-freight-api/src/migrations/2820000000000-AddMileTonsQuantity.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Bulk tonnage at assignment time. First-mile trucks and export self-haul - * trucks carry a planned load (tonnes + optional item count) so bulk bookings - * draw down as vehicles are assigned — not only at the weighbridge. - */ -export class AddMileTonsQuantity2820000000000 implements MigrationInterface { - name = 'AddMileTonsQuantity2820000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.first_mile_vehicle_assignments ADD COLUMN IF NOT EXISTS tons numeric(14,3);`, - ); - await queryRunner.query( - `ALTER TABLE freight.first_mile_vehicle_assignments ADD COLUMN IF NOT EXISTS quantity integer;`, - ); - await queryRunner.query( - `ALTER TABLE freight.customer_truck_assignments ADD COLUMN IF NOT EXISTS planned_tons numeric(14,3);`, - ); - await queryRunner.query( - `ALTER TABLE freight.customer_truck_assignments ADD COLUMN IF NOT EXISTS planned_quantity integer;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.customer_truck_assignments DROP COLUMN IF EXISTS planned_quantity;`, - ); - await queryRunner.query( - `ALTER TABLE freight.customer_truck_assignments DROP COLUMN IF EXISTS planned_tons;`, - ); - await queryRunner.query( - `ALTER TABLE freight.first_mile_vehicle_assignments DROP COLUMN IF EXISTS quantity;`, - ); - await queryRunner.query( - `ALTER TABLE freight.first_mile_vehicle_assignments DROP COLUMN IF EXISTS tons;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2820000000000-CustomsClearanceRouteScope.ts b/apps/edr-freight-api/src/migrations/2820000000000-CustomsClearanceRouteScope.ts deleted file mode 100644 index 17902ff0d..000000000 --- a/apps/edr-freight-api/src/migrations/2820000000000-CustomsClearanceRouteScope.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Scope the customs clearance service fee to a direction + route. - * - * The fee was a single global flat rate; the business sells it per lane — - * "import clearance, Djibouti → Adama, 300 USD". CUSTOMS_CLEARANCE rates now - * carry trade_direction + the yard pair, and contract pricing matches on them - * strictly (no route-less fallback). - * - * Existing route-less clearance rates cannot be backfilled (no way to know - * which lane each was meant for) — retired exactly like the base-freight - * retirement in AddRateYardScope: SUPERSEDED + soft-deleted, kept for - * snapshot history. - */ -export class CustomsClearanceRouteScope2820000000000 implements MigrationInterface { - name = 'CustomsClearanceRouteScope2820000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.rates - SET status = 'SUPERSEDED', - deleted_at = now(), - updated_at = now() - WHERE deleted_at IS NULL - AND rate_type = 'CUSTOMS_CLEARANCE' - AND (origin_yard_id IS NULL OR destination_yard_id IS NULL); - `); - - await queryRunner.query( - `ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope";`, - ); - await queryRunner.query(` - ALTER TABLE freight.rates - ADD CONSTRAINT "CK_rates_yard_scope" CHECK ( - deleted_at IS NOT NULL - OR status = 'SUPERSEDED' - OR CASE - WHEN ("trigger" = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY')) - OR "trigger" = 'CUSTOMS_CLEARANCE' - THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL - ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL - END - ); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - // Retired rates stay retired (their lanes were never recorded); down only - // restores the pre-customs constraint shape. - await queryRunner.query( - `ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope";`, - ); - await queryRunner.query(` - ALTER TABLE freight.rates - ADD CONSTRAINT "CK_rates_yard_scope" CHECK ( - deleted_at IS NOT NULL - OR status = 'SUPERSEDED' - OR CASE - WHEN "trigger" = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY') - THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL - ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL - END - ); - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2830000000000-ReturnSurchargeRouteScope.ts b/apps/edr-freight-api/src/migrations/2830000000000-ReturnSurchargeRouteScope.ts deleted file mode 100644 index 26c512cfc..000000000 --- a/apps/edr-freight-api/src/migrations/2830000000000-ReturnSurchargeRouteScope.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Scope the empty-container return surcharge to a direction + route + - * container type, like base freight (import-only for now — the box only goes - * back to the port on imports). - * - * Existing route-less RETURN_SURCHARGE rates cannot be backfilled — retired - * (SUPERSEDED + soft-deleted) exactly like base freight and customs clearance - * were, kept readable for snapshot history. Route-scoped replacements must be - * re-entered; a booking that asks for return with no matching rate hard-blocks. - */ -export class ReturnSurchargeRouteScope2830000000000 implements MigrationInterface { - name = 'ReturnSurchargeRouteScope2830000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.rates - SET status = 'SUPERSEDED', - deleted_at = now(), - updated_at = now() - WHERE deleted_at IS NULL - AND rate_type = 'RETURN_SURCHARGE' - AND (origin_yard_id IS NULL OR destination_yard_id IS NULL); - `); - - await queryRunner.query( - `ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope";`, - ); - await queryRunner.query(` - ALTER TABLE freight.rates - ADD CONSTRAINT "CK_rates_yard_scope" CHECK ( - deleted_at IS NOT NULL - OR status = 'SUPERSEDED' - OR CASE - WHEN ("trigger" = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY')) - OR "trigger" IN ('CUSTOMS_CLEARANCE', 'WITH_RETURN') - THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL - ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL - END - ); - `); - } - - public async down(queryRunner: QueryRunner): Promise { - // Retired rates stay retired; down only restores the customs-era shape. - await queryRunner.query( - `ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope";`, - ); - await queryRunner.query(` - ALTER TABLE freight.rates - ADD CONSTRAINT "CK_rates_yard_scope" CHECK ( - deleted_at IS NOT NULL - OR status = 'SUPERSEDED' - OR CASE - WHEN ("trigger" = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY')) - OR "trigger" = 'CUSTOMS_CLEARANCE' - THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL - ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL - END - ); - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2840000000000-AddTruckTypes.ts b/apps/edr-freight-api/src/migrations/2840000000000-AddTruckTypes.ts deleted file mode 100644 index fb22cbee9..000000000 --- a/apps/edr-freight-api/src/migrations/2840000000000-AddTruckTypes.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Truck types become back-office data instead of a hardcoded `VehicleType` enum, - * so EDR can add a configuration without a code change. - * - * `vehicles.vehicle_type` is deliberately LEFT IN PLACE as a denormalised code. - * Truck-detention billing groups trucks with raw SQL over that column - * (`SELECT v.vehicle_type ... GROUP BY`, warehouse-fee.service.ts) and matches - * the result against `warehouse_fee_rules.vehicle_type`. Swapping it for the FK - * outright would silently drop detention charges, so the FK is additive and the - * service writes the type's code through on every save. - * - * Raw SQL, `freight.`-qualified, IF NOT EXISTS throughout — the TypeORM builder - * API resolves bare names against `public` and crash-loops boot. - */ -export class AddTruckTypes2840000000000 implements MigrationInterface { - name = "AddTruckTypes2840000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.truck_types ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - code varchar(32) NOT NULL, - name varchar(100) NOT NULL, - capacity_tons numeric(10,3), - has_trailer boolean NOT NULL DEFAULT false, - description text, - is_active boolean NOT NULL DEFAULT true, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS ux_truck_types_code - ON freight.truck_types (code) - `); - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS ix_truck_types_is_active - ON freight.truck_types (is_active) - `); - - // Seed one row per legacy enum value so vehicles already carrying that code - // keep resolving, plus CASONI as the first rigid (no-trailer) configuration. - // has_trailer is true only for the articulated configurations. - await queryRunner.query(` - INSERT INTO freight.truck_types (code, name, has_trailer) - VALUES - ('TRUCK', 'Truck', true), - ('TRAILER', 'Trailer', true), - ('TANKER', 'Tanker', true), - ('FLATBED', 'Flatbed', true), - ('VAN', 'Van', false), - ('CAR', 'Car', false), - ('BUS', 'Bus', false), - ('CASONI', 'Casoni (rigid, no trailer)', false) - ON CONFLICT (code) DO NOTHING - `); - - await queryRunner.query(` - ALTER TABLE freight.vehicles - ADD COLUMN IF NOT EXISTS truck_type_id uuid - `); - - // Separate DO block: ADD CONSTRAINT has no IF NOT EXISTS in Postgres. - await queryRunner.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'fk_vehicles_truck_type' - ) THEN - ALTER TABLE freight.vehicles - ADD CONSTRAINT fk_vehicles_truck_type - FOREIGN KEY (truck_type_id) REFERENCES freight.truck_types (id) - ON DELETE SET NULL; - END IF; - END $$ - `); - - // Backfill the FK from the code already stored on each vehicle. - await queryRunner.query(` - UPDATE freight.vehicles v - SET truck_type_id = t.id - FROM freight.truck_types t - WHERE v.truck_type_id IS NULL - AND upper(trim(v.vehicle_type)) = t.code - `); - - // Truck-type codes are varchar(32); the fee-rule column they are matched - // against was varchar(20) and would truncate/reject longer codes. - await queryRunner.query(` - ALTER TABLE freight.warehouse_fee_rules - ALTER COLUMN vehicle_type TYPE varchar(32) - `); - - // A VIN identifies exactly one vehicle worldwide. Partial index so the many - // existing rows without a VIN do not collide. - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS ux_vehicles_vin - ON freight.vehicles (vin) - WHERE vin IS NOT NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight.ux_vehicles_vin`); - await queryRunner.query(` - ALTER TABLE freight.vehicles - DROP CONSTRAINT IF EXISTS fk_vehicles_truck_type - `); - await queryRunner.query(` - ALTER TABLE freight.vehicles - DROP COLUMN IF EXISTS truck_type_id - `); - await queryRunner.query(`DROP TABLE IF EXISTS freight.truck_types`); - // warehouse_fee_rules.vehicle_type is left widened: narrowing it back would - // fail on any row that stored a code longer than 20 characters. - } -} diff --git a/apps/edr-freight-api/src/migrations/2850000000000-AddBookingDoubleHandling.ts b/apps/edr-freight-api/src/migrations/2850000000000-AddBookingDoubleHandling.ts deleted file mode 100644 index be98729e7..000000000 --- a/apps/edr-freight-api/src/migrations/2850000000000-AddBookingDoubleHandling.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Double handling becomes an explicit per-booking decision instead of an - * implicit "every import" charge. Warehouse staff record Yes/No after - * unloading (whether the goods actually had to be re-handled); the - * DOUBLE_HANDLING_FEE rule only bills when the answer is Yes. - * - * NULL = not decided yet → no charge, and the UI shows "not set" so the - * operator is prompted. Existing rows stay NULL deliberately: back-billing a - * fee nobody confirmed would be wrong. - */ -export class AddBookingDoubleHandling2850000000000 implements MigrationInterface { - name = 'AddBookingDoubleHandling2850000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS double_handling boolean;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS double_handling_set_at timestamptz;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS double_handling_set_by varchar(160);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS double_handling_set_by;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS double_handling_set_at;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS double_handling;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2860000000000-AddPerTruckDetentionWindow.ts b/apps/edr-freight-api/src/migrations/2860000000000-AddPerTruckDetentionWindow.ts deleted file mode 100644 index 710ad12ad..000000000 --- a/apps/edr-freight-api/src/migrations/2860000000000-AddPerTruckDetentionWindow.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Per-truck detention clocks. Detention was timed once per last-mile leg - * (last_mile.arrived_at / delivered_at), so every truck on a multi-truck - * delivery shared one window and was billed identical days — wrong the moment - * two trucks arrive or return at different times. - * - * Deliberately NEW columns rather than reusing the existing per-truck - * arrived_at / departed_at on this table: those are WAREHOUSE gate-in/gate-out - * events stamped by release(), whereas detention runs from arrival at the - * DESTINATION until the truck is released/returned. - * - * Both nullable — a truck without its own window falls back to the leg-level - * timestamps, so legacy legs keep billing exactly as before. - */ -export class AddPerTruckDetentionWindow2860000000000 implements MigrationInterface { - name = 'AddPerTruckDetentionWindow2860000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.last_mile_vehicle_assignments - ADD COLUMN IF NOT EXISTS destination_arrived_at timestamptz, - ADD COLUMN IF NOT EXISTS returned_at timestamptz; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.last_mile_vehicle_assignments - DROP COLUMN IF EXISTS returned_at, - DROP COLUMN IF EXISTS destination_arrived_at; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2860000000000-DropClearanceFeePrepay.ts b/apps/edr-freight-api/src/migrations/2860000000000-DropClearanceFeePrepay.ts deleted file mode 100644 index 9fd854b94..000000000 --- a/apps/edr-freight-api/src/migrations/2860000000000-DropClearanceFeePrepay.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * The customs clearance service fee is no longer prepaid via its own - * `clearance`-source invoice — it is billed as a CUSTOMS_CLEARANCE line on the - * booking invoice, together with the freight (see BookingPricingService). - * - * - Contracts/bookings parked at the payment gate move straight to the - * document step (the gate no longer exists — nothing could ever pay them). - * - Open (unpaid) clearance invoices are expired; PAID ones stay as history. - * NOTE: a ONE_TIME customs contract that already PAID its prepaid fee but - * has not booked yet will be billed the fee again on its booking invoice — - * accepted for dev data; reverses the old AddClearanceFeePayment migration. - * - clearance_fee_paid_at columns are dropped from contracts and bookings. - */ -export class DropClearanceFeePrepay2860000000000 implements MigrationInterface { - name = 'DropClearanceFeePrepay2860000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.contracts - SET status = 'AWAITING_CLEARANCE_DOCUMENTS', updated_at = now() - WHERE status = 'AWAITING_CLEARANCE_PAYMENT'; - `); - await queryRunner.query(` - UPDATE freight.contracts - SET clearance_status = 'AWAITING_DOCUMENTS', updated_at = now() - WHERE clearance_status = 'AWAITING_PAYMENT'; - `); - await queryRunner.query(` - UPDATE freight.bookings - SET status = 'AWAITING_DOCUMENTS', updated_at = now() - WHERE status = 'AWAITING_CLEARANCE_PAYMENT'; - `); - await queryRunner.query(` - UPDATE freight.invoices - SET status = 'EXPIRED', updated_at = now() - WHERE source = 'clearance' - AND status IN ('DRAFT', 'ISSUED', 'PENDING', 'PARTIALLY_PAID', 'OVERDUE'); - `); - await queryRunner.query( - `ALTER TABLE freight.contracts DROP COLUMN IF EXISTS clearance_fee_paid_at;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS clearance_fee_paid_at;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - // Moved rows and expired invoices stay — only the columns come back. - await queryRunner.query( - `ALTER TABLE freight.contracts ADD COLUMN IF NOT EXISTS clearance_fee_paid_at timestamptz;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS clearance_fee_paid_at timestamptz;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2870000000000-CustomsClearancePerKind.ts b/apps/edr-freight-api/src/migrations/2870000000000-CustomsClearancePerKind.ts deleted file mode 100644 index be01e7825..000000000 --- a/apps/edr-freight-api/src/migrations/2870000000000-CustomsClearancePerKind.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Customs clearance fees are now sold per cargo kind: container fees name a - * container type (billed PER_CONTAINER / PER_WAGON), bulk fees carry no type - * (billed PER_TON / PER_WAGON). The old one-FLAT-fee-per-route shape cannot be - * mapped to a kind — retired (SUPERSEDED + soft-deleted) exactly like the - * base-freight and return-surcharge reshapes, kept readable for snapshot - * history. Per-kind replacements must be re-entered; a customs contract or - * booking without a matching fee hard-blocks. Contracts that already froze a - * FLAT snapshot keep billing it (legacy honoured at booking pricing). - */ -export class CustomsClearancePerKind2870000000000 implements MigrationInterface { - name = 'CustomsClearancePerKind2870000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.rates - SET status = 'SUPERSEDED', - deleted_at = now(), - updated_at = now() - WHERE deleted_at IS NULL - AND rate_type = 'CUSTOMS_CLEARANCE' - AND rate_unit = 'FLAT'; - `); - } - - public async down(): Promise { - // Retired rates stay retired — re-enter per-kind rates instead. - } -} diff --git a/apps/edr-freight-api/src/migrations/2880000000000-LashingPerKind.ts b/apps/edr-freight-api/src/migrations/2880000000000-LashingPerKind.ts deleted file mode 100644 index 9d311c60f..000000000 --- a/apps/edr-freight-api/src/migrations/2880000000000-LashingPerKind.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Lashing is now sold per cargo kind, like the customs clearance fee: - * container rates name a container type (PER_CONTAINER / PER_WAGON), bulk - * rates carry no type (PER_TON / PER_WAGON). The old flat-per-booking shape - * cannot be mapped to a kind — retired (SUPERSEDED + soft-deleted), kept - * readable for snapshot history. Per-kind replacements must be re-entered; - * an unconfigured lashing rate simply bills nothing (lenient, like - * hazard/reefer). Matched on trigger, not rate_type — CONSOLIDATION rates - * share the LASHING rate_type and must survive. - */ -export class LashingPerKind2880000000000 implements MigrationInterface { - name = 'LashingPerKind2880000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.rates - SET status = 'SUPERSEDED', - deleted_at = now(), - updated_at = now() - WHERE deleted_at IS NULL - AND "trigger" = 'LASHING' - AND rate_unit = 'FLAT'; - `); - } - - public async down(): Promise { - // Retired rates stay retired — re-enter per-kind rates instead. - } -} diff --git a/apps/edr-freight-api/src/migrations/2890000000000-LashingBulkOnlyPerDirection.ts b/apps/edr-freight-api/src/migrations/2890000000000-LashingBulkOnlyPerDirection.ts deleted file mode 100644 index 8904c6194..000000000 --- a/apps/edr-freight-api/src/migrations/2890000000000-LashingBulkOnlyPerDirection.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Lashing is now BULK-only and sold per trade direction (IMPORT / EXPORT), - * optionally narrowed to one leaf commodity. Rates that no longer fit — - * container-scoped, or carrying no direction — cannot be mapped and are - * retired (SUPERSEDED + soft-deleted), kept readable for snapshot history. - * Matched on trigger, not rate_type (CONSOLIDATION shares rate_type LASHING). - */ -export class LashingBulkOnlyPerDirection2890000000000 implements MigrationInterface { - name = 'LashingBulkOnlyPerDirection2890000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.rates - SET status = 'SUPERSEDED', - deleted_at = now(), - updated_at = now() - WHERE deleted_at IS NULL - AND "trigger" = 'LASHING' - AND (container_type_id IS NOT NULL - OR trade_direction IS NULL - OR trade_direction NOT IN ('IMPORT', 'EXPORT')); - `); - } - - public async down(): Promise { - // Retired rates stay retired — re-enter per-direction bulk rates instead. - } -} diff --git a/apps/edr-freight-api/src/migrations/2900000000000-LivestockPerItem.ts b/apps/edr-freight-api/src/migrations/2900000000000-LivestockPerItem.ts deleted file mode 100644 index ce05a7ce1..000000000 --- a/apps/edr-freight-api/src/migrations/2900000000000-LivestockPerItem.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Livestock is billed and counted per head, not per ton — line it up with the - * other break-bulk cargo types (Machinery, Truck, Automobile) so bulk - * storage/demurrage fees charge per item instead of per ton for it. - */ -export class LivestockPerItem2900000000000 implements MigrationInterface { - name = "LivestockPerItem2900000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.cargo_types - SET unit_of_measure = 'PER_ITEM' - WHERE code = 'LIVESTOCK' - AND unit_of_measure IS DISTINCT FROM 'PER_ITEM' - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE freight.cargo_types - SET unit_of_measure = 'PER_TON' - WHERE code = 'LIVESTOCK' - AND unit_of_measure IS DISTINCT FROM 'PER_TON' - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2910000000000-AddContractSignatureStamp.ts b/apps/edr-freight-api/src/migrations/2910000000000-AddContractSignatureStamp.ts deleted file mode 100644 index e28748cb7..000000000 --- a/apps/edr-freight-api/src/migrations/2910000000000-AddContractSignatureStamp.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Company stamp (seal) attached alongside the drawn signature, for both the - * client and the EDR side. Stored the same way the signature image is: a - * FileRecord on the contract (`resource: 'contracts'`, `code: 'stamp_'`) - * referenced from the signature row. - * - * Nullable — existing signature rows predate the stamp requirement. The - * "both stamps recorded" gate lives in ContractTransitionService.counterSign, - * not in a NOT NULL constraint, so historical rows stay readable. - */ -export class AddContractSignatureStamp2910000000000 implements MigrationInterface { - name = 'AddContractSignatureStamp2910000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_signatures ADD COLUMN IF NOT EXISTS stamp_file_id uuid;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_signatures DROP COLUMN IF EXISTS stamp_file_id;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2920000000000-AddRevisionActorName.ts b/apps/edr-freight-api/src/migrations/2920000000000-AddRevisionActorName.ts deleted file mode 100644 index 2263d78fc..000000000 --- a/apps/edr-freight-api/src/migrations/2920000000000-AddRevisionActorName.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Store WHO made a contract edit as a name, not just an id. Denormalised on - * purpose: an audit trail must still read correctly after the user is renamed, - * deactivated or deleted, and `iam.users` lives outside this module's schema. - */ -export class AddRevisionActorName2920000000000 implements MigrationInterface { - name = 'AddRevisionActorName2920000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_document_revisions ADD COLUMN IF NOT EXISTS actor_name varchar(200);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contract_document_revisions DROP COLUMN IF EXISTS actor_name;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2930000000000-AddWagonTransferPartialFulfilment.ts b/apps/edr-freight-api/src/migrations/2930000000000-AddWagonTransferPartialFulfilment.ts deleted file mode 100644 index a9cc5e74d..000000000 --- a/apps/edr-freight-api/src/migrations/2930000000000-AddWagonTransferPartialFulfilment.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Partial wagon-transfer fulfilment. - * - * A request for 50 wagons no longer has to be met in one go: OCC moves what the - * source yard can spare, whenever it can, and the request stays open until the - * full count is met (FULFILLED) or OCC ends it short (CLOSED_SHORT) so the - * requester can ask another yard for the rest. - * - * Existing rows are back-filled so history keeps reading correctly: a FULFILLED - * request delivered its whole quantity; anything else delivered nothing. - */ -export class AddWagonTransferPartialFulfilment2930000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagon_transfer_requests - ADD COLUMN IF NOT EXISTS fulfilled_quantity integer NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS closed_short_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS closed_short_by_user_id uuid NULL - `); - await queryRunner.query(` - UPDATE freight.wagon_transfer_requests - SET fulfilled_quantity = quantity - WHERE status = 'FULFILLED' - AND fulfilled_quantity = 0 - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.wagon_transfer_requests - DROP COLUMN IF EXISTS fulfilled_quantity, - DROP COLUMN IF EXISTS closed_short_at, - DROP COLUMN IF EXISTS closed_short_by_user_id - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2940000000000-AddFileVersionHistory.ts b/apps/edr-freight-api/src/migrations/2940000000000-AddFileVersionHistory.ts deleted file mode 100644 index ad9bae99c..000000000 --- a/apps/edr-freight-api/src/migrations/2940000000000-AddFileVersionHistory.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Keep every version of a stored document. - * - * Replacing a file used to DELETE the previous row outright, so a staff - * correction erased the customer's original upload with no trail. Superseded - * versions are now soft-deleted (already excluded from every read by TypeORM's - * soft-delete filter) and stamped with who replaced them and why, which is what - * the document's version history reads back. - */ -export class AddFileVersionHistory2940000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.files - ADD COLUMN IF NOT EXISTS replaced_by_user_id uuid NULL, - ADD COLUMN IF NOT EXISTS replace_reason text NULL - `); - // History reads walk one document's versions, deleted rows included. - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "IDX_files_version_history" - ON freight.files (resource, resource_id, code, created_at DESC) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_files_version_history"`); - await queryRunner.query(` - ALTER TABLE freight.files - DROP COLUMN IF EXISTS replaced_by_user_id, - DROP COLUMN IF EXISTS replace_reason - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2950000000000-AddTransitAssigneeHandshake.ts b/apps/edr-freight-api/src/migrations/2950000000000-AddTransitAssigneeHandshake.ts deleted file mode 100644 index bd9647d44..000000000 --- a/apps/edr-freight-api/src/migrations/2950000000000-AddTransitAssigneeHandshake.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Transit-assignee handshake before the customs declaration. - * - * GL Ethiopia must ask GL Djibouti who will handle the shipment in transit, and - * Djibouti answers with a name, before the declaration can be filed. The whole - * exchange lives on the clearance cycle so it repeats naturally with each cycle - * of a GENERAL contract. - */ -export class AddTransitAssigneeHandshake2950000000000 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.contract_clearance_cycles - ADD COLUMN IF NOT EXISTS transit_assignee_requested_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS transit_assignee_requested_by_user_id uuid NULL, - ADD COLUMN IF NOT EXISTS transit_assignee_request_note text NULL, - ADD COLUMN IF NOT EXISTS transit_assignee_name text NULL, - ADD COLUMN IF NOT EXISTS transit_assignee_assigned_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS transit_assignee_assigned_by_user_id uuid NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.contract_clearance_cycles - DROP COLUMN IF EXISTS transit_assignee_requested_at, - DROP COLUMN IF EXISTS transit_assignee_requested_by_user_id, - DROP COLUMN IF EXISTS transit_assignee_request_note, - DROP COLUMN IF EXISTS transit_assignee_name, - DROP COLUMN IF EXISTS transit_assignee_assigned_at, - DROP COLUMN IF EXISTS transit_assignee_assigned_by_user_id - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/2960000000000-AddContractHazardDeclaration.ts b/apps/edr-freight-api/src/migrations/2960000000000-AddContractHazardDeclaration.ts deleted file mode 100644 index 810f59a07..000000000 --- a/apps/edr-freight-api/src/migrations/2960000000000-AddContractHazardDeclaration.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Hazardous contracts now declare WHAT the dangerous good is, not just that it - * exists: the UN/ADR class (CLASS_1..CLASS_9) and the shipment's UN number. Both - * are captured in the portal alongside the hazard documents and reviewed by the - * two hazardous approval desks. - * - * Nullable — non-hazardous contracts leave both null, and contracts created - * before this change have no declaration to backfill. - */ -export class AddContractHazardDeclaration2960000000000 - implements MigrationInterface -{ - name = 'AddContractHazardDeclaration2960000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contracts ADD COLUMN IF NOT EXISTS hazard_class varchar(16);`, - ); - await queryRunner.query( - `ALTER TABLE freight.contracts ADD COLUMN IF NOT EXISTS un_number varchar(16);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contracts DROP COLUMN IF EXISTS un_number;`, - ); - await queryRunner.query( - `ALTER TABLE freight.contracts DROP COLUMN IF EXISTS hazard_class;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2970000000000-AddDoCollectionDates.ts b/apps/edr-freight-api/src/migrations/2970000000000-AddDoCollectionDates.ts deleted file mode 100644 index 90b3a9298..000000000 --- a/apps/edr-freight-api/src/migrations/2970000000000-AddDoCollectionDates.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Djibouti GL must record WHEN the vessel arrived and WHEN the Delivery Order - * was collected, not just attach the DO file. Both are mandatory on DO upload - * (enforced in the clearance services), so the columns are new and nullable — - * DOs uploaded before this change have no dates to backfill. - * - * `vessel_departure_date` is the EXPORT Release-Order date and stays as-is; the - * import arrival date gets its own column rather than overloading it. - */ -export class AddDoCollectionDates2970000000000 implements MigrationInterface { - name = 'AddDoCollectionDates2970000000000'; - - public async up(queryRunner: QueryRunner): Promise { - for (const table of [ - 'freight.contract_clearance_cycles', - 'freight.bookings', - ]) { - await queryRunner.query( - `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS vessel_arrival_date date;`, - ); - await queryRunner.query( - `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS do_collected_date date;`, - ); - } - } - - public async down(queryRunner: QueryRunner): Promise { - for (const table of [ - 'freight.contract_clearance_cycles', - 'freight.bookings', - ]) { - await queryRunner.query( - `ALTER TABLE ${table} DROP COLUMN IF EXISTS do_collected_date;`, - ); - await queryRunner.query( - `ALTER TABLE ${table} DROP COLUMN IF EXISTS vessel_arrival_date;`, - ); - } - } -} diff --git a/apps/edr-freight-api/src/migrations/2980000000000-AddBookingRequestCurrency.ts b/apps/edr-freight-api/src/migrations/2980000000000-AddBookingRequestCurrency.ts deleted file mode 100644 index 5f92cff68..000000000 --- a/apps/edr-freight-api/src/migrations/2980000000000-AddBookingRequestCurrency.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Currency moved from the contract to the shipment: a contract now quotes in - * USD and the customer picks the billing currency per booking. On a customs - * contract GL books on the customer's behalf, so the shipment request is where - * the customer states the currency — GL reads it when creating the booking. - * - * Nullable: requests submitted before this change fall back to the contract's - * own currency, which is exactly what their bookings already used. - */ -export class AddBookingRequestCurrency2980000000000 implements MigrationInterface { - name = 'AddBookingRequestCurrency2980000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.booking_requests ADD COLUMN IF NOT EXISTS payment_currency varchar(5);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.booking_requests DROP COLUMN IF EXISTS payment_currency;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/2990000000000-IndodeYardsAndCargoRouting.ts b/apps/edr-freight-api/src/migrations/2990000000000-IndodeYardsAndCargoRouting.ts deleted file mode 100644 index d89e0c04b..000000000 --- a/apps/edr-freight-api/src/migrations/2990000000000-IndodeYardsAndCargoRouting.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Indode's real 11-yard layout, plus the plumbing to auto-route a booking to - * the right yard by cargo type (and, for container yards, trade direction): - * - * - `warehouse_yards.direction` — IMPORT | EXPORT | BOTH | null. Only - * meaningful for CONTAINER_YARD, where import and export stacks are - * physically separate (Yard 5 vs Yard 6). Everything else takes cargo - * either way. A CONTAINER_YARD left at null/BOTH is a signal too: it means - * "not a customer cargo yard" — Yards 10/11 (service/equipment) are - * CONTAINER_YARD structurally but must never be offered for ordinary - * import/export cargo, so the frontend match requires an EXACT IMPORT/ - * EXPORT direction hit for container freight rather than treating BOTH as - * a wildcard. - * - `warehouse_yard_cargo_types` — which cargo types a yard accepts (mirrors - * the existing `cargo_type_wagon_types` join table). Empty = open to any - * cargo type of the yard's structural type (additive, never restrictive - * by default), so this cannot break a yard nobody has configured yet. - * - * Three cargo types didn't exist yet (Fertilizer, Coffee, Tea) — added here - * so Yards 1 and 9 have a real mapping ready for when they reopen. - */ -export class IndodeYardsAndCargoRouting2990000000000 implements MigrationInterface { - name = "IndodeYardsAndCargoRouting2990000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.warehouse_yards - ADD COLUMN IF NOT EXISTS direction varchar(10) - `); - - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.warehouse_yard_cargo_types ( - yard_id uuid NOT NULL REFERENCES freight.warehouse_yards (id) ON DELETE CASCADE, - cargo_type_id uuid NOT NULL REFERENCES freight.cargo_types (id) ON DELETE CASCADE, - PRIMARY KEY (yard_id, cargo_type_id) - ) - `); - - // New cargo types Indode's yard list names but the catalog didn't have yet. - await queryRunner.query(` - INSERT INTO freight.cargo_types (code, cargo_type_name, unit_of_measure, is_active) - VALUES - ('FERTILIZER', 'Fertilizer', 'PER_TON', true), - ('COFFEE', 'Coffee', 'PER_TON', true), - ('TEA', 'Tea', 'PER_TON', true) - ON CONFLICT (code) DO NOTHING - `); - - // The 11 real yards at Indode Open Warehouse (code 'IOW'). - await queryRunner.query(` - INSERT INTO freight.warehouse_yards - (warehouse_id, name, code, type, direction, status, is_active) - SELECT w.id, y.name, y.code, y.type, y.direction, y.status, y.status = 'ACTIVE' - FROM freight.warehouses w - CROSS JOIN (VALUES - ('Y1', 'Bagged Cargo Discharge - Fertilizer', 'BULK_YARD', NULL, 'INACTIVE'), - ('Y2', 'Break Bulk', 'GENERAL_CARGO_YARD', NULL, 'ACTIVE'), - ('Y3', 'Ro-Ro / Pac', 'GENERAL_CARGO_YARD', NULL, 'ACTIVE'), - ('Y4', 'Dry Bulk', 'BULK_YARD', NULL, 'INACTIVE'), - ('Y5', 'Container Terminal - Import (Stack Area)', 'CONTAINER_YARD', 'IMPORT', 'ACTIVE'), - ('Y6', 'Container Terminal - Export', 'CONTAINER_YARD', 'EXPORT', 'ACTIVE'), - ('Y7', 'Cold Chain', 'COLD_STORAGE_YARD', NULL, 'INACTIVE'), - ('Y8', 'Chemical', 'HAZARDOUS_YARD', NULL, 'INACTIVE'), - ('Y9', 'Coffee and Tea', 'GENERAL_CARGO_YARD', NULL, 'INACTIVE'), - ('Y10', 'Container Service Yard - Maintenance', 'CONTAINER_YARD', 'BOTH', 'ACTIVE'), - ('Y11', 'Equipment (Empty Container)', 'CONTAINER_YARD', 'BOTH', 'ACTIVE') - ) AS y(code, name, type, direction, status) - WHERE w.code = 'IOW' - ON CONFLICT (warehouse_id, code) DO NOTHING - `); - - // One default zone per new yard, matching its yard's type — every existing - // yard (CY-1, CY-A) already follows this one-zone-per-yard shape. - await queryRunner.query(` - INSERT INTO freight.warehouse_zones (yard_id, name, code, type, status, is_active) - SELECT y.id, y.name || ' Zone 1', 'Z1', - CASE y.type - WHEN 'CONTAINER_YARD' THEN 'CONTAINER_ZONE' - WHEN 'COLD_STORAGE_YARD' THEN 'COLD_STORAGE_ZONE' - WHEN 'HAZARDOUS_YARD' THEN 'HAZARDOUS_ZONE' - WHEN 'BULK_YARD' THEN 'BULK_ZONE' - ELSE 'GENERAL_CARGO_ZONE' - END, - y.status, y.status = 'ACTIVE' - FROM freight.warehouse_yards y - JOIN freight.warehouses w ON w.id = y.warehouse_id - WHERE w.code = 'IOW' AND y.code LIKE 'Y%' - ON CONFLICT (yard_id, code) DO NOTHING - `); - - // Cargo-type routing. Yards 5/6/10/11 (CONTAINER_YARD) are intentionally - // left with no rows — direction alone decides those, per the entity comment. - await queryRunner.query(` - INSERT INTO freight.warehouse_yard_cargo_types (yard_id, cargo_type_id) - SELECT y.id, ct.id - FROM freight.warehouses w - JOIN freight.warehouse_yards y ON y.warehouse_id = w.id - JOIN (VALUES - ('Y1', 'FERTILIZER'), - ('Y2', 'STEEL_BILLET'), ('Y2', 'PLASTIC_BARREL'), ('Y2', 'MACHINERY'), ('Y2', 'LIVESTOCK'), - ('Y3', 'AUTOMOBILE'), ('Y3', 'TRUCK'), - ('Y4', 'BARLY'), ('Y4', 'BEANS'), ('Y4', 'BULK'), ('Y4', 'CEREAL'), - ('Y4', 'EDIBLE_OIL'), ('Y4', 'RICE'), ('Y4', 'SUGAR'), ('Y4', 'WHEAT'), - ('Y7', 'PERISHABLE'), - ('Y9', 'COFFEE'), ('Y9', 'TEA') - ) AS m(yard_code, cargo_code) ON m.yard_code = y.code - JOIN freight.cargo_types ct ON ct.code = m.cargo_code - WHERE w.code = 'IOW' - ON CONFLICT (yard_id, cargo_type_id) DO NOTHING - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DELETE FROM freight.warehouse_zones z - USING freight.warehouse_yards y, freight.warehouses w - WHERE z.yard_id = y.id AND y.warehouse_id = w.id - AND w.code = 'IOW' AND y.code LIKE 'Y%' - `); - await queryRunner.query(` - DELETE FROM freight.warehouse_yards y - USING freight.warehouses w - WHERE y.warehouse_id = w.id AND w.code = 'IOW' AND y.code LIKE 'Y%' - `); - // Cargo types and the join table are left in place — other data may have - // started referencing them since; dropping columns/tables is not reversible - // once real rows exist, and leaving them is harmless. - } -} diff --git a/apps/edr-freight-api/src/migrations/3000000000000-AddContractSuspension.ts b/apps/edr-freight-api/src/migrations/3000000000000-AddContractSuspension.ts deleted file mode 100644 index 0e0484345..000000000 --- a/apps/edr-freight-api/src/migrations/3000000000000-AddContractSuspension.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Backoffice contract suspension (reversible freeze at any post-signature step) - * and customer-initiated contract cancellation. - * - * Only one new column is needed: the status to restore when the suspension is - * lifted. The reason and the actor already have a home — contract_review_notes - * rows with note_type SUSPENSION / SUSPENSION_LIFTED / CANCELLATION. - */ -export class AddContractSuspension3000000000000 implements MigrationInterface { - name = 'AddContractSuspension3000000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contracts ADD COLUMN IF NOT EXISTS status_before_suspension varchar(40);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contracts DROP COLUMN IF EXISTS status_before_suspension;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/3010000000000-AddBookingTransitAssignee.ts b/apps/edr-freight-api/src/migrations/3010000000000-AddBookingTransitAssignee.ts deleted file mode 100644 index 50fb9db2b..000000000 --- a/apps/edr-freight-api/src/migrations/3010000000000-AddBookingTransitAssignee.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Transit-assignee handshake on the SHIPMENT, not the contract. - * - * Clearance runs per booking now, so the ask GL Ethiopia raises before filing a - * customs declaration ("who handles this shipment in Djibouti?") and Djibouti's - * answer belong on the booking. The contract-cycle columns added by - * 2950000000000 stay for the legacy contract-level cycles. - */ -export class AddBookingTransitAssignee3010000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - ADD COLUMN IF NOT EXISTS transit_assignee_requested_at timestamptz NULL, - ADD COLUMN IF NOT EXISTS transit_assignee_request_note text NULL, - ADD COLUMN IF NOT EXISTS transit_assignee_name text NULL, - ADD COLUMN IF NOT EXISTS transit_assignee_assigned_at timestamptz NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.bookings - DROP COLUMN IF EXISTS transit_assignee_requested_at, - DROP COLUMN IF EXISTS transit_assignee_request_note, - DROP COLUMN IF EXISTS transit_assignee_name, - DROP COLUMN IF EXISTS transit_assignee_assigned_at - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/3020000000000-AddContractSubmittedAt.ts b/apps/edr-freight-api/src/migrations/3020000000000-AddContractSubmittedAt.ts deleted file mode 100644 index f2c579f6b..000000000 --- a/apps/edr-freight-api/src/migrations/3020000000000-AddContractSubmittedAt.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * A contract's `created_at` is the DRAFT row's insert time, not when the - * customer actually submitted it for review — a DRAFT can sit edited for days - * first. `submitted_at` is stamped by ContractTransitionService.submit / - * confirmSubmit so the history UI can show a real submission time. - */ -export class AddContractSubmittedAt3020000000000 implements MigrationInterface { - name = 'AddContractSubmittedAt3020000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contracts ADD COLUMN IF NOT EXISTS submitted_at timestamptz;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.contracts DROP COLUMN IF EXISTS submitted_at;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/3030000000000-AddGlExchangeDocumentFields.ts b/apps/edr-freight-api/src/migrations/3030000000000-AddGlExchangeDocumentFields.ts deleted file mode 100644 index 2958f5406..000000000 --- a/apps/edr-freight-api/src/migrations/3030000000000-AddGlExchangeDocumentFields.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * GL Ethiopia ↔ GL Djibouti document exchange. The documents are ordinary - * `freight.files` rows (resource `gl_exchange`), so they only need the metadata - * a free-form upload has and a catalog-driven one does not: the uploader's own - * title, who uploaded it (the only user allowed to change it afterwards) and - * whether the customer may see it in the portal. - */ -export class AddGlExchangeDocumentFields3030000000000 - implements MigrationInterface -{ - name = 'AddGlExchangeDocumentFields3030000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.files - ADD COLUMN IF NOT EXISTS title varchar(300), - ADD COLUMN IF NOT EXISTS visible_to_customer boolean NOT NULL DEFAULT false, - ADD COLUMN IF NOT EXISTS uploaded_by_user_id uuid, - ADD COLUMN IF NOT EXISTS uploaded_by_name varchar(200);`, - ); - // Every read of a thread is "all files of one resource" — index the pair. - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS idx_files_resource_lookup - ON freight.files (resource, resource_id);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `DROP INDEX IF EXISTS freight.idx_files_resource_lookup;`, - ); - await queryRunner.query( - `ALTER TABLE freight.files - DROP COLUMN IF EXISTS title, - DROP COLUMN IF EXISTS visible_to_customer, - DROP COLUMN IF EXISTS uploaded_by_user_id, - DROP COLUMN IF EXISTS uploaded_by_name;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/3040000000000-AddTransitAgents.ts b/apps/edr-freight-api/src/migrations/3040000000000-AddTransitAgents.ts deleted file mode 100644 index f587af9a7..000000000 --- a/apps/edr-freight-api/src/migrations/3040000000000-AddTransitAgents.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddTransitAgents3040000000000 implements MigrationInterface { - name = "AddTransitAgents3040000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS freight.transit_agents ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - name varchar(150) NOT NULL, - valid_from date NOT NULL, - valid_to date NOT NULL, - is_active boolean NOT NULL DEFAULT true, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz - ) - `); - - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS ix_transit_agents_is_active - ON freight.transit_agents (is_active) - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS freight.transit_agents`); - } -} diff --git a/apps/edr-freight-api/src/migrations/3050000000000-AddCbeBillPaymentMethod.ts b/apps/edr-freight-api/src/migrations/3050000000000-AddCbeBillPaymentMethod.ts deleted file mode 100644 index 449f3cebe..000000000 --- a/apps/edr-freight-api/src/migrations/3050000000000-AddCbeBillPaymentMethod.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddCbeBillPaymentMethod3050000000000 implements MigrationInterface { - name = "AddCbeBillPaymentMethod3050000000000"; - - public async up(queryRunner: QueryRunner): Promise { - // CBE Unified Bill Payment (docs/cbe/CBE_IMPLEMENTATION_PLAN.md §4.3) — lowercase-hyphen - // per the local convention (see 2460000000000-AddCacBankPaymentMethod). - await queryRunner.query(`ALTER TYPE freight.payments_method_enum ADD VALUE IF NOT EXISTS 'cbe-bill';`); - } - - public async down(_queryRunner: QueryRunner): Promise { - // PostgreSQL does not support removing enum values directly. - } -} diff --git a/apps/edr-freight-api/src/migrations/3050000000000-MergeDuplicateSebetaYards.ts b/apps/edr-freight-api/src/migrations/3050000000000-MergeDuplicateSebetaYards.ts deleted file mode 100644 index c8a2c5722..000000000 --- a/apps/edr-freight-api/src/migrations/3050000000000-MergeDuplicateSebetaYards.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Two active "Sebeta" yards existed (code LEGACY_DEST label "Sebeta", and code - * SEBETA label "sebeta") — rates and routes pointed at one or the other, so a - * rate configured against one never matched a contract routed via the other. - * Merge them: keep the row all rates/distances/facilities reference - * (LEGACY_DEST), repoint every yard reference from the duplicate to it, retire - * the duplicate, and give the survivor the clean SEBETA code. Then make - * duplicate active yard labels/codes impossible at the DB level. - */ -export class MergeDuplicateSebetaYards3050000000000 implements MigrationInterface { - name = "MergeDuplicateSebetaYards3050000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DO $$ - DECLARE - survivor uuid; - dupe uuid; - col record; - BEGIN - SELECT id INTO survivor FROM freight.yards - WHERE code = 'LEGACY_DEST' AND lower(trim(label)) = 'sebeta' AND deleted_at IS NULL; - SELECT id INTO dupe FROM freight.yards - WHERE code = 'SEBETA' AND deleted_at IS NULL; - IF survivor IS NULL OR dupe IS NULL OR survivor = dupe THEN - RETURN; - END IF; - - -- Every yard-referencing column in the schema, so rows created between - -- authoring and running this migration are repointed too. - FOR col IN - SELECT table_name, column_name FROM information_schema.columns - WHERE table_schema = 'freight' - AND table_name <> 'yards' - AND (column_name LIKE '%yard_id%' OR column_name LIKE '%station_id%') - LOOP - EXECUTE format( - 'UPDATE freight.%I SET %I = $1 WHERE %I = $2', - col.table_name, col.column_name, col.column_name - ) USING survivor, dupe; - END LOOP; - - UPDATE freight.yards - SET code = 'SEBETA@merged', label = 'sebeta@merged', deleted_at = now() - WHERE id = dupe; - UPDATE freight.yards SET code = 'SEBETA', label = 'Sebeta' WHERE id = survivor; - END $$; - `); - - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_yards_label_active" - ON freight.yards (lower(trim(label))) WHERE deleted_at IS NULL - `); - await queryRunner.query(` - CREATE UNIQUE INDEX IF NOT EXISTS "UQ_yards_code_active" - ON freight.yards (lower(trim(code))) WHERE deleted_at IS NULL - `); - } - - public async down(queryRunner: QueryRunner): Promise { - // Data repair — not reversible. The uniqueness indexes are the new invariant. - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_yards_label_active"`); - await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_yards_code_active"`); - } -} diff --git a/apps/edr-freight-api/src/migrations/3060000000000-AddExportPaymentWindow.ts b/apps/edr-freight-api/src/migrations/3060000000000-AddExportPaymentWindow.ts deleted file mode 100644 index c55745126..000000000 --- a/apps/edr-freight-api/src/migrations/3060000000000-AddExportPaymentWindow.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Export bookings get their own pay window, separately tunable from import: - * - global_rules.export_payment_window_minutes — global default for EXPORT - * (payment_window_minutes keeps governing IMPORT/DOMESTIC). - * - train_schedules.rule_payment_window_minutes — per-schedule override; until - * now the DTO accepted paymentWindowMinutes but only folded it into the - * reopen-delay sum, so the override never reached the actual pay window. - * - bookings.requested_train_schedule_id — the export train the customer picked - * at day-commit; pickExportSchedule honors it instead of earliest-first. - * - bookings.payment_reminder_sent_at — marks the one pre-deadline pay - * reminder so the 10s window tick doesn't re-send it. - */ -export class AddExportPaymentWindow3060000000000 implements MigrationInterface { - name = 'AddExportPaymentWindow3060000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.train_scheduling_global_rules ADD COLUMN IF NOT EXISTS export_payment_window_minutes int NOT NULL DEFAULT 60;`, - ); - await queryRunner.query( - `ALTER TABLE freight.train_schedules ADD COLUMN IF NOT EXISTS rule_payment_window_minutes int;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS requested_train_schedule_id uuid;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS payment_reminder_sent_at timestamptz;`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS payment_reminder_sent_at;`, - ); - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS requested_train_schedule_id;`, - ); - await queryRunner.query( - `ALTER TABLE freight.train_schedules DROP COLUMN IF EXISTS rule_payment_window_minutes;`, - ); - await queryRunner.query( - `ALTER TABLE freight.train_scheduling_global_rules DROP COLUMN IF EXISTS export_payment_window_minutes;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/3070000000000-AddBulkTotalWeightTons.ts b/apps/edr-freight-api/src/migrations/3070000000000-AddBulkTotalWeightTons.ts deleted file mode 100644 index f2c50ec8d..000000000 --- a/apps/edr-freight-api/src/migrations/3070000000000-AddBulkTotalWeightTons.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Break-bulk (PER_ITEM) bookings store their item count in - * cargo_total_weight_vgm, so the actual tonnage was never captured — wagon - * allocation divided an item COUNT by a tons capacity and under-allocated - * (400 machines ÷ 69T wagon read as 6 wagons instead of 12). New column holds - * the real total weight in tons for PER_ITEM cargo; null for PER_TON bulk and - * container bookings. - */ -export class AddBulkTotalWeightTons3070000000000 implements MigrationInterface { - name = 'AddBulkTotalWeightTons3070000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS bulk_total_weight_tons numeric(12,3);`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.bookings DROP COLUMN IF EXISTS bulk_total_weight_tons;`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/3080000000000-BackfillDireDawaMilestone.ts b/apps/edr-freight-api/src/migrations/3080000000000-BackfillDireDawaMilestone.ts deleted file mode 100644 index 78c44c0ce..000000000 --- a/apps/edr-freight-api/src/migrations/3080000000000-BackfillDireDawaMilestone.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Several DCT (DORALEH) → GMP (KALITY) routes are missing the Dire Dawa stop - * in their milestone list. The corridor budget builds its per-leg edges from - * route_milestones, so on those routes a DCT→Dire Dawa or Dire Dawa→GMP - * booking cannot resolve its own leg and conservatively occupies the WHOLE - * route — per-leg wagon reuse (a wagon freed at Dire Dawa reloading for GMP) - * silently degrades to train-wide accounting. - * - * Insert the Dire Dawa milestone at sequence 2 on every active DORALEH→KALITY - * route with a stop list that lacks it, shifting later stops down. Matched by - * yard CODE so the repair is portable across environments. Idempotent: routes - * already carrying Dire Dawa are untouched. - */ -export class BackfillDireDawaMilestone3080000000000 implements MigrationInterface { - name = "BackfillDireDawaMilestone3080000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DO $$ - DECLARE - dire uuid; - r record; - BEGIN - SELECT id INTO dire FROM freight.yards - WHERE code = 'DIRE_DAWA' AND deleted_at IS NULL; - IF dire IS NULL THEN - RETURN; - END IF; - - FOR r IN - SELECT rt.id - FROM freight.routes rt - JOIN freight.yards o ON o.id = rt.origin_yard_id AND o.code = 'DORALEH' - JOIN freight.yards d ON d.id = rt.destination_yard_id AND d.code = 'KALITY' - WHERE rt.deleted_at IS NULL - AND EXISTS (SELECT 1 FROM freight.route_milestones m - WHERE m.route_id = rt.id AND m.deleted_at IS NULL) - AND NOT EXISTS (SELECT 1 FROM freight.route_milestones m - WHERE m.route_id = rt.id AND m.yard_id = dire - AND m.deleted_at IS NULL) - LOOP - -- Two-phase shift: uq_route_milestones_route_sequence isn't deferrable, - -- so a direct +1 UPDATE can collide mid-scan (seq 2 -> 3 while seq 3 still live). - -- Route through negative sequence_no first to avoid any interim collision. - -- Soft-deleted rows shift too: the constraint counts them, so a dead row - -- left at a target sequence would still collide. - UPDATE freight.route_milestones - SET sequence_no = -sequence_no - WHERE route_id = r.id AND sequence_no >= 2; - UPDATE freight.route_milestones - SET sequence_no = -sequence_no + 1 - WHERE route_id = r.id AND sequence_no < 0; - INSERT INTO freight.route_milestones (route_id, yard_id, sequence_no) - VALUES (r.id, dire, 2); - END LOOP; - END $$; - `); - } - - public async down(): Promise { - // Data repair — not reversible. - } -} diff --git a/apps/edr-freight-api/src/migrations/3090000000000-AddSavedSignatureStamp.ts b/apps/edr-freight-api/src/migrations/3090000000000-AddSavedSignatureStamp.ts deleted file mode 100644 index 3beafb6bf..000000000 --- a/apps/edr-freight-api/src/migrations/3090000000000-AddSavedSignatureStamp.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddSavedSignatureStamp3090000000000 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.saved_signatures - ADD COLUMN IF NOT EXISTS stamp_file_id UUID NULL; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.saved_signatures - DROP COLUMN IF EXISTS stamp_file_id; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/3100000000000-PromoteDraftSchedulesToScheduled.ts b/apps/edr-freight-api/src/migrations/3100000000000-PromoteDraftSchedulesToScheduled.ts deleted file mode 100644 index f5a916bd3..000000000 --- a/apps/edr-freight-api/src/migrations/3100000000000-PromoteDraftSchedulesToScheduled.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * The draft/finalize phase is abolished: train schedules are created SCHEDULED - * and the Finalize button is gone from the backoffice. Promote every surviving - * DRAFT schedule so it stays reachable (dispatch requires SCHEDULED and there - * is no manual promotion path anymore). Idempotent; one-way — the original - * DRAFT set is not recorded, so down() cannot restore it. - */ -export class PromoteDraftSchedulesToScheduled3100000000000 implements MigrationInterface { - name = "PromoteDraftSchedulesToScheduled3100000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `UPDATE freight.train_schedules - SET status = 'SCHEDULED' - WHERE status = 'DRAFT' - AND deleted_at IS NULL`, - ); - } - - public async down(): Promise { - // One-way data promotion — nothing to restore. - } -} diff --git a/apps/edr-freight-api/src/migrations/3110000000000-AddYardToScheduleWagonAdjustmentLogs.ts b/apps/edr-freight-api/src/migrations/3110000000000-AddYardToScheduleWagonAdjustmentLogs.ts deleted file mode 100644 index 0e1d13175..000000000 --- a/apps/edr-freight-api/src/migrations/3110000000000-AddYardToScheduleWagonAdjustmentLogs.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Consist adjustments can now happen mid-route (train standing at a stop), so - * each history row records WHERE it happened. Nullable — rows written before - * this column simply have no yard. - */ -export class AddYardToScheduleWagonAdjustmentLogs3110000000000 implements MigrationInterface { - name = "AddYardToScheduleWagonAdjustmentLogs3110000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.schedule_wagon_adjustment_logs - ADD COLUMN IF NOT EXISTS yard_id uuid`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.schedule_wagon_adjustment_logs - DROP COLUMN IF EXISTS yard_id`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/3120000000000-AddApprovedAtToCompanies.ts b/apps/edr-freight-api/src/migrations/3120000000000-AddApprovedAtToCompanies.ts deleted file mode 100644 index 976aca798..000000000 --- a/apps/edr-freight-api/src/migrations/3120000000000-AddApprovedAtToCompanies.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * Pending→Active is the only company-level approval event; `updatedAt` can't - * stand in for it since any field edit bumps that too. Nullable — existing - * companies (approved before this column existed) have no recorded moment. - */ -export class AddApprovedAtToCompanies3120000000000 implements MigrationInterface { - name = "AddApprovedAtToCompanies3120000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.companies - ADD COLUMN IF NOT EXISTS approved_at timestamptz`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE freight.companies - DROP COLUMN IF EXISTS approved_at`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/3120000000000-AddCargoTypeItemsPerWagonMap.ts b/apps/edr-freight-api/src/migrations/3120000000000-AddCargoTypeItemsPerWagonMap.ts deleted file mode 100644 index 153928b04..000000000 --- a/apps/edr-freight-api/src/migrations/3120000000000-AddCargoTypeItemsPerWagonMap.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Break-bulk (PER_ITEM) cargo needs a physical items-fit per allowed wagon - * type (e.g. cars → NW5: 4, NW7: 6): a wagon runs out of floor space before it - * runs out of rated tonnage, so allocation must respect BOTH limits. Stored as - * a jsonb map { [wagonTypeId]: itemsFit } on cargo_types — keys mirror the - * cargo_type_wagon_types join rows, kept in sync by the cargo-types service. - */ -export class AddCargoTypeItemsPerWagonMap3120000000000 implements MigrationInterface { - name = 'AddCargoTypeItemsPerWagonMap3120000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "freight"."cargo_types" ADD COLUMN IF NOT EXISTS "items_per_wagon_map" jsonb`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "freight"."cargo_types" DROP COLUMN IF EXISTS "items_per_wagon_map"`, - ); - } -} diff --git a/apps/edr-freight-api/src/migrations/3130000000000-CreateCompanyRevisions.ts b/apps/edr-freight-api/src/migrations/3130000000000-CreateCompanyRevisions.ts deleted file mode 100644 index 01c1c6991..000000000 --- a/apps/edr-freight-api/src/migrations/3130000000000-CreateCompanyRevisions.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -/** - * Append-only audit of company edits made before the company reaches Active - * (the onboarding phase) — that write path has no approval gate and, until - * now, left no trace of what changed (e.g. a phone number or a document). - */ -export class CreateCompanyRevisions3130000000000 implements MigrationInterface { - name = 'CreateCompanyRevisions3130000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'company_revisions', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - { name: 'company_id', type: 'uuid' }, - { name: 'actor_id', type: 'uuid', isNullable: true }, - { name: 'summary', type: 'varchar', length: '255' }, - { name: 'changes', type: 'jsonb', default: "'[]'::jsonb" }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - foreignKeys: [ - { - columnNames: ['company_id'], - referencedSchema: 'freight', - referencedTableName: 'companies', - referencedColumnNames: ['id'], - onDelete: 'CASCADE', - }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.company_revisions', - new TableIndex({ name: 'idx_company_revisions_company', columnNames: ['company_id'] }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.company_revisions', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/3140000000000-SyncBulkRateUnitsToCargoUom.ts b/apps/edr-freight-api/src/migrations/3140000000000-SyncBulkRateUnitsToCargoUom.ts deleted file mode 100644 index b2a931542..000000000 --- a/apps/edr-freight-api/src/migrations/3140000000000-SyncBulkRateUnitsToCargoUom.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Rates created before their commodity's unit_of_measure was flipped kept the - * old bulk-quantity unit, so bookings of a PER_ITEM commodity (e.g. Machinery) - * quoted "per ton". PER_TON and PER_ITEM bill the same stored quantity — only - * the name differs — so renaming is safe. Going forward the cargo-types - * service syncs rates on every uom change; this backfills the drift. - */ -export class SyncBulkRateUnitsToCargoUom3140000000000 implements MigrationInterface { - name = 'SyncBulkRateUnitsToCargoUom3140000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `UPDATE "freight"."rates" r - SET "rate_unit" = 'PER_ITEM' - FROM "freight"."cargo_types" ct - WHERE ct."id" = r."cargo_type_id" - AND ct."unit_of_measure" = 'PER_ITEM' - AND r."rate_unit" = 'PER_TON'`, - ); - await queryRunner.query( - `UPDATE "freight"."rates" r - SET "rate_unit" = 'PER_TON' - FROM "freight"."cargo_types" ct - WHERE ct."id" = r."cargo_type_id" - AND ct."unit_of_measure" = 'PER_TON' - AND r."rate_unit" = 'PER_ITEM'`, - ); - } - - public async down(): Promise { - // Irreversible rename-by-join: the pre-sync unit is not recorded. Both - // units bill identically, so rolling back the code needs no data change. - } -} diff --git a/apps/edr-freight-api/src/migrations/3150000000000-CreateUserTradeAccess.ts b/apps/edr-freight-api/src/migrations/3150000000000-CreateUserTradeAccess.ts deleted file mode 100644 index 9e7cba924..000000000 --- a/apps/edr-freight-api/src/migrations/3150000000000-CreateUserTradeAccess.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; - -/** - * Per-backoffice-user trade-direction scope (import / export / intercity). - * A user with no row (or all three directions) is unrestricted. Admins - * (super_admin / organization_admin) bypass the scope entirely. - */ -export class CreateUserTradeAccess3150000000000 implements MigrationInterface { - name = 'CreateUserTradeAccess3150000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.createTable( - new Table({ - schema: 'freight', - name: 'user_trade_access', - columns: [ - { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' }, - // IAM user id (iam.users) — no FK, iam schema is externally owned. - { name: 'user_id', type: 'uuid', isUnique: true }, - // Comma-separated subset of IMPORT,EXPORT,DOMESTIC (simple-array). - { name: 'directions', type: 'text', default: "''" }, - { name: 'updated_by_id', type: 'uuid', isNullable: true }, - { name: 'created_at', type: 'timestamptz', default: 'now()' }, - { name: 'updated_at', type: 'timestamptz', default: 'now()' }, - { name: 'deleted_at', type: 'timestamptz', isNullable: true }, - ], - }), - true, - ); - - await queryRunner.createIndex( - 'freight.user_trade_access', - new TableIndex({ - name: 'idx_user_trade_access_user_id', - columnNames: ['user_id'], - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('freight.user_trade_access', true); - } -} diff --git a/apps/edr-freight-api/src/migrations/3150000000000-FixFaydaAddressShape.ts b/apps/edr-freight-api/src/migrations/3150000000000-FixFaydaAddressShape.ts deleted file mode 100644 index 7a8923f3c..000000000 --- a/apps/edr-freight-api/src/migrations/3150000000000-FixFaydaAddressShape.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * A normalizeUserInfo bug in VerifaydaService read the raw `address#en` / - * `address#am` claim objects (e.g. `{ "zone#en": "...", "region#en": "...", - * "woreda#en": "..." }`) straight through as if they were strings, so any - * company verified before the fix has `attributes.ownerAddress` / - * `poaAddress` stored as that raw object instead of a formatted string — - * which crashes the portal when it tries to render it as text. - * - * Reformats every affected row's ownerAddress/poaAddress into - * "woreda, zone, region" (falling back to whatever #en fields are present, - * in that preferred order, then any leftover fields), mirroring - * VerifaydaService.formatFaydaAddress. Only touches rows where the field is - * still a jsonb object, so it's idempotent and a no-op once repaired. - */ -export class FixFaydaAddressShape3150000000000 implements MigrationInterface { - name = "FixFaydaAddressShape3150000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE OR REPLACE FUNCTION pg_temp.format_fayda_address(addr jsonb) - RETURNS text AS $$ - DECLARE - field_order text[] := ARRAY['houseNumber','kebele','woreda','city','subCity','zone','region','postalCode','country']; - f text; - v text; - parts text[] := '{}'; - used_keys text[] := '{}'; - kv record; - BEGIN - IF addr IS NULL OR jsonb_typeof(addr) != 'object' THEN - RETURN NULL; - END IF; - - FOREACH f IN ARRAY field_order LOOP - v := addr ->> (f || '#en'); - IF v IS NOT NULL AND trim(v) != '' THEN - parts := array_append(parts, trim(v)); - used_keys := array_append(used_keys, f || '#en'); - END IF; - END LOOP; - - FOR kv IN SELECT * FROM jsonb_each_text(addr) LOOP - IF kv.key LIKE '%#en' AND NOT (kv.key = ANY(used_keys)) - AND kv.value IS NOT NULL AND trim(kv.value) != '' THEN - parts := array_append(parts, trim(kv.value)); - END IF; - END LOOP; - - IF array_length(parts, 1) IS NULL THEN - RETURN NULL; - END IF; - RETURN array_to_string(parts, ', '); - END; - $$ LANGUAGE plpgsql; - - UPDATE freight.companies - SET attributes = jsonb_set( - attributes, - '{ownerAddress}', - to_jsonb(pg_temp.format_fayda_address(attributes -> 'ownerAddress')) - ) - WHERE jsonb_typeof(attributes -> 'ownerAddress') = 'object'; - - UPDATE freight.companies - SET attributes = jsonb_set( - attributes, - '{poaAddress}', - to_jsonb(pg_temp.format_fayda_address(attributes -> 'poaAddress')) - ) - WHERE jsonb_typeof(attributes -> 'poaAddress') = 'object'; - - DROP FUNCTION pg_temp.format_fayda_address(jsonb); - `); - } - - public async down(): Promise { - // Data repair — not reversible (the original malformed shape isn't worth restoring). - } -} diff --git a/apps/edr-freight-api/src/migrations/3160000000000-FixPaymentPaidAtType.ts b/apps/edr-freight-api/src/migrations/3160000000000-FixPaymentPaidAtType.ts deleted file mode 100644 index 35ccdc658..000000000 --- a/apps/edr-freight-api/src/migrations/3160000000000-FixPaymentPaidAtType.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -/** - * `freight.payments.paid_at` was created as `date` (CreatePaymentTable) and never - * migrated to `timestamp` alongside its siblings `refunded_at`/`expires_at` - * (UpdatePaymentTimestamp). TypeORM's postgres driver hydrates `date` columns as a - * plain "YYYY-MM-DD" string, not a `Date` — so `PaymentEntity.paidAt` (typed `Date`) - * was actually a string once read back from the DB, and - * `intent.paidAt?.toISOString()` in PaymentService.formatIntentStatus threw - * `TypeError: intent.paidAt.toISOString is not a function`. This hit every - * OTP-confirm response (CAC Bank) because confirmOtp always re-reads the intent - * before formatting the response. - */ -export class FixPaymentPaidAtType3160000000000 implements MigrationInterface { - name = "FixPaymentPaidAtType3160000000000"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.payments - ALTER COLUMN paid_at TYPE timestamp - USING paid_at::timestamp; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.payments - ALTER COLUMN paid_at TYPE date - USING paid_at::date; - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/3170000000000-YardFacilityOriginDestination.ts b/apps/edr-freight-api/src/migrations/3170000000000-YardFacilityOriginDestination.ts deleted file mode 100644 index fb361b702..000000000 --- a/apps/edr-freight-api/src/migrations/3170000000000-YardFacilityOriginDestination.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -/** - * Handling a freight type is not the same as handling it in both directions. A - * facility can be equipped to load containers onto a train but have no yard - * space to receive and stage inbound ones, so the origin and destination sides - * are stated independently per freight type. - * - * Backfilled from `handles_container` / `handles_bulk` so every existing row - * keeps its current behaviour: a facility that handles a type today handles it - * on both sides until someone narrows it in the backoffice. New rows default - * false — an unconfigured facility offers nothing rather than silently - * offering everything. - */ -export class YardFacilityOriginDestination3170000000000 implements MigrationInterface { - name = 'YardFacilityOriginDestination3170000000000'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.yard_facilities - ADD COLUMN IF NOT EXISTS has_container_facility_origin boolean NOT NULL DEFAULT false, - ADD COLUMN IF NOT EXISTS has_bulk_facility_origin boolean NOT NULL DEFAULT false, - ADD COLUMN IF NOT EXISTS has_container_facility_destination boolean NOT NULL DEFAULT false, - ADD COLUMN IF NOT EXISTS has_bulk_facility_destination boolean NOT NULL DEFAULT false - `); - - await queryRunner.query(` - UPDATE freight.yard_facilities - SET has_container_facility_origin = handles_container, - has_container_facility_destination = handles_container, - has_bulk_facility_origin = handles_bulk, - has_bulk_facility_destination = handles_bulk - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE freight.yard_facilities - DROP COLUMN IF EXISTS has_container_facility_origin, - DROP COLUMN IF EXISTS has_bulk_facility_origin, - DROP COLUMN IF EXISTS has_container_facility_destination, - DROP COLUMN IF EXISTS has_bulk_facility_destination - `); - } -} diff --git a/apps/edr-freight-api/src/migrations/3250000000000-CreateLastMileRequests.ts b/apps/edr-freight-api/src/migrations/3250000000000-CreateLastMileRequests.ts new file mode 100644 index 000000000..0763e32a3 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/3250000000000-CreateLastMileRequests.ts @@ -0,0 +1,87 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +/** + * Create freight.last_mile_requests — the pre-approval confirmation stage that + * sits in front of freight.last_mile: a train departs Djibouti, the customer + * confirms which containers go via EDR last-mile, and the Truck & Machinery + * chief approves/rejects before a freight.last_mile execution record exists. + */ +export class CreateLastMileRequests3250000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const exists = await queryRunner.hasTable('freight.last_mile_requests'); + if (exists) return; + + await queryRunner.createTable( + new Table({ + name: 'freight.last_mile_requests', + columns: [ + { name: 'id', type: 'uuid', isPrimary: true, default: 'gen_random_uuid()' }, + { name: 'booking_id', type: 'uuid', isNullable: false }, + { name: 'train_schedule_id', type: 'uuid', isNullable: false }, + { + name: 'status', + type: 'varchar', + length: '30', + default: `'AWAITING_CONFIRMATION'`, + isNullable: false, + }, + { name: 'requested_container_numbers', type: 'text', isArray: true, isNullable: true }, + { name: 'reminder_sent_at', type: 'timestamptz', isNullable: true }, + { name: 'submitted_by_user_id', type: 'uuid', isNullable: true }, + { name: 'submitted_at', type: 'timestamptz', isNullable: true }, + { name: 'reviewed_by_staff_id', type: 'uuid', isNullable: true }, + { name: 'reviewed_at', type: 'timestamptz', isNullable: true }, + { name: 'rejection_reason', type: 'text', isNullable: true }, + { name: 'resulting_last_mile_id', type: 'uuid', isNullable: true }, + { name: 'created_at', type: 'timestamptz', default: 'now()' }, + { name: 'updated_at', type: 'timestamptz', default: 'now()' }, + { name: 'deleted_at', type: 'timestamptz', isNullable: true }, + ], + }), + true, + ); + + await queryRunner.createForeignKey( + 'freight.last_mile_requests', + new TableForeignKey({ + columnNames: ['booking_id'], + referencedTableName: 'freight.bookings', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + await queryRunner.createForeignKey( + 'freight.last_mile_requests', + new TableForeignKey({ + columnNames: ['train_schedule_id'], + referencedTableName: 'freight.train_schedules', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }), + ); + await queryRunner.createForeignKey( + 'freight.last_mile_requests', + new TableForeignKey({ + columnNames: ['resulting_last_mile_id'], + referencedTableName: 'freight.last_mile', + referencedColumnNames: ['id'], + onDelete: 'SET NULL', + }), + ); + + // One request per booking per departure — remind()/submit() are idempotent on this. + await queryRunner.query( + `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_last_mile_requests_booking_schedule" ON "freight"."last_mile_requests" ("booking_id", "train_schedule_id") WHERE "deleted_at" IS NULL`, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_last_mile_requests_status" ON "freight"."last_mile_requests" ("status")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + const exists = await queryRunner.hasTable('freight.last_mile_requests'); + if (exists) { + await queryRunner.dropTable('freight.last_mile_requests'); + } + } +} diff --git a/apps/edr-freight-api/src/migrations/3250000000000-FreightBaseline.ts b/apps/edr-freight-api/src/migrations/3250000000000-FreightBaseline.ts new file mode 100644 index 000000000..5e39b3974 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/3250000000000-FreightBaseline.ts @@ -0,0 +1,5561 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Squashed baseline for the whole `freight` schema. + * + * Replaces the 299 incremental migrations that preceded it. The SQL below is a + * `pg_dump` of a database built by running those 299 in order, so a fresh + * database ends up byte-identical to one migrated the long way — including the + * reference/seed rows the old migrations inserted (wagon fleet, wagon types, + * cargo types, yards, approval rules, dropdown settings, contract templates, + * truck types, gov companies, exchange settings). + * + * On a database that already ran the old migrations this is a no-op: `up()` + * returns early when the schema is already there, and the row is recorded so the + * history stays linear. It is therefore safe to deploy to dev/prod as-is. + * + * Order matters: structure → seed rows → foreign keys. The FK constraints are + * applied last so the dumped INSERTs never depend on table ordering. + * + * Regenerate (never hand-edit): run every migration into an empty database, then + * `pg_dump --schema-only -n freight -T freight.migrations` plus a + * `--data-only --column-inserts` dump of the non-empty tables. + * + * ONE deliberate deviation from that dump, which a regeneration would silently + * undo — reapply it: `freight.otp_verifications` carries the constraint names + * `pk_otp_verifications` / `uq_otp_verifications_phone` rather than TypeORM's + * generated `PK_91d17…` / `UQ_5121c…`. Prod built that table through + * `1870000000000-RepairSynchronizeDrift` (which names its constraints by hand) + * while a fresh run builds it through `1810000000003-CreateOtpVerifications` + * (which lets TypeORM hash them). Same columns either way; matching prod's names + * keeps `sql/adopt-split-migration-history.sql` strict. + */ + +const STRUCTURE_SQL = ` +CREATE SCHEMA IF NOT EXISTS freight; + +CREATE TYPE freight.consignments_cargo_type_enum AS ENUM ( + 'CONTAINER', + 'BULK_LIQUID', + 'BULK_DRY', + 'GENERAL', + 'REFRIGERATED', + 'HAZARDOUS' +); + +CREATE TYPE freight.consignments_status_enum AS ENUM ( + 'PENDING', + 'LOADED', + 'IN_TRANSIT', + 'AT_DESTINATION', + 'DELIVERED', + 'RETURNED' +); + +CREATE TYPE freight.invoices_status_enum AS ENUM ( + 'DRAFT', + 'ISSUED', + 'PENDING', + 'PARTIALLY_PAID', + 'PAID', + 'OVERDUE', + 'CANCELLED', + 'REFUNDED', + 'EXPIRED' +); + +CREATE TYPE freight.payment_webhook_method_enum AS ENUM ( + 'telebirr', + 'cbe-birr', + 'ebirr' +); + +CREATE TYPE freight.payments_currency_enum AS ENUM ( + 'ETB', + 'USD' +); + +CREATE TYPE freight.payments_method_enum AS ENUM ( + 'telebirr', + 'cbe-birr', + 'ebirr', + 'waafi', + 'card', + 'dmoney', + 'cac-bank', + 'cbe-bill' +); + +CREATE TYPE freight.payments_status_enum AS ENUM ( + 'action-required', + 'processing', + 'success', + 'failed', + 'canceled', + 'refunded' +); + +CREATE TYPE freight.priority_rules_priority_type_enum AS ENUM ( + 'USD_PAYER', + 'RAIL_AND_FORWARDING', + 'GOVERNMENT_ACCOUNT', + 'HIGH_VOLUME_SHIPMENT' +); + +CREATE TYPE freight.surcharges_calculation_method_enum AS ENUM ( + 'PER_TON', + 'FLAT_FEE', + 'PERCENTAGE' +); + +CREATE TYPE freight.tracking_events_status_enum AS ENUM ( + 'PENDING', + 'LOADED', + 'IN_TRANSIT', + 'AT_DESTINATION', + 'DELIVERED', + 'RETURNED' +); + +CREATE TYPE freight.train_status AS ENUM ( + 'AVAILABLE', + 'SCHEDULED', + 'IN_SERVICE', + 'UNDER_MAINTENANCE', + 'OUT_OF_SERVICE', + 'DEACTIVATED' +); + +CREATE TYPE freight.vehicles_status_enum AS ENUM ( + 'ACTIVE', + 'FREE', + 'BUSY', + 'MAINTENANCE', + 'RETIRED', + 'OUT_OF_SERVICE' +); + +CREATE TYPE freight.weight_limit_rules_exceeded_action_enum AS ENUM ( + 'WARNING_ONLY', + 'HARD_BLOCK' +); + +CREATE TYPE freight.weight_limit_rules_trade_direction_enum AS ENUM ( + 'IMPORT', + 'EXPORT', + 'BOTH', + 'DOMESTIC' +); + +CREATE TABLE freight.approval_rules ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + requires_director_approval boolean NOT NULL, + step_order smallint NOT NULL, + required_role character varying(64) NOT NULL, + action_label character varying(50) NOT NULL, + blocks_role character varying(64), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.asset_acquisitions ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + vehicle_id uuid, + vendor_id uuid, + acquisition_type character varying NOT NULL, + acquisition_date date NOT NULL, + cost numeric(14,2), + useful_life_months integer, + salvage_value numeric(14,2), + lease_start date, + lease_end date, + monthly_payment numeric(14,2), + status character varying DEFAULT 'ACTIVE'::character varying NOT NULL, + notes text, + item_name character varying(200) +); + +CREATE TABLE freight.asset_disposals ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + vehicle_id uuid NOT NULL, + disposal_date date NOT NULL, + method character varying NOT NULL, + sale_price numeric(14,2), + buyer character varying, + notes text +); + +CREATE TABLE freight.booking_batch_offers ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + train_schedule_id uuid NOT NULL, + offered_wagons integer NOT NULL, + total_wagons integer NOT NULL, + offered_lines jsonb, + offered_weight_tons numeric(12,3) NOT NULL, + offered_amount numeric(14,2) NOT NULL, + offered_pricing_breakdown jsonb, + invoice_id uuid, + payment_deadline timestamp with time zone NOT NULL, + status character varying(10) DEFAULT 'OFFERED'::character varying NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.booking_cargo_modifier ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + booking_id uuid NOT NULL, + trigger_value numeric(14,4), + calculated_amount numeric(14,2) NOT NULL, + rate_snapshot_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + rate_id uuid NOT NULL +); + +CREATE TABLE freight.booking_container ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + booking_id uuid NOT NULL, + container_type_id uuid, + quantity smallint NOT NULL, + vgm_per_unit_tons numeric(10,3) NOT NULL, + total_vgm_tons numeric(12,3) NOT NULL, + wagons_required numeric(6,2) NOT NULL, + weight_limit_rule_id uuid, + is_overweight boolean DEFAULT false NOT NULL, + overweight_excess_tons numeric(10,3), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + container_number character varying(64), + container_size character varying(10), + hazardous_quantity smallint DEFAULT 0, + reefer_quantity smallint DEFAULT 0, + return_quantity smallint DEFAULT 0 NOT NULL +); + +CREATE TABLE freight.booking_container_allocations ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + container_id uuid NOT NULL, + vehicle_id uuid, + container_type text NOT NULL, + quantity integer DEFAULT 1 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.booking_container_units ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_container_id uuid NOT NULL, + container_number character varying(64) NOT NULL, + seal_number character varying(64), + vgm_tons numeric(10,3) NOT NULL, + is_hazardous boolean DEFAULT false, + is_reefer boolean DEFAULT false, + sort_order smallint DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + received_to_port boolean DEFAULT false NOT NULL, + received_at timestamp with time zone, + grn_number character varying(100), + is_return boolean DEFAULT false NOT NULL +); + +CREATE TABLE freight.booking_contract_signatures ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + signer_role character varying(20) NOT NULL, + signer_user_id uuid, + signer_display_name character varying(200) NOT NULL, + signed_at timestamp with time zone DEFAULT now() NOT NULL, + signature_file_id uuid, + consent_text text, + ip_address character varying(64), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.booking_document_review ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + setting_code character varying(128) NOT NULL, + file_key character varying(128) NOT NULL, + file_record_id uuid, + status character varying(20) DEFAULT 'PENDING'::character varying NOT NULL, + note text, + reviewed_by_staff_id uuid, + reviewed_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + contract_id uuid +); + +CREATE TABLE freight.booking_handovers ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + truck_assignment_id uuid, + truck_plate character varying(32), + mile_type character varying(20) NOT NULL, + reference character varying(100) NOT NULL, + generated_at timestamp with time zone DEFAULT now() NOT NULL, + signed_at timestamp with time zone, + signed_by_user_id uuid, + delivered_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + signer_name character varying(160), + edr_assignment_id uuid, + signature_image_url text +); + +CREATE TABLE freight.booking_rate_snapshot ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + booking_id uuid NOT NULL, + rate_id uuid NOT NULL, + rate_type character varying(50) NOT NULL, + rate_value numeric(14,4) NOT NULL, + rate_unit character varying(30) NOT NULL, + currency character varying(5) NOT NULL, + snapshotted_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.booking_requests ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + reference character varying(40) DEFAULT ''::character varying NOT NULL, + contract_id uuid NOT NULL, + requested_by_user_id uuid, + contract_route_id uuid, + scheduled_date timestamp with time zone, + status character varying(16) DEFAULT 'PENDING'::character varying NOT NULL, + requested_lines jsonb DEFAULT '{}'::jsonb NOT NULL, + notes text, + created_booking_id uuid, + reviewed_by_staff_id uuid, + reviewed_at timestamp with time zone, + review_note text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + payment_currency character varying(5) +); + +CREATE TABLE freight.booking_review_note ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + author_id uuid, + note text NOT NULL, + type character varying(30) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.bookings ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + reference character varying(64) NOT NULL, + customer_id uuid, + train_id uuid, + status character varying(40) DEFAULT 'DRAFT'::character varying NOT NULL, + scheduled_date timestamp with time zone DEFAULT now(), + total_amount numeric(14,2) DEFAULT 0 NOT NULL, + payment_status character varying(20) DEFAULT 'PENDING'::character varying NOT NULL, + contract_type character varying(20) DEFAULT 'SPOT'::character varying NOT NULL, + trade_direction character varying(10) DEFAULT 'IMPORT'::character varying NOT NULL, + equipment_return character varying(20) DEFAULT 'RETURN'::character varying NOT NULL, + first_mile_pickup_address text, + last_mile_delivery_address text, + cargo_total_weight_vgm numeric(12,3) DEFAULT 0 NOT NULL, + is_hazardous boolean DEFAULT false NOT NULL, + payment_currency character varying(5) DEFAULT 'USD'::character varying NOT NULL, + start_date date, + end_date date, + financial_terms text, + version_number integer DEFAULT 1 NOT NULL, + approved_by_staff_id uuid, + approved_by_staff_at timestamp with time zone, + signed_by_director_id uuid, + signed_by_director_at timestamp with time zone, + signed_by_ceo_id uuid, + signed_by_ceo_at timestamp with time zone, + priority_score integer DEFAULT 0 NOT NULL, + consolidation_partner_id uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + origin_yard_id uuid NOT NULL, + destination_yard_id uuid NOT NULL, + service_type_id uuid NOT NULL, + cargo_type_id uuid, + cargo_free_text character varying(200), + shipping_line_id uuid, + pnr_code character varying(50), + customer_signed_at timestamp with time zone, + fully_executed_at timestamp with time zone, + marketing_approved_by_id uuid, + marketing_approved_at timestamp with time zone, + contract_summary text, + locked_at timestamp with time zone, + freight_type character varying(20) NOT NULL, + contract_template_key character varying(80), + contract_generated_at timestamp with time zone, + pricing_breakdown jsonb, + company_id uuid NOT NULL, + wagons_required numeric(6,2), + scheduling_status character varying(30) DEFAULT 'NOT_SCHEDULED'::character varying NOT NULL, + hold_started_at timestamp with time zone, + hold_expires_at timestamp with time zone, + scheduled_at timestamp with time zone, + is_government boolean DEFAULT false NOT NULL, + government_institution character varying(255), + train_schedule_id uuid, + payment_deadline timestamp with time zone, + selected_for_batch_at timestamp with time zone, + company_profile_id uuid NOT NULL, + expires_at timestamp with time zone, + adjusted_total_amount numeric(14,2), + adjusted_by_staff_id uuid, + adjusted_at timestamp with time zone, + adjustment_reason text, + contract_validity_days integer, + contract_valid_from timestamp with time zone, + contract_valid_until timestamp with time zone, + first_mile_pickup_lat numeric(10,7), + first_mile_pickup_lng numeric(10,7), + last_mile_delivery_lat numeric(10,7), + last_mile_delivery_lng numeric(10,7), + customs_clearing_enabled boolean DEFAULT false NOT NULL, + customs_clearing_agent character varying(200), + is_reefer boolean DEFAULT false NOT NULL, + estimated_shipment_date timestamp with time zone, + contract_id uuid, + contract_route_id uuid, + created_by_role character varying(20) DEFAULT 'CUSTOMER'::character varying, + created_by_user_id uuid, + contract_kind character varying(20), + gl_station_yard_id uuid, + gl_assigned_staff_id uuid, + gl_assigned_at timestamp with time zone, + bulk_hazardous_quantity numeric(12,3) DEFAULT 0 NOT NULL, + bulk_reefer_quantity numeric(12,3) DEFAULT 0 NOT NULL, + clearance_current_phase character varying(40), + duty_required boolean, + vessel_departure_date date, + ro_amendment_requested_at timestamp with time zone, + ro_hold_reason text, + pre_clearance_finalized_at timestamp with time zone, + customer_truck_plate_number character varying(32), + customer_truck_driver_name character varying(120), + customer_truck_type character varying(60), + customer_truck_container_number character varying(16), + customer_truck_assigned_at timestamp with time zone, + customer_truck_arrived_at timestamp with time zone, + booking_type character varying(20) DEFAULT 'ONE_TIME'::character varying NOT NULL, + loaded_at timestamp with time zone, + loaded_by_user_id uuid, + arrived_at timestamp with time zone, + arrived_by_user_id uuid, + consolidation_resume_status character varying(40), + is_split boolean DEFAULT false NOT NULL, + pre_split_quantities jsonb, + double_handling boolean, + double_handling_set_at timestamp with time zone, + double_handling_set_by character varying(160), + vessel_arrival_date date, + do_collected_date date, + transit_assignee_requested_at timestamp with time zone, + transit_assignee_request_note text, + transit_assignee_name text, + transit_assignee_assigned_at timestamp with time zone, + requested_train_schedule_id uuid, + payment_reminder_sent_at timestamp with time zone, + bulk_total_weight_tons numeric(12,3), + CONSTRAINT chk_bookings_freight_type CHECK (((freight_type)::text = ANY ((ARRAY['CONTAINER'::character varying, 'BULK'::character varying])::text[]))) +); + +CREATE TABLE freight.cargo_type_wagon_types ( + cargo_type_id uuid NOT NULL, + wagon_type_id uuid NOT NULL +); + +CREATE TABLE freight.cargo_types ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + cargo_type_name character varying(255) NOT NULL, + parent_group_id uuid, + requires_director_approval boolean DEFAULT false NOT NULL, + is_active boolean DEFAULT true NOT NULL, + display_order integer DEFAULT 1 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + code character varying(50) NOT NULL, + unit_of_measure character varying(16), + has_lashing boolean DEFAULT false NOT NULL, + items_per_wagon_map jsonb, + tons_per_wagon_map jsonb +); + +CREATE TABLE freight.cargoes ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + cargo_reference character varying NOT NULL, + shipment_id uuid NOT NULL, + container_id uuid, + cargo_type_id uuid, + description text, + quantity numeric(12,3) NOT NULL, + weight numeric(10,2) NOT NULL, + volume numeric(10,2), + status character varying DEFAULT 'PENDING'::character varying NOT NULL, + loaded_at timestamp without time zone, + unloaded_at timestamp without time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + wagon_booking_allocation_id uuid, + booking_id uuid, + load_type character varying(20), + receiver_name character varying, + delivered_at timestamp without time zone, + delivery_remarks text +); + +CREATE TABLE freight.clearance_incidents ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + incident_type character varying(32) NOT NULL, + description text NOT NULL, + photo_file_ids jsonb DEFAULT '[]'::jsonb NOT NULL, + reported_by_user_id uuid, + reported_at timestamp with time zone DEFAULT now() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.clearance_milestones ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid, + contract_id uuid, + clearance_cycle_id uuid, + milestone_code character varying(64) NOT NULL, + milestone_label character varying(255) NOT NULL, + status character varying(20) DEFAULT 'PENDING'::character varying NOT NULL, + owner_region character varying(5), + triggered_by_doc boolean DEFAULT false, + triggered_at timestamp with time zone, + triggered_by_user_id uuid, + note text, + sort_order smallint DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + metadata jsonb +); + +CREATE TABLE freight.companies ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + name character varying(200) NOT NULL, + type character varying(32) NOT NULL, + status character varying(32) DEFAULT 'pending'::character varying NOT NULL, + tin character varying(10) NOT NULL, + vat_number character varying(50), + fan_number character varying(16), + country character varying(32) DEFAULT 'Ethiopia'::character varying NOT NULL, + address text, + phone character varying(20), + email character varying(150), + website character varying(200), + attributes jsonb, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + contact_person_name character varying(100), + contact_person_phone character varying(20), + general_manager_name character varying(100), + general_manager_email character varying(150), + general_manager_phone character varying(20), + nationality character varying(32), + licence_number character varying(100), + status_description text, + date_registered character varying(50), + renewed_from character varying(50), + renewal_date character varying(50), + renewed_to character varying(50), + region character varying(100), + zone character varying(100), + woreda character varying(100), + kebele character varying(100), + house_no character varying(100), + etrade_phone character varying(20), + kind character varying(20) DEFAULT 'commercial'::character varying NOT NULL, + approved_at timestamp with time zone +); + +CREATE TABLE freight.company_change_request ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + company_id uuid NOT NULL, + snapshot jsonb NOT NULL, + documents jsonb, + status character varying(20) DEFAULT 'pending'::character varying NOT NULL, + note text, + submitted_by uuid, + submitted_at timestamp with time zone, + reviewed_by uuid, + reviewed_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.company_profiles ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + company_id uuid NOT NULL, + type character varying(32) NOT NULL, + reference character varying(20), + status character varying(32) DEFAULT 'pending'::character varying NOT NULL, + business_license character varying(100), + attributes jsonb, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + business_license_files jsonb, + review_note text, + reviewed_by uuid, + reviewed_at timestamp with time zone +); + +CREATE TABLE freight.company_revisions ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + company_id uuid NOT NULL, + actor_id uuid, + summary character varying(255) NOT NULL, + changes jsonb DEFAULT '[]'::jsonb NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.compliance_records ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + vehicle_id uuid NOT NULL, + type character varying NOT NULL, + document_number character varying, + issued_date date, + expiry_date date NOT NULL, + status character varying DEFAULT 'VALID'::character varying NOT NULL, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.consignments ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + tracking_number character varying(64) NOT NULL, + cargo_type freight.consignments_cargo_type_enum NOT NULL, + weight_kg numeric(12,2) NOT NULL, + status freight.consignments_status_enum DEFAULT 'PENDING'::freight.consignments_status_enum NOT NULL, + origin_station character varying(128) NOT NULL, + destination_station character varying(128) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.container_type_wagon_types ( + container_type_id uuid NOT NULL, + wagon_type_id uuid NOT NULL +); + +CREATE TABLE freight.container_types ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + code character varying(20) NOT NULL, + label character varying(100), + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + size_ft smallint, + is_reefer boolean DEFAULT false NOT NULL, + is_open_top boolean DEFAULT false NOT NULL, + display_order integer DEFAULT 1 NOT NULL +); + +CREATE TABLE freight.containers ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + container_number character varying NOT NULL, + container_type_id uuid NOT NULL, + wagon_id uuid, + "position" integer, + tare_weight numeric(10,2) NOT NULL, + max_gross_weight numeric(10,2) NOT NULL, + seal_number character varying, + status character varying DEFAULT 'AVAILABLE'::character varying NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + booking_id uuid, + wagon_booking_allocation_id uuid, + booking_container_id uuid +); + +CREATE TABLE freight.contract_approval_steps ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + contract_id uuid NOT NULL, + step_order smallint DEFAULT 0 NOT NULL, + required_role character varying(64) NOT NULL, + blocks_role character varying(64), + status character varying(20) DEFAULT 'PENDING'::character varying NOT NULL, + acted_by_staff_id uuid, + acted_at timestamp with time zone, + note text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.contract_cargo_scope ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + contract_id uuid NOT NULL, + container_size character varying(10), + cargo_type_id uuid, + cargo_free_text character varying(200), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + quantity_cap numeric(12,2) +); + +CREATE TABLE freight.contract_clearance_cycles ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + contract_id uuid NOT NULL, + cycle_number integer NOT NULL, + status character varying(40) DEFAULT 'AWAITING_DOCUMENTS'::character varying NOT NULL, + booking_id uuid, + started_at timestamp with time zone DEFAULT now() NOT NULL, + clearance_ready_at timestamp with time zone, + completed_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + duty_required boolean, + vessel_departure_date date, + ro_amendment_requested_at timestamp with time zone, + ro_hold_reason text, + current_phase character varying(40), + pre_clearance_finalized_at timestamp with time zone, + transit_assignee_requested_at timestamp with time zone, + transit_assignee_requested_by_user_id uuid, + transit_assignee_request_note text, + transit_assignee_name text, + transit_assignee_assigned_at timestamp with time zone, + transit_assignee_assigned_by_user_id uuid, + vessel_arrival_date date, + do_collected_date date +); + +CREATE TABLE freight.contract_document_review ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + contract_id uuid NOT NULL, + clearance_cycle_id uuid, + setting_code character varying(128) NOT NULL, + file_key character varying(128) NOT NULL, + file_record_id uuid, + status character varying(20) DEFAULT 'PENDING'::character varying NOT NULL, + note text, + uploaded_by_role character varying(20) DEFAULT 'CUSTOMER'::character varying NOT NULL, + reviewed_by_staff_id uuid, + reviewed_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.contract_document_revisions ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + contract_id uuid NOT NULL, + actor_id uuid, + actor_role character varying(64), + step_id uuid, + summary character varying(255), + changes jsonb DEFAULT '[]'::jsonb NOT NULL, + actor_name character varying(200) +); + +CREATE TABLE freight.contract_rate_snapshots ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + contract_id uuid NOT NULL, + rate_id uuid, + rate_code character varying(64) NOT NULL, + description character varying(255), + unit_price numeric(14,2) NOT NULL, + unit_of_measure character varying(32) NOT NULL, + currency character varying(5) NOT NULL, + container_size character varying(10), + is_surcharge boolean DEFAULT false, + conditional_on character varying(32), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + is_clearance boolean DEFAULT false NOT NULL +); + +CREATE TABLE freight.contract_review_notes ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + contract_id uuid NOT NULL, + note_type character varying(40) NOT NULL, + body text NOT NULL, + author_role character varying(20), + author_user_id uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.contract_routes ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + contract_id uuid NOT NULL, + origin_yard_id uuid NOT NULL, + destination_yard_id uuid NOT NULL, + km numeric(10,2), + sort_order smallint DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.contract_signatures ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + contract_id uuid NOT NULL, + role character varying(20) NOT NULL, + signer_display_name character varying(255) NOT NULL, + signature_file_id uuid, + consent_text text, + signed_at timestamp with time zone DEFAULT now() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + stamp_file_id uuid +); + +CREATE TABLE freight.contract_templates ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + code character varying(40) NOT NULL, + name character varying(200) NOT NULL, + description text, + document_title character varying(300) NOT NULL, + whereas_clauses jsonb DEFAULT '[]'::jsonb NOT NULL, + articles jsonb DEFAULT '[]'::jsonb NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.contracts ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + reference character varying(64) NOT NULL, + company_id uuid, + company_profile_id uuid, + is_government boolean DEFAULT false NOT NULL, + government_institution character varying(255), + contract_kind character varying(20) NOT NULL, + renewal_of_id uuid, + trade_direction character varying(10) NOT NULL, + freight_type character varying(20) NOT NULL, + service_type_id uuid NOT NULL, + payment_currency character varying(5) NOT NULL, + customs_clearing_enabled boolean DEFAULT false NOT NULL, + customs_clearing_agent character varying(200), + equipment_return character varying(20), + first_mile_pickup_address text, + first_mile_pickup_lat numeric(10,7), + first_mile_pickup_lng numeric(10,7), + last_mile_delivery_address text, + last_mile_delivery_lat numeric(10,7), + last_mile_delivery_lng numeric(10,7), + is_hazardous boolean DEFAULT false NOT NULL, + is_reefer boolean DEFAULT false NOT NULL, + estimated_shipment_date timestamp with time zone, + contract_validity_days integer, + contract_valid_from timestamp with time zone, + contract_valid_until timestamp with time zone, + expires_at timestamp with time zone, + status character varying(40) DEFAULT 'DRAFT'::character varying NOT NULL, + clearance_status character varying(40) DEFAULT 'NOT_APPLICABLE'::character varying NOT NULL, + clearance_cycle_number integer DEFAULT 0 NOT NULL, + pricing_breakdown jsonb, + pricing_display_mode character varying(20) DEFAULT 'UNIT_RATES'::character varying, + contract_type character varying(20), + contract_template_key character varying(128), + contract_generated_at timestamp with time zone, + contract_summary text, + version_number integer DEFAULT 1 NOT NULL, + financial_terms jsonb, + approved_by_staff_id uuid, + approved_by_staff_at timestamp with time zone, + signed_by_director_id uuid, + signed_by_director_at timestamp with time zone, + signed_by_ceo_id uuid, + signed_by_ceo_at timestamp with time zone, + customer_signed_at timestamp with time zone, + fully_executed_at timestamp with time zone, + locked_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + document_snapshot jsonb, + hazard_class character varying(16), + un_number character varying(16), + status_before_suspension character varying(40), + submitted_at timestamp with time zone +); + +CREATE TABLE freight.customer_truck_assignments ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + plate_number character varying(32) NOT NULL, + driver_name character varying(120) NOT NULL, + truck_type character varying(60) NOT NULL, + assigned_at timestamp with time zone DEFAULT now() NOT NULL, + arrived_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + gross_weight_kg numeric(14,2), + departed_at timestamp with time zone, + tare_weight_tons numeric(14,3), + net_weight_tons numeric(14,3), + planned_tons numeric(14,3), + planned_quantity integer +); + +CREATE TABLE freight.customer_truck_containers ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + assignment_id uuid NOT NULL, + booking_id uuid NOT NULL, + container_number character varying(64) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + loaded_at timestamp with time zone +); + +CREATE TABLE freight.customers ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + user_id uuid NOT NULL, + first_name character varying(100) NOT NULL, + last_name character varying(100) NOT NULL, + email character varying(150) NOT NULL, + phone character varying(20) NOT NULL, + company_name character varying(200) NOT NULL, + company_email character varying(150) NOT NULL, + company_phone character varying(20) NOT NULL, + company_location character varying(100) NOT NULL, + company_address text NOT NULL, + customer_type character varying(32), + status character varying(32), + contact_person_name character varying(100) NOT NULL, + contact_person_phone character varying(20) NOT NULL, + tin_number character varying(10) NOT NULL, + vat_number character varying(50), + fan_number character varying(16) NOT NULL, + general_manager_name character varying(100) NOT NULL, + general_manager_email character varying(150) NOT NULL, + general_manager_phone character varying(20) NOT NULL, + poa_name character varying(100), + poa_phone character varying(20), + poa_address text, + poa_email character varying(150), + poa_location character varying(100), + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.djibouti_import_incidents ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + booking_id uuid NOT NULL, + container_number character varying(80), + cargo_id uuid, + facility character varying(120), + station character varying(120), + incident_type character varying(40) NOT NULL, + description text NOT NULL, + photos jsonb DEFAULT '[]'::jsonb NOT NULL, + reported_by character varying(120), + reported_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.drivers ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + license_number character varying NOT NULL, + first_name character varying NOT NULL, + last_name character varying NOT NULL, + email character varying NOT NULL, + phone_number character varying NOT NULL, + date_of_birth date NOT NULL, + license_expiry_date date NOT NULL, + status character varying DEFAULT 'ACTIVE'::character varying NOT NULL, + vehicle_types_authorized character varying[], + address text, + emergency_contact character varying, + notes text, + total_trips integer DEFAULT 0, + rating numeric(3,2), + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted_at timestamp without time zone, + fayda_verified boolean DEFAULT false, + fayda_sub character varying, + gender character varying +); + +CREATE TABLE freight.dropdown_options ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + setting_id uuid NOT NULL, + value character varying(256) NOT NULL, + label character varying(256) NOT NULL, + note text, + is_disabled boolean DEFAULT false NOT NULL, + display_order integer DEFAULT 0 NOT NULL, + meta jsonb, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.dropdown_settings ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + code character varying(128) NOT NULL, + label character varying(256) NOT NULL, + description text, + multiple boolean DEFAULT false NOT NULL, + meta jsonb, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.empty_container_returns ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + container_number character varying(80) NOT NULL, + booking_id uuid, + customer_id uuid, + return_date timestamp with time zone NOT NULL, + facility character varying(120), + yard character varying(120), + zone character varying(120), + condition text, + handover_note text, + status character varying(40) DEFAULT 'RETURNED'::character varying NOT NULL, + wagon_allocation_reference character varying(120), + performed_by character varying(120), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + returned_by character varying(20), + status_history jsonb DEFAULT '[]'::jsonb NOT NULL +); + +CREATE TABLE freight.exchange_settings ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + fallback_rate numeric(18,6) NOT NULL, + fallback_source character varying(16) DEFAULT 'AUTO'::character varying NOT NULL, + last_synced_at timestamp with time zone, + updated_by_id uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.external_profiles ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + user_id uuid NOT NULL, + company_id uuid NOT NULL, + first_name character varying(100) NOT NULL, + last_name character varying(100) NOT NULL, + national_id character varying(50), + job_title character varying(100), + is_primary_contact boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + onboarding_step character varying(40), + onboarding_completed boolean DEFAULT false NOT NULL +); + +CREATE TABLE freight.facilities ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + code character varying(40) NOT NULL, + name character varying(160) NOT NULL, + description text, + facility_type character varying(32) NOT NULL, + facility_status character varying(32) DEFAULT 'ACTIVE'::character varying NOT NULL, + location_name character varying(200), + country character varying(100), + city character varying(100), + address text, + latitude numeric(10,8), + longitude numeric(11,8), + capacity numeric(14,3), + is_active boolean DEFAULT true NOT NULL, + notes text, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted_at timestamp without time zone +); + +CREATE TABLE freight.facility_handling_events ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + yard_id uuid NOT NULL, + train_schedule_id uuid, + event_type character varying(10) NOT NULL, + grn_number character varying(60), + quantity numeric(14,3), + weight_tons numeric(14,3), + inventory_id uuid, + performed_by character varying(120), + occurred_at timestamp with time zone DEFAULT now() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.fayda_verification_sessions ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + state character varying NOT NULL, + code_verifier character varying NOT NULL, + purpose character varying DEFAULT 'VERIFY'::character varying NOT NULL, + platform character varying DEFAULT 'WEB'::character varying NOT NULL, + save_to_account boolean DEFAULT false NOT NULL, + status character varying DEFAULT 'PENDING'::character varying NOT NULL, + error_code character varying, + error_description text, + iam_user_id uuid, + expires_at timestamp with time zone NOT NULL, + completed_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.ff_clients ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + forwarder_company_id uuid NOT NULL, + client_company_id uuid NOT NULL, + relationship_type character varying(32) DEFAULT 'managed_account'::character varying NOT NULL, + can_book_on_behalf boolean DEFAULT true NOT NULL, + can_view_documents boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.file_upload_fields ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + setting_id uuid NOT NULL, + file_key character varying(128) NOT NULL, + file_label character varying(256) NOT NULL, + help_text text, + is_required boolean DEFAULT false NOT NULL, + is_multiple boolean DEFAULT false NOT NULL, + max_files integer DEFAULT 1 NOT NULL, + allowed_extensions text[] DEFAULT '{}'::text[] NOT NULL, + max_size_mb integer DEFAULT 10 NOT NULL, + display_order integer DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + phase character varying(40), + owner_region character varying(5), + trade_direction character varying(10), + triggers_milestone_code character varying(64), + CONSTRAINT "CHK_file_upload_fields_max_files" CHECK ((max_files > 0)), + CONSTRAINT "CHK_file_upload_fields_max_size_mb" CHECK ((max_size_mb > 0)) +); + +CREATE TABLE freight.file_upload_settings ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + code character varying(128) NOT NULL, + label character varying(256) NOT NULL, + description text, + entity character varying(32) DEFAULT 'other'::character varying NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.files ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + resource_id uuid NOT NULL, + resource character varying(100) NOT NULL, + code character varying(100) NOT NULL, + name character varying(500) NOT NULL, + url text NOT NULL, + size integer NOT NULL, + mime_type character varying(255) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + review_status character varying(32), + review_note text, + reviewed_by uuid, + reviewed_at timestamp with time zone, + replaced_by_user_id uuid, + replace_reason text, + title character varying(300), + visible_to_customer boolean DEFAULT false NOT NULL, + uploaded_by_user_id uuid, + uploaded_by_name character varying(200) +); + +CREATE TABLE freight.first_mile ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + status character varying(30) DEFAULT 'PAYMENT_PENDING'::character varying NOT NULL, + advanced_payment numeric(14,2) DEFAULT 0 NOT NULL, + remaining_payment numeric(14,2) DEFAULT 0 NOT NULL, + estimated_km numeric(10,2), + exact_km numeric(10,2), + vehicle_id uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + paid boolean DEFAULT false NOT NULL +); + +CREATE TABLE freight.first_mile_container_allocations ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + first_mile_id uuid NOT NULL, + container_id uuid NOT NULL, + vehicle_id uuid, + container_type text NOT NULL, + quantity integer DEFAULT 1 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.first_mile_vehicle_assignments ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + first_mile_id uuid NOT NULL, + vehicle_id uuid NOT NULL, + container_number character varying, + distance_km numeric(10,2), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + tons numeric(14,3), + quantity integer +); + +CREATE TABLE freight.fleet_events ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + event_type character varying NOT NULL, + vehicle_id uuid, + driver_id uuid, + first_mile_id uuid, + last_mile_id uuid, + from_value character varying, + to_value character varying, + label character varying, + metadata jsonb, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.fuel_consumption ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + vehicle_id uuid NOT NULL, + month date NOT NULL, + total_liters numeric(10,2) NOT NULL, + total_cost numeric(14,2) NOT NULL, + total_distance_km numeric(10,2) NOT NULL, + fuel_efficiency_km_per_l numeric(10,2), + number_of_purchases integer DEFAULT 0, + average_cost_per_liter numeric(10,2), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.fuel_purchases ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + vehicle_id uuid NOT NULL, + purchase_date timestamp with time zone NOT NULL, + liters numeric(10,2) NOT NULL, + cost_per_liter numeric(10,2) NOT NULL, + total_cost numeric(14,2) NOT NULL, + fuel_station character varying(255), + payment_method character varying(50) DEFAULT 'CASH'::character varying, + odometer_reading numeric(10,2), + driver_id uuid, + receipt_number character varying(255), + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.gps_devices ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + imei character varying(20) NOT NULL, + name character varying, + vehicle_id uuid, + status character varying(16) DEFAULT 'REGISTERED'::character varying NOT NULL, + last_seen_at timestamp with time zone, + last_lat numeric(10,6), + last_lng numeric(10,6), + last_speed numeric(6,2), + last_course integer, + last_fix_at timestamp with time zone, + voltage_level integer, + gsm_level integer, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.gps_positions ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + device_id uuid NOT NULL, + imei character varying(20) NOT NULL, + vehicle_id uuid, + lat numeric(10,6) NOT NULL, + lng numeric(10,6) NOT NULL, + speed numeric(6,2) DEFAULT 0 NOT NULL, + course integer DEFAULT 0 NOT NULL, + satellites integer DEFAULT 0 NOT NULL, + positioned boolean DEFAULT false NOT NULL, + gps_time timestamp with time zone NOT NULL, + alarm integer DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.import_customs_finalizations ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + booking_id uuid NOT NULL, + documents jsonb DEFAULT '{}'::jsonb NOT NULL, + declaration_serial_number character varying(120), + duties_taxes_notified_at timestamp with time zone, + duties_taxes_paid_at timestamp with time zone, + customs_risk character varying(12), + import_release_permitted_at timestamp with time zone, + completed_at timestamp with time zone, + performed_by character varying(120), + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.import_djibouti_operations ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + train_schedule_id uuid NOT NULL, + documents jsonb DEFAULT '{}'::jsonb NOT NULL, + gatepass_granted_at timestamp with time zone, + ready_for_loading_at timestamp with time zone, + loaded_on_train_at timestamp with time zone, + departed_from_djibouti_at timestamp with time zone, + load_list_generated_at timestamp with time zone, + performed_by character varying(120), + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.incidents ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + vehicle_id uuid, + driver_id uuid, + booking_id uuid, + type character varying NOT NULL, + severity character varying NOT NULL, + occurred_at timestamp with time zone NOT NULL, + location character varying, + description text NOT NULL, + damage_estimate numeric(14,2), + status character varying DEFAULT 'REPORTED'::character varying NOT NULL, + insurance_claim_number character varying, + reported_by character varying +); + +CREATE TABLE freight.interchange_document_items ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + interchange_document_id uuid NOT NULL, + booking_id uuid, + booking_reference character varying(64), + item_type character varying(20) NOT NULL, + booking_container_id uuid, + booking_cargo_id uuid, + container_number character varying(64), + seal_number character varying(100), + cargo_id uuid, + cargo_type character varying(255), + cargo_description text, + weight numeric(14,3), + quantity numeric(12,3), + package_count integer, + wagon_number character varying(80), + condition_status character varying(20) DEFAULT 'GOOD'::character varying NOT NULL, + damage_description text, + remarks text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.interchange_documents ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + document_no character varying(40) NOT NULL, + direction character varying(10) NOT NULL, + schedule_id uuid, + train_no character varying(40), + route_id uuid, + origin_facility_id uuid, + destination_facility_id uuid, + handover_location character varying(255) NOT NULL, + handover_from character varying(255) NOT NULL, + handover_to character varying(255) NOT NULL, + operator_name character varying(255), + port_operator_name character varying(255), + shipping_line_name character varying(255), + customs_reference character varying(120), + manifest_reference character varying(120), + status character varying(20) DEFAULT 'DRAFT'::character varying NOT NULL, + generated_at timestamp with time zone, + acknowledged_at timestamp with time zone, + generated_by character varying(120), + acknowledged_by character varying(120), + remarks text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.invoice_lines ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + invoice_id uuid NOT NULL, + charge_type character varying NOT NULL, + description character varying(255), + quantity numeric(12,2) DEFAULT 1 NOT NULL, + unit_rate numeric(14,2) DEFAULT 0 NOT NULL, + amount numeric(14,2) NOT NULL, + currency character varying(8) DEFAULT 'ETB'::character varying NOT NULL, + metadata jsonb, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.invoices ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + invoice_number character varying(64) NOT NULL, + company_id uuid NOT NULL, + company_profile_id uuid NOT NULL, + total_amount numeric(14,2) NOT NULL, + currency character varying(8) DEFAULT 'ETB'::character varying NOT NULL, + status freight.invoices_status_enum DEFAULT 'DRAFT'::freight.invoices_status_enum NOT NULL, + source character varying(255) NOT NULL, + source_id character varying(255) NOT NULL, + type character varying(255) NOT NULL, + issued_at timestamp with time zone, + payment_id uuid, + due_at timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + subtotal_amount numeric(14,2) DEFAULT 0 NOT NULL, + tax_amount numeric(14,2) DEFAULT 0 NOT NULL, + paid_amount numeric(14,2) DEFAULT 0 NOT NULL, + balance_amount numeric(14,2) DEFAULT 0 NOT NULL, + paid_at timestamp with time zone, + payments jsonb DEFAULT '[]'::jsonb NOT NULL +); + +CREATE TABLE freight.last_mile ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + booking_id uuid NOT NULL, + status character varying(30) DEFAULT 'PAYMENT_PENDING'::character varying NOT NULL, + advanced_payment numeric(14,2) DEFAULT 0 NOT NULL, + remaining_payment numeric(14,2) DEFAULT 0 NOT NULL, + estimated_km numeric(10,2), + exact_km numeric(10,2), + vehicle_id uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + paid boolean DEFAULT false NOT NULL, + arrived_at timestamp with time zone, + delivered_at timestamp with time zone, + pod_recipient_name character varying(160), + pod_signature_file_id uuid, + pod_photo_file_ids text[] DEFAULT '{}'::text[] NOT NULL, + pod_notes text, + pod_captured_at timestamp with time zone +); + +CREATE TABLE freight.last_mile_container_allocations ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + last_mile_id uuid NOT NULL, + container_id uuid NOT NULL, + vehicle_id uuid, + container_type text NOT NULL, + quantity integer DEFAULT 1 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.last_mile_vehicle_assignments ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + last_mile_id uuid NOT NULL, + vehicle_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + container_number character varying, + distance_km numeric(10,2), + arrived_at timestamp with time zone, + departed_at timestamp with time zone, + gross_weight_tons numeric(14,3), + net_weight_tons numeric(14,3), + destination_arrived_at timestamp with time zone, + returned_at timestamp with time zone +); + +CREATE TABLE freight.last_mile_vehicle_containers ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + assignment_id uuid NOT NULL, + last_mile_id uuid NOT NULL, + container_number character varying(32) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.locomotives ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + code character varying(32) NOT NULL, + name character varying(100), + max_pull_weight_tons numeric(10,3) NOT NULL, + status character varying(20) DEFAULT 'AVAILABLE'::character varying NOT NULL, + available_from timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + locomotive_type character varying(20) DEFAULT 'DIESEL'::character varying NOT NULL, + max_train_length_meters numeric(10,3) DEFAULT 760 NOT NULL, + power_kw numeric(10,3), + traction_force_kn numeric(10,3), + max_speed_kmh numeric(10,3), + current_yard_id uuid, + overage_tolerance_tons numeric(10,3), + overage_tolerance_meters numeric(10,3) +); + +CREATE TABLE freight.maintenance_costs ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + vehicle_id uuid NOT NULL, + maintenance_schedule_id uuid, + incurred_date timestamp with time zone NOT NULL, + cost_amount numeric(14,2) NOT NULL, + cost_type character varying NOT NULL, + description character varying NOT NULL, + service_provider character varying, + invoice_number character varying, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.maintenance_intervals ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + vehicle_id uuid NOT NULL, + maintenance_type character varying NOT NULL, + interval_km numeric(14,2), + interval_days integer, + description text, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + service_item character varying(120) +); + +CREATE TABLE freight.maintenance_schedules ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + vehicle_id uuid NOT NULL, + maintenance_type character varying NOT NULL, + description character varying NOT NULL, + scheduled_date timestamp with time zone NOT NULL, + completed_date timestamp with time zone, + estimated_cost numeric(14,2), + actual_cost numeric(14,2), + status character varying DEFAULT 'SCHEDULED'::character varying NOT NULL, + odometer_reading numeric, + service_provider character varying, + notes text, + next_due_km numeric, + next_due_date timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + due_notified_at timestamp with time zone, + service_item character varying(120) +); + +CREATE TABLE freight.notifications ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + recipient_user_id uuid NOT NULL, + audience character varying(20) NOT NULL, + type character varying(48) DEFAULT 'GENERIC'::character varying NOT NULL, + title character varying(200) NOT NULL, + body text NOT NULL, + link character varying, + data jsonb, + priority character varying(12) DEFAULT 'NORMAL'::character varying NOT NULL, + is_read boolean DEFAULT false NOT NULL, + read_at timestamp with time zone, + channels_sent jsonb, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.otp_verifications ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + phone character varying, + otp character varying NOT NULL, + verified boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + email character varying +); + +CREATE TABLE freight.parts ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + name character varying NOT NULL, + sku character varying, + category character varying, + quantity_in_stock integer DEFAULT 0 NOT NULL, + reorder_level integer DEFAULT 0 NOT NULL, + unit_cost numeric(14,2), + location character varying, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.payment_refunds ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + payment_id uuid NOT NULL, + amount_minor integer NOT NULL, + reason character varying(255), + provider_refund_id character varying(255), + status character varying(50) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL +); + +CREATE TABLE freight.payment_webhook_events ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + provider freight.payment_webhook_method_enum NOT NULL, + external_event_id character varying(255) NOT NULL, + merchant_order_id character varying(255), + provider_txn_id character varying(255), + signature_valid boolean NOT NULL, + status character varying(100) NOT NULL, + payload jsonb NOT NULL, + received_at timestamp without time zone DEFAULT now() NOT NULL, + processed_at timestamp without time zone, + processing_error text +); + +CREATE TABLE freight.payments ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + ref_id character varying(255) NOT NULL, + type character varying(50) NOT NULL, + method freight.payments_method_enum NOT NULL, + currency freight.payments_currency_enum NOT NULL, + amount numeric NOT NULL, + raw_initiation jsonb DEFAULT '{}'::jsonb NOT NULL, + client_action jsonb, + merchant_order_id character varying(255) NOT NULL, + transaction_id character varying(255), + status freight.payments_status_enum DEFAULT 'action-required'::freight.payments_status_enum NOT NULL, + paid_at timestamp without time zone, + refunded_at timestamp without time zone, + expires_at timestamp without time zone, + failer_code character varying(30), + failer_message character varying(255), + reason character varying(255), + created_at timestamp without time zone DEFAULT now() NOT NULL, + reference_type character varying(40) +); + +CREATE TABLE freight.priority_configs ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + type character varying(20) NOT NULL, + label character varying(100) NOT NULL, + currency character varying(5), + min_wagon_count integer NOT NULL, + max_wagon_count integer NOT NULL, + score_points integer DEFAULT 0 NOT NULL, + is_active boolean DEFAULT false NOT NULL, + display_order integer DEFAULT 1 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + CONSTRAINT chk_currency_for_type CHECK (((((type)::text = 'WAGON'::text) AND (currency IS NULL)) OR (((type)::text = 'CURRENCY'::text) AND (currency IS NOT NULL)) OR (((type)::text = 'CUSTOMS'::text) AND (currency IS NULL)))), + CONSTRAINT chk_wagon_range CHECK ((min_wagon_count <= max_wagon_count)), + CONSTRAINT priority_configs_type_check CHECK (((type)::text = ANY ((ARRAY['WAGON'::character varying, 'CURRENCY'::character varying, 'CUSTOMS'::character varying])::text[]))) +); + +CREATE TABLE freight.priority_rule_change_requests ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + action character varying(10) NOT NULL, + priority_config_id uuid, + payload jsonb, + status character varying(10) DEFAULT 'PENDING'::character varying NOT NULL, + requested_by_user_id uuid, + decided_by_user_id uuid, + decided_at timestamp with time zone, + decision_note text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.priority_rules ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + is_active boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + code character varying(40) NOT NULL, + label character varying(100) NOT NULL, + score integer DEFAULT 0 NOT NULL, + condition_currency character varying(5) +); + +CREATE TABLE freight.rate_change_requests ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + rate_id uuid NOT NULL, + payload jsonb NOT NULL, + previous_values jsonb NOT NULL, + status character varying(10) DEFAULT 'PENDING'::character varying NOT NULL, + requested_by_user_id uuid, + decided_by_user_id uuid, + decided_at timestamp with time zone, + decision_note text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.rates ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + rate_type character varying(50) NOT NULL, + container_type_id uuid, + trade_direction character varying(10), + currency character varying(5) NOT NULL, + rate_value numeric(14,4) NOT NULL, + rate_unit character varying(30) NOT NULL, + status character varying(20) DEFAULT 'DRAFT'::character varying NOT NULL, + proposed_by_staff_id uuid NOT NULL, + approved_by_ceo_id uuid, + approved_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + applies_to character varying(20) DEFAULT 'OTHER'::character varying NOT NULL, + trigger character varying(20) DEFAULT 'ALWAYS'::character varying NOT NULL, + cargo_type_id uuid, + origin_yard_id uuid, + destination_yard_id uuid, + CONSTRAINT "CK_rates_yard_scope" CHECK (((deleted_at IS NOT NULL) OR ((status)::text = 'SUPERSEDED'::text) OR +CASE + WHEN ((((trigger)::text = 'ALWAYS'::text) AND ((applies_to)::text = ANY ((ARRAY['BULK'::character varying, 'CONTAINER'::character varying, 'INTERCITY'::character varying])::text[]))) OR ((trigger)::text = ANY ((ARRAY['CUSTOMS_CLEARANCE'::character varying, 'WITH_RETURN'::character varying])::text[]))) THEN ((origin_yard_id IS NOT NULL) AND (destination_yard_id IS NOT NULL)) + ELSE ((origin_yard_id IS NULL) AND (destination_yard_id IS NULL)) +END)) +); + +CREATE TABLE freight.route_milestones ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + route_id uuid NOT NULL, + yard_id uuid NOT NULL, + sequence_no integer NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + distance_km numeric(10,2) +); + +CREATE TABLE freight.routes ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + origin_yard_id uuid NOT NULL, + destination_yard_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + status character varying(32) DEFAULT 'AVAILABLE'::character varying NOT NULL, + direction character varying(10) NOT NULL, + CONSTRAINT chk_routes_direction CHECK (((direction)::text = ANY ((ARRAY['IMPORT'::character varying, 'EXPORT'::character varying, 'DOMESTIC'::character varying])::text[]))) +); + +CREATE TABLE freight.saved_signatures ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + user_id uuid NOT NULL, + signer_display_name character varying(200) NOT NULL, + signature_file_id uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + stamp_file_id uuid +); + +CREATE TABLE freight.schedule_wagon_adjustment_logs ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + train_schedule_id uuid NOT NULL, + train_id uuid NOT NULL, + action character varying(10) NOT NULL, + wagon_id uuid NOT NULL, + wagon_number character varying(50) NOT NULL, + adjusted_by_user_id uuid, + occurred_at timestamp with time zone DEFAULT now() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + yard_id uuid +); + +CREATE TABLE freight.scheduling_events ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + train_schedule_id uuid NOT NULL, + trigger character varying(40) NOT NULL, + actor_user_id uuid, + reason text, + plan_snapshot jsonb DEFAULT '{}'::jsonb NOT NULL, + displaced_booking_ids jsonb DEFAULT '[]'::jsonb NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE SEQUENCE freight.seq_company_profile_ex + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE SEQUENCE freight.seq_company_profile_ffe + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE SEQUENCE freight.seq_company_profile_fwj + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE SEQUENCE freight.seq_company_profile_im + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE SEQUENCE freight.seq_company_profile_tr + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +CREATE TABLE freight.service_types ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + service_name character varying(255) NOT NULL, + description text, + can_be_booked_alone boolean DEFAULT true NOT NULL, + includes_first_mile boolean DEFAULT false NOT NULL, + includes_last_mile boolean DEFAULT false NOT NULL, + includes_customs boolean DEFAULT false NOT NULL, + is_active boolean DEFAULT true NOT NULL, + display_order integer DEFAULT 1 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + code character varying(50) NOT NULL +); + +CREATE TABLE freight.shipping_lines ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + code character varying(20) NOT NULL, + label character varying(100) NOT NULL, + mapped_to_code character varying(20), + show_extra_fee_notice boolean DEFAULT false NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.support_conversations ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + company_id uuid NOT NULL, + company_name character varying(200), + created_by_user_id uuid, + last_message_at timestamp with time zone, + last_message_preview character varying(280), + last_message_author_role character varying(12), + customer_last_read_at timestamp with time zone, + agent_last_read_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.support_messages ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + conversation_id uuid NOT NULL, + author_user_id uuid NOT NULL, + author_role character varying(12) NOT NULL, + author_name character varying(200), + body text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.tracking_events ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + consignment_id uuid NOT NULL, + location character varying(256) NOT NULL, + status freight.tracking_events_status_enum NOT NULL, + occurred_at timestamp with time zone NOT NULL, + description text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.train_checkpoint_events ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + train_schedule_id uuid NOT NULL, + yard_id uuid NOT NULL, + sequence_no integer NOT NULL, + kind character varying(20) NOT NULL, + occurred_at timestamp with time zone DEFAULT now() NOT NULL, + note text, + recorded_by_user_id uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.train_composition_removal_logs ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + schedule_id uuid NOT NULL, + booking_id uuid NOT NULL, + booking_reference character varying(64), + removed_by_user_id uuid, + removed_at timestamp with time zone DEFAULT now() NOT NULL, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.train_locomotives ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + train_id uuid NOT NULL, + locomotive_id uuid NOT NULL, + sequence_no integer DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.train_schedule_bookings ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + train_schedule_id uuid NOT NULL, + booking_id uuid NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + loading_status character varying(20) DEFAULT 'UNLOADED'::character varying NOT NULL +); + +CREATE TABLE freight.train_schedules ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + train_set_id uuid NOT NULL, + origin_station_id uuid NOT NULL, + destination_station_id uuid NOT NULL, + scheduled_departure_date timestamp with time zone NOT NULL, + scheduled_arrival_date timestamp with time zone, + status character varying(20) DEFAULT 'DRAFT'::character varying NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + route_id uuid, + train_number character varying(20), + direction character varying(10), + actual_departure_at timestamp with time zone, + actual_arrival_at timestamp with time zone, + prepared_by_user_id uuid, + checked_by_user_id uuid, + max_wagons integer DEFAULT 53 NOT NULL, + booking_window_status character varying(10) DEFAULT 'OPEN'::character varying NOT NULL, + window_phase character varying(20), + window_opens_at timestamp with time zone, + window_closes_at timestamp with time zone, + doc_review_ends_at timestamp with time zone, + doc_review_completed_at timestamp with time zone, + payment_phase_ends_at timestamp with time zone, + booking_cycle_no integer DEFAULT 0 NOT NULL, + rule_window_open_hour integer, + rule_window_duration_hours numeric(6,4), + rule_reopen_delay_minutes integer, + rule_import_window_lead_days integer, + rule_export_booking_lead_hours integer, + rule_window_close_hour integer, + reference character varying(20), + wagon_allocation_snapshot jsonb, + rule_import_close_offset_minutes integer, + rule_export_close_offset_minutes integer, + reverse_wagon_order boolean DEFAULT false NOT NULL, + rule_payment_window_minutes integer, + window_rule_custom boolean DEFAULT false NOT NULL +); + +CREATE TABLE freight.train_scheduling_global_rules ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + max_train_length_meters numeric(10,2) DEFAULT 760 NOT NULL, + max_train_weight_tons numeric(10,3) DEFAULT 3500 NOT NULL, + max_wagons_per_train integer DEFAULT 53 NOT NULL, + max_20ft_container_weight_tons numeric(8,3) DEFAULT 30 NOT NULL, + max_20ft_pair_weight_diff_tons numeric(8,3) DEFAULT 10 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + import_window_lead_days integer DEFAULT 3 NOT NULL, + export_booking_lead_hours integer DEFAULT 24 NOT NULL, + window_open_hour integer DEFAULT 8 NOT NULL, + window_duration_hours numeric(6,4) DEFAULT 3 NOT NULL, + doc_review_minutes integer DEFAULT 30 NOT NULL, + payment_window_minutes integer DEFAULT 60 NOT NULL, + window_close_hour integer DEFAULT 17 NOT NULL, + import_close_offset_minutes integer, + export_close_offset_minutes integer, + export_payment_window_minutes integer DEFAULT 60 NOT NULL +); + +CREATE TABLE freight.train_set_locomotives ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + train_set_id uuid NOT NULL, + locomotive_id uuid NOT NULL, + sequence_no integer DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.train_set_wagons ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + train_set_id uuid NOT NULL, + wagon_type_id uuid NOT NULL, + sequence_no integer NOT NULL, + capacity_tons numeric(10,3) NOT NULL, + length_meters numeric(10,3) NOT NULL, + assigned_weight_tons numeric(10,3) DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + physical_wagon_id uuid, + status character varying(20) DEFAULT 'PLANNED'::character varying NOT NULL, + board_yard_id uuid, + alight_yard_id uuid +); + +CREATE TABLE freight.train_sets ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + locomotive_id uuid NOT NULL, + total_weight_tons numeric(10,3) NOT NULL, + total_length_meters numeric(10,3) NOT NULL, + wagon_count integer NOT NULL, + status character varying(20) DEFAULT 'DRAFT'::character varying NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + train_id uuid +); + +CREATE TABLE freight.trains ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + code character varying(32) NOT NULL, + capacity_tons numeric(10,2) DEFAULT 0 NOT NULL, + status freight.train_status DEFAULT 'AVAILABLE'::freight.train_status NOT NULL, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + train_number character varying(20), + train_name character varying(100), + route_id uuid, + origin_station_id uuid, + destination_station_id uuid, + departure_time timestamp with time zone, + arrival_time timestamp with time zone, + locomotive_number character varying(50), + remarks text, + current_yard_id uuid, + import_train_number character varying(20), + export_train_number character varying(20) +); + +CREATE TABLE freight.transit_agents ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + name character varying(150) NOT NULL, + valid_from date NOT NULL, + valid_to date NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.truck_types ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + code character varying(32) NOT NULL, + name character varying(100) NOT NULL, + capacity_tons numeric(10,3), + has_trailer boolean DEFAULT false NOT NULL, + description text, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.user_trade_access ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + user_id uuid NOT NULL, + directions text DEFAULT ''::text NOT NULL, + updated_by_id uuid, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.vehicles ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + plate_number character varying NOT NULL, + registration_number character varying NOT NULL, + vehicle_type character varying NOT NULL, + manufacturer character varying NOT NULL, + model character varying NOT NULL, + year integer NOT NULL, + fuel_type character varying NOT NULL, + capacity numeric NOT NULL, + status character varying DEFAULT 'ACTIVE'::character varying NOT NULL, + description text, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted_at timestamp without time zone, + assigned_driver_id uuid, + assigned_driver_name character varying, + code character varying, + power_plate_no character varying, + trailer_plate_no character varying, + estimated_distance_km numeric, + actual_distance_km numeric, + location_id uuid, + availability character varying DEFAULT 'FREE'::character varying, + vin character varying, + ownership character varying, + insurance_expiry date, + registration_expiry date, + next_inspection_date date, + price_per_km numeric(14,2), + currency character varying(8) DEFAULT 'ETB'::character varying NOT NULL, + truck_type_id uuid +); + +CREATE TABLE freight.vendors ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + name character varying NOT NULL, + type character varying, + contact_person character varying, + phone character varying, + email character varying, + address character varying, + is_active boolean DEFAULT true NOT NULL +); + +CREATE TABLE freight.wagon_allocation_bulk_loads ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + wagon_booking_allocation_id uuid NOT NULL, + booking_id uuid NOT NULL, + cargo_type_id uuid, + cargo_description text, + pricing_unit character varying(20) DEFAULT 'PER_TON'::character varying NOT NULL, + quantity numeric(12,3) DEFAULT 0 NOT NULL, + weight_tons numeric(10,3) DEFAULT 0 NOT NULL, + truck_plate_number character varying(32), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.wagon_allocation_container_items ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + wagon_booking_allocation_id uuid NOT NULL, + booking_container_id uuid, + container_id uuid, + container_number character varying(64), + container_type_id uuid, + position_on_wagon smallint, + seal_number character varying(64), + chassis_number character varying(64), + gross_weight_tons numeric(10,3), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.wagon_booking_allocations ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + train_set_wagon_id uuid NOT NULL, + booking_id uuid NOT NULL, + allocated_weight_tons numeric(10,3) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + load_type character varying(20), + status character varying(20) DEFAULT 'PLANNED'::character varying NOT NULL, + confirmed_at timestamp with time zone, + confirmed_by_user_id uuid +); + +CREATE TABLE freight.wagon_movements ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + wagon_id uuid NOT NULL, + from_yard_id uuid, + to_yard_id uuid NOT NULL, + train_schedule_id uuid, + booking_id uuid, + kind character varying(30) NOT NULL, + moved_by_user_id uuid, + occurred_at timestamp with time zone DEFAULT now() NOT NULL, + note text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + transfer_request_id uuid +); + +CREATE TABLE freight.wagon_transfer_requests ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + from_yard_id uuid NOT NULL, + to_yard_id uuid NOT NULL, + wagon_type_id uuid NOT NULL, + quantity integer NOT NULL, + status character varying(20) DEFAULT 'PENDING'::character varying NOT NULL, + requested_by_user_id uuid, + fulfilled_by_user_id uuid, + fulfilled_at timestamp with time zone, + note text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + reason text, + fulfilled_quantity integer DEFAULT 0 NOT NULL, + closed_short_at timestamp with time zone, + closed_short_by_user_id uuid, + CONSTRAINT chk_wtr_quantity CHECK ((quantity > 0)) +); + +CREATE TABLE freight.wagon_types ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + code character varying(32) NOT NULL, + name character varying(100) NOT NULL, + capacity_tons numeric(10,3) NOT NULL, + length_meters numeric(10,3) NOT NULL, + supported_load_types text[] DEFAULT '{}'::text[] NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + equated_length_m numeric(10,3), + tare_weight_tons numeric(10,3) NOT NULL, + supports_container boolean DEFAULT false NOT NULL, + max_container_gross_t numeric(10,3) +); + +CREATE TABLE freight.wagons ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + wagon_number character varying NOT NULL, + wagon_type_id uuid NOT NULL, + train_id uuid, + sequence_number integer, + status character varying DEFAULT 'AVAILABLE'::character varying NOT NULL, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + current_location_yard_id uuid, + train_set_wagon_id uuid, + current_train_schedule_id uuid, + current_yard_id uuid, + export_train_number character varying(20), + import_train_number character varying(20) +); + +CREATE TABLE freight.warehouse_accrual_acks ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + inventory_id uuid NOT NULL, + acknowledged_by uuid, + acknowledged_at timestamp with time zone DEFAULT now() NOT NULL, + snooze_until timestamp with time zone, + note text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + +CREATE TABLE freight.warehouse_activity_log ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + inventory_id uuid, + warehouse_id uuid, + activity_type character varying(40) NOT NULL, + description text, + performed_by character varying(120), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.warehouse_allocation_rules ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + name character varying(160) NOT NULL, + priority integer DEFAULT 100 NOT NULL, + freight_type character varying(16), + trade_direction character varying(16), + cargo_type_code character varying(50), + container_status character varying(24), + requires_inspection boolean, + target_facility_code character varying(40), + target_yard_code character varying(40) NOT NULL, + target_warehouse_code character varying(40), + target_zone_code character varying(40), + storage_type character varying(80), + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.warehouse_fee_rules ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + name character varying(160) NOT NULL, + rule_type character varying(20) NOT NULL, + priority integer DEFAULT 100 NOT NULL, + freight_type character varying(16), + trade_direction character varying(16), + cargo_type_code character varying(50), + container_type character varying(40), + facility_id uuid, + warehouse_id uuid, + yard_id uuid, + zone_id uuid, + free_days integer DEFAULT 0 NOT NULL, + rate_per_day numeric(14,2) DEFAULT 0 NOT NULL, + tiers jsonb DEFAULT '[]'::jsonb NOT NULL, + currency character varying(8) DEFAULT 'USD'::character varying NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + basis character varying(20), + free_hours integer, + vehicle_type character varying(32) +); + +CREATE TABLE freight.warehouse_inspection_reports ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + inventory_id uuid NOT NULL, + booking_id uuid, + customer_id uuid, + report_type character varying(32) DEFAULT 'INSPECTION'::character varying NOT NULL, + inspection_status character varying(20) DEFAULT 'NEEDS_REVIEW'::character varying NOT NULL, + has_damage boolean DEFAULT false NOT NULL, + damage_description text, + has_weight_loss boolean DEFAULT false NOT NULL, + expected_weight numeric(14,3), + actual_weight numeric(14,3), + weight_loss numeric(14,3), + weight_loss_unit character varying(12), + has_missing_items boolean DEFAULT false NOT NULL, + missing_items_description text, + remarks text, + inspected_by_id uuid, + inspected_at timestamp with time zone, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.warehouse_inventory ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + warehouse_id uuid NOT NULL, + yard_id uuid NOT NULL, + zone_id uuid NOT NULL, + booking_id uuid, + cargo_id uuid, + container_id uuid, + goods_id uuid, + quantity numeric(12,3) DEFAULT 0 NOT NULL, + weight numeric(14,3) DEFAULT 0 NOT NULL, + volume numeric(12,3), + status character varying(32) DEFAULT 'RECEIVED'::character varying NOT NULL, + inspection_status character varying(20), + arrived_at timestamp with time zone, + inspected_at timestamp with time zone, + ready_for_loading_at timestamp with time zone, + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + stored_at timestamp with time zone, + reserved_at timestamp with time zone, + loaded_at timestamp with time zone, + dispatched_at timestamp with time zone, + inspection_started_at timestamp with time zone, + inspection_completed_at timestamp with time zone, + ready_for_pickup_at timestamp with time zone, + release_date timestamp with time zone, + gate_cleared_at timestamp with time zone, + release_order_reference character varying(100), + delivered_at timestamp with time zone, + unloaded_at timestamp with time zone, + grn_number character varying(100) +); + +CREATE TABLE freight.warehouse_inventory_movement ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + inventory_id uuid NOT NULL, + from_warehouse_id uuid NOT NULL, + from_yard_id uuid NOT NULL, + from_zone_id uuid NOT NULL, + to_warehouse_id uuid NOT NULL, + to_yard_id uuid NOT NULL, + to_zone_id uuid NOT NULL, + remarks text, + moved_by character varying(120), + moved_at timestamp with time zone DEFAULT now() NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.warehouse_loadings ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + warehouse_inventory_id uuid NOT NULL, + booking_id uuid, + wagon_id uuid, + loaded_at timestamp with time zone DEFAULT now() NOT NULL, + loaded_by character varying(120), + loaded_weight numeric(14,3), + notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + train_schedule_id uuid +); + +CREATE TABLE freight.warehouse_yard_cargo_types ( + yard_id uuid NOT NULL, + cargo_type_id uuid NOT NULL +); + +CREATE TABLE freight.warehouse_yards ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + warehouse_id uuid NOT NULL, + name character varying(160) NOT NULL, + code character varying(40) NOT NULL, + type character varying(32) NOT NULL, + capacity_weight numeric(14,3), + capacity_containers integer, + current_weight numeric(14,3) DEFAULT 0 NOT NULL, + current_containers integer DEFAULT 0 NOT NULL, + status character varying(16) DEFAULT 'ACTIVE'::character varying NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + max_weight numeric(14,3), + max_volume numeric(14,3), + current_volume numeric(14,3) DEFAULT 0 NOT NULL, + direction character varying(10) +); + +CREATE TABLE freight.warehouse_zones ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + yard_id uuid NOT NULL, + name character varying(160) NOT NULL, + code character varying(40) NOT NULL, + type character varying(32) NOT NULL, + capacity_weight numeric(14,3), + capacity_containers integer, + current_weight numeric(14,3) DEFAULT 0 NOT NULL, + current_containers integer DEFAULT 0 NOT NULL, + status character varying(16) DEFAULT 'ACTIVE'::character varying NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + max_weight numeric(14,3), + max_volume numeric(14,3), + current_volume numeric(14,3) DEFAULT 0 NOT NULL +); + +CREATE TABLE freight.warehouses ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + name character varying(160) NOT NULL, + code character varying(40) NOT NULL, + type character varying(32) NOT NULL, + station_id uuid, + location_name character varying(200), + capacity_weight numeric(14,3), + capacity_containers integer, + current_weight numeric(14,3) DEFAULT 0 NOT NULL, + current_containers integer DEFAULT 0 NOT NULL, + status character varying(16) DEFAULT 'ACTIVE'::character varying NOT NULL, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + max_weight numeric(14,3), + max_volume numeric(14,3), + current_volume numeric(14,3) DEFAULT 0 NOT NULL, + facility_id uuid +); + +CREATE TABLE freight.warranties ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + vehicle_id uuid NOT NULL, + component character varying NOT NULL, + provider character varying, + start_date date, + expiry_date date NOT NULL, + coverage_notes text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.weight_limit_rules ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + container_type_id uuid NOT NULL, + trade_direction freight.weight_limit_rules_trade_direction_enum NOT NULL, + max_vgm_tons numeric(8,3) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + max_capacity_tons numeric(8,3) +); + +CREATE TABLE freight.work_orders ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + vehicle_id uuid NOT NULL, + title character varying NOT NULL, + description text, + status character varying DEFAULT 'OPEN'::character varying NOT NULL, + priority character varying DEFAULT 'MEDIUM'::character varying NOT NULL, + assigned_to character varying, + opened_at timestamp with time zone DEFAULT now() NOT NULL, + closed_at timestamp with time zone, + labor_cost numeric(14,2), + parts_cost numeric(14,2), + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.yard_distances ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + from_yard_id uuid NOT NULL, + to_yard_id uuid NOT NULL, + distance_km numeric(10,2) NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone +); + +CREATE TABLE freight.yard_facilities ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + yard_id uuid NOT NULL, + has_warehouse boolean DEFAULT false NOT NULL, + equipment_notes text, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + handles_container boolean DEFAULT true NOT NULL, + handles_bulk boolean DEFAULT true NOT NULL, + has_container_facility_origin boolean DEFAULT false NOT NULL, + has_bulk_facility_origin boolean DEFAULT false NOT NULL, + has_container_facility_destination boolean DEFAULT false NOT NULL, + has_bulk_facility_destination boolean DEFAULT false NOT NULL +); + +CREATE TABLE freight.yards ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + code character varying(40) NOT NULL, + label character varying(100) NOT NULL, + country character varying(50) NOT NULL, + is_active boolean DEFAULT true NOT NULL, + display_order integer DEFAULT 1 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + has_facility boolean DEFAULT false NOT NULL, + CONSTRAINT chk_yards_country CHECK (((country)::text = ANY ((ARRAY['Ethiopia'::character varying, 'Djibouti'::character varying])::text[]))) +); + +ALTER TABLE ONLY freight.approval_rules + ADD CONSTRAINT "PK_048d2ffdf7169337b4cc237d772" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.company_change_request + ADD CONSTRAINT "PK_0a57bba5aaf77b376e7fee5fcf8" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.interchange_document_items + ADD CONSTRAINT "PK_163803862ca83ef98678e39a43b" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.company_profiles + ADD CONSTRAINT "PK_1980200b310bd1e2ac86aa1ae4a" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.service_types + ADD CONSTRAINT "PK_1dc93417a097cdee3491f39d7cc" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.train_composition_removal_logs + ADD CONSTRAINT "PK_29f7cdb5e0ae32ef1b96155fc9a" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.rates + ADD CONSTRAINT "PK_2c804ed4019b80ce48eedba5cec" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.facilities + ADD CONSTRAINT "PK_2e6c685b2e1195e6d6394a22bc7" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouse_allocation_rules + ADD CONSTRAINT "PK_2e7632f009769d4da579c14f151" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.shipping_lines + ADD CONSTRAINT "PK_336306f24563d798ec54eedca0c" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.import_djibouti_operations + ADD CONSTRAINT "PK_3456f58d03f6719b38c70018ab6" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.yards + ADD CONSTRAINT "PK_3aa7dacb4c4fb065b1e2f8dfb5a" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_document_review + ADD CONSTRAINT "PK_3d7705ea54a01cb4d6db5b743cd" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.exchange_settings + ADD CONSTRAINT "PK_48f5730ce6881caf75f279d0a5d" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.container_types + ADD CONSTRAINT "PK_50e6b62fcd07ba58bdb6415d47c" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouse_inspection_reports + ADD CONSTRAINT "PK_625df1e3a62e5aa4c3f14e7172e" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_requests + ADD CONSTRAINT "PK_62c29ee249979fe0bcdcde33dae" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.first_mile + ADD CONSTRAINT "PK_72bc92607db5356f03c2aaeb6ec" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.first_mile_container_allocations + ADD CONSTRAINT "PK_8be4b0df481f8a1555a4fb9d05d" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.last_mile + ADD CONSTRAINT "PK_8be9fbd78fd39e78b6d30ab119b" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_rate_snapshot + ADD CONSTRAINT "PK_8ca5cf692c4c3e95d91a7343117" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.otp_verifications + ADD CONSTRAINT pk_otp_verifications PRIMARY KEY (id); + +ALTER TABLE ONLY freight.user_trade_access + ADD CONSTRAINT "PK_94e21d7bfb20e57940e3255df87" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.priority_rules + ADD CONSTRAINT "PK_95c73e8a6e80f29d81edf66dbe2" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.interchange_documents + ADD CONSTRAINT "PK_95e5a5484c57ecc6f4a2927ddb4" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.last_mile_container_allocations + ADD CONSTRAINT "PK_9d5180799a6dd494156ec980dfb" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_container + ADD CONSTRAINT "PK_c1805410bccc51530297b2960ef" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.company_revisions + ADD CONSTRAINT "PK_c8ed8b5e0e0d93edffd43c8c024" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.cargo_type_wagon_types + ADD CONSTRAINT "PK_cargo_type_wagon_types" PRIMARY KEY (cargo_type_id, wagon_type_id); + +ALTER TABLE ONLY freight.djibouti_import_incidents + ADD CONSTRAINT "PK_cf3871f27f8bd0db9176f09bf95" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.container_type_wagon_types + ADD CONSTRAINT "PK_container_type_wagon_types" PRIMARY KEY (container_type_id, wagon_type_id); + +ALTER TABLE ONLY freight.warehouse_fee_rules + ADD CONSTRAINT "PK_d24f444e0cb6841c1b19342e134" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.companies + ADD CONSTRAINT "PK_d4bc3e82a314fa9e29f652c2c22" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.weight_limit_rules + ADD CONSTRAINT "PK_d82c70fa7e878ca301e2aaff709" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_cargo_modifier + ADD CONSTRAINT "PK_d949ef8f1b862135ad7e4afdbf1" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.cargo_types + ADD CONSTRAINT "PK_db1aa5f07b0c5ea996eaea03394" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.dropdown_options + ADD CONSTRAINT "PK_dropdown_options" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.dropdown_settings + ADD CONSTRAINT "PK_dropdown_settings" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.ff_clients + ADD CONSTRAINT "PK_eb91bf3ee74a361acee369dbda2" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.external_profiles + ADD CONSTRAINT "PK_f01bade59a434fa68039620ccb9" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.import_customs_finalizations + ADD CONSTRAINT "PK_f67c60e65b732153fc050dd166c" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.empty_container_returns + ADD CONSTRAINT "PK_f82bda09e0ff411811193579948" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_container_allocations + ADD CONSTRAINT "PK_fb0dee3634a65d2ebd734ff46e3" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.schedule_wagon_adjustment_logs + ADD CONSTRAINT "PK_schedule_wagon_adjustment_logs" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.train_locomotives + ADD CONSTRAINT "PK_train_locomotives" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.train_set_locomotives + ADD CONSTRAINT "PK_train_set_locomotives" PRIMARY KEY (id); + +ALTER TABLE ONLY freight.companies + ADD CONSTRAINT "UQ_1b3738f728d42fc9aca8d4ac19b" UNIQUE (tin); + +ALTER TABLE ONLY freight.ff_clients + ADD CONSTRAINT "UQ_4107fef2abf7128f890ef276244" UNIQUE (forwarder_company_id, client_company_id); + +ALTER TABLE ONLY freight.otp_verifications + ADD CONSTRAINT uq_otp_verifications_phone UNIQUE (phone); + +ALTER TABLE ONLY freight.company_profiles + ADD CONSTRAINT "UQ_539ff8f8225951adb2ef76b0313" UNIQUE (reference); + +ALTER TABLE ONLY freight.import_djibouti_operations + ADD CONSTRAINT "UQ_6c3e3046300b152a13505bf8ffd" UNIQUE (train_schedule_id); + +ALTER TABLE ONLY freight.first_mile_vehicle_assignments + ADD CONSTRAINT "UQ_FIRST_MILE_VEHICLE" UNIQUE (first_mile_id, vehicle_id); + +ALTER TABLE ONLY freight.last_mile_vehicle_assignments + ADD CONSTRAINT "UQ_LAST_MILE_VEHICLE" UNIQUE (last_mile_id, vehicle_id); + +ALTER TABLE ONLY freight.import_customs_finalizations + ADD CONSTRAINT "UQ_ad87af234cc0068a05fa7bd2335" UNIQUE (booking_id); + +ALTER TABLE ONLY freight.approval_rules + ADD CONSTRAINT "UQ_approval_rules_chain_step" UNIQUE (requires_director_approval, step_order); + +ALTER TABLE ONLY freight.interchange_documents + ADD CONSTRAINT "UQ_b44369ad5b13e0d897487fc4284" UNIQUE (document_no); + +ALTER TABLE ONLY freight.facilities + ADD CONSTRAINT "UQ_bf808b325b190fb9049254ba55e" UNIQUE (code); + +ALTER TABLE ONLY freight.user_trade_access + ADD CONSTRAINT "UQ_e54889f850f4db112a77b9e8431" UNIQUE (user_id); + +ALTER TABLE ONLY freight.asset_acquisitions + ADD CONSTRAINT asset_acquisitions_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.asset_disposals + ADD CONSTRAINT asset_disposals_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_batch_offers + ADD CONSTRAINT booking_batch_offers_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_container_units + ADD CONSTRAINT booking_container_units_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_contract_signatures + ADD CONSTRAINT booking_contract_signatures_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_handovers + ADD CONSTRAINT booking_handovers_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_review_note + ADD CONSTRAINT booking_review_note_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT bookings_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT bookings_reference_key UNIQUE (reference); + +ALTER TABLE ONLY freight.cargoes + ADD CONSTRAINT cargoes_cargo_reference_key UNIQUE (cargo_reference); + +ALTER TABLE ONLY freight.cargoes + ADD CONSTRAINT cargoes_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.clearance_incidents + ADD CONSTRAINT clearance_incidents_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.clearance_milestones + ADD CONSTRAINT clearance_milestones_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.compliance_records + ADD CONSTRAINT compliance_records_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.containers + ADD CONSTRAINT containers_container_number_key UNIQUE (container_number); + +ALTER TABLE ONLY freight.containers + ADD CONSTRAINT containers_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_approval_steps + ADD CONSTRAINT contract_approval_steps_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_cargo_scope + ADD CONSTRAINT contract_cargo_scope_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_clearance_cycles + ADD CONSTRAINT contract_clearance_cycles_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_document_review + ADD CONSTRAINT contract_document_review_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_document_revisions + ADD CONSTRAINT contract_document_revisions_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_rate_snapshots + ADD CONSTRAINT contract_rate_snapshots_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_review_notes + ADD CONSTRAINT contract_review_notes_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_routes + ADD CONSTRAINT contract_routes_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_signatures + ADD CONSTRAINT contract_signatures_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contract_templates + ADD CONSTRAINT contract_templates_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contracts + ADD CONSTRAINT contracts_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.contracts + ADD CONSTRAINT contracts_reference_key UNIQUE (reference); + +ALTER TABLE ONLY freight.customer_truck_assignments + ADD CONSTRAINT customer_truck_assignments_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.customer_truck_containers + ADD CONSTRAINT customer_truck_containers_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.customers + ADD CONSTRAINT customers_email_key UNIQUE (email); + +ALTER TABLE ONLY freight.customers + ADD CONSTRAINT customers_fan_number_key UNIQUE (fan_number); + +ALTER TABLE ONLY freight.customers + ADD CONSTRAINT customers_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.customers + ADD CONSTRAINT customers_tin_number_key UNIQUE (tin_number); + +ALTER TABLE ONLY freight.drivers + ADD CONSTRAINT drivers_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.facility_handling_events + ADD CONSTRAINT facility_handling_events_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.fayda_verification_sessions + ADD CONSTRAINT fayda_verification_sessions_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.fayda_verification_sessions + ADD CONSTRAINT fayda_verification_sessions_state_key UNIQUE (state); + +ALTER TABLE ONLY freight.file_upload_fields + ADD CONSTRAINT file_upload_fields_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.file_upload_settings + ADD CONSTRAINT file_upload_settings_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.files + ADD CONSTRAINT files_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.first_mile_vehicle_assignments + ADD CONSTRAINT first_mile_vehicle_assignments_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.fleet_events + ADD CONSTRAINT fleet_events_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.gps_devices + ADD CONSTRAINT gps_devices_imei_key UNIQUE (imei); + +ALTER TABLE ONLY freight.gps_devices + ADD CONSTRAINT gps_devices_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.gps_positions + ADD CONSTRAINT gps_positions_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.incidents + ADD CONSTRAINT incidents_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.last_mile_vehicle_assignments + ADD CONSTRAINT last_mile_vehicle_assignments_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.last_mile_vehicle_containers + ADD CONSTRAINT last_mile_vehicle_containers_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.locomotives + ADD CONSTRAINT locomotives_code_key UNIQUE (code); + +ALTER TABLE ONLY freight.locomotives + ADD CONSTRAINT locomotives_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.maintenance_costs + ADD CONSTRAINT maintenance_costs_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.maintenance_intervals + ADD CONSTRAINT maintenance_intervals_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.maintenance_schedules + ADD CONSTRAINT maintenance_schedules_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.notifications + ADD CONSTRAINT notifications_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.otp_verifications + ADD CONSTRAINT otp_verifications_email_key UNIQUE (email); + +ALTER TABLE ONLY freight.parts + ADD CONSTRAINT parts_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.consignments + ADD CONSTRAINT pk_consignments PRIMARY KEY (id); + +ALTER TABLE ONLY freight.fuel_consumption + ADD CONSTRAINT pk_fuel_consumption PRIMARY KEY (id); + +ALTER TABLE ONLY freight.fuel_purchases + ADD CONSTRAINT pk_fuel_purchases PRIMARY KEY (id); + +ALTER TABLE ONLY freight.invoice_lines + ADD CONSTRAINT pk_invoice_lines PRIMARY KEY (id); + +ALTER TABLE ONLY freight.invoices + ADD CONSTRAINT pk_invoices PRIMARY KEY (id); + +ALTER TABLE ONLY freight.payment_refunds + ADD CONSTRAINT pk_payment_refunds PRIMARY KEY (id); + +ALTER TABLE ONLY freight.payment_webhook_events + ADD CONSTRAINT pk_payment_webhook_events PRIMARY KEY (id); + +ALTER TABLE ONLY freight.payments + ADD CONSTRAINT pk_payments PRIMARY KEY (id); + +ALTER TABLE ONLY freight.tracking_events + ADD CONSTRAINT pk_tracking_events PRIMARY KEY (id); + +ALTER TABLE ONLY freight.wagon_transfer_requests + ADD CONSTRAINT pk_wagon_transfer_requests PRIMARY KEY (id); + +ALTER TABLE ONLY freight.priority_configs + ADD CONSTRAINT priority_configs_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.priority_rule_change_requests + ADD CONSTRAINT priority_rule_change_requests_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.rate_change_requests + ADD CONSTRAINT rate_change_requests_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.route_milestones + ADD CONSTRAINT route_milestones_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.routes + ADD CONSTRAINT routes_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.saved_signatures + ADD CONSTRAINT saved_signatures_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.scheduling_events + ADD CONSTRAINT scheduling_events_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.support_conversations + ADD CONSTRAINT support_conversations_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.support_messages + ADD CONSTRAINT support_messages_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.train_checkpoint_events + ADD CONSTRAINT train_checkpoint_events_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.train_schedule_bookings + ADD CONSTRAINT train_schedule_bookings_booking_id_key UNIQUE (booking_id); + +ALTER TABLE ONLY freight.train_schedule_bookings + ADD CONSTRAINT train_schedule_bookings_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.train_schedules + ADD CONSTRAINT train_schedules_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.train_schedules + ADD CONSTRAINT train_schedules_train_set_id_key UNIQUE (train_set_id); + +ALTER TABLE ONLY freight.train_scheduling_global_rules + ADD CONSTRAINT train_scheduling_global_rules_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.train_set_wagons + ADD CONSTRAINT train_set_wagons_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.train_sets + ADD CONSTRAINT train_sets_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.trains + ADD CONSTRAINT trains_code_key UNIQUE (code); + +ALTER TABLE ONLY freight.trains + ADD CONSTRAINT trains_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.transit_agents + ADD CONSTRAINT transit_agents_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.truck_types + ADD CONSTRAINT truck_types_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.booking_container_units + ADD CONSTRAINT uq_booking_container_unit_number UNIQUE (booking_container_id, container_number); + +ALTER TABLE ONLY freight.booking_contract_signatures + ADD CONSTRAINT uq_booking_contract_signatures_role UNIQUE (booking_id, signer_role); + +ALTER TABLE ONLY freight.consignments + ADD CONSTRAINT uq_consignments_tracking_number UNIQUE (tracking_number); + +ALTER TABLE ONLY freight.contract_clearance_cycles + ADD CONSTRAINT uq_contract_clearance_cycle UNIQUE (contract_id, cycle_number); + +ALTER TABLE ONLY freight.contract_cargo_scope + ADD CONSTRAINT uq_contract_container_size UNIQUE NULLS NOT DISTINCT (contract_id, container_size); + +ALTER TABLE ONLY freight.contract_document_review + ADD CONSTRAINT uq_contract_document_review_doc UNIQUE NULLS NOT DISTINCT (contract_id, clearance_cycle_id, setting_code, file_key); + +ALTER TABLE ONLY freight.contract_routes + ADD CONSTRAINT uq_contract_route UNIQUE (contract_id, origin_yard_id, destination_yard_id); + +ALTER TABLE ONLY freight.contract_templates + ADD CONSTRAINT uq_contract_templates_code UNIQUE (code); + +ALTER TABLE ONLY freight.fuel_consumption + ADD CONSTRAINT uq_fuel_consumption_vehicle_month UNIQUE (vehicle_id, month); + +ALTER TABLE ONLY freight.invoices + ADD CONSTRAINT uq_invoices_invoice_number UNIQUE (invoice_number); + +ALTER TABLE ONLY freight.payment_webhook_events + ADD CONSTRAINT uq_payment_webhook_events_provider_event UNIQUE (provider, external_event_id); + +ALTER TABLE ONLY freight.payments + ADD CONSTRAINT uq_payments_merchant_order_id UNIQUE (merchant_order_id); + +ALTER TABLE ONLY freight.payments + ADD CONSTRAINT uq_payments_transaction_id UNIQUE (transaction_id); + +ALTER TABLE ONLY freight.route_milestones + ADD CONSTRAINT uq_route_milestones_route_sequence UNIQUE (route_id, sequence_no); + +ALTER TABLE ONLY freight.saved_signatures + ADD CONSTRAINT uq_saved_signatures_user_id UNIQUE (user_id); + +ALTER TABLE ONLY freight.train_schedule_bookings + ADD CONSTRAINT uq_train_schedule_booking UNIQUE (train_schedule_id, booking_id); + +ALTER TABLE ONLY freight.train_set_wagons + ADD CONSTRAINT uq_train_set_wagons_sequence UNIQUE (train_set_id, sequence_no); + +ALTER TABLE ONLY freight.warehouse_yards + ADD CONSTRAINT uq_warehouse_yards_code UNIQUE (warehouse_id, code); + +ALTER TABLE ONLY freight.warehouse_zones + ADD CONSTRAINT uq_warehouse_zones_code UNIQUE (yard_id, code); + +ALTER TABLE ONLY freight.vehicles + ADD CONSTRAINT vehicles_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.vehicles + ADD CONSTRAINT vehicles_plate_number_key UNIQUE (plate_number); + +ALTER TABLE ONLY freight.vehicles + ADD CONSTRAINT vehicles_registration_number_key UNIQUE (registration_number); + +ALTER TABLE ONLY freight.vendors + ADD CONSTRAINT vendors_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.wagon_allocation_bulk_loads + ADD CONSTRAINT wagon_allocation_bulk_loads_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.wagon_allocation_bulk_loads + ADD CONSTRAINT wagon_allocation_bulk_loads_wagon_booking_allocation_id_key UNIQUE (wagon_booking_allocation_id); + +ALTER TABLE ONLY freight.wagon_allocation_container_items + ADD CONSTRAINT wagon_allocation_container_items_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.wagon_booking_allocations + ADD CONSTRAINT wagon_booking_allocations_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.wagon_movements + ADD CONSTRAINT wagon_movements_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.wagon_types + ADD CONSTRAINT wagon_types_code_key UNIQUE (code); + +ALTER TABLE ONLY freight.wagon_types + ADD CONSTRAINT wagon_types_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.wagons + ADD CONSTRAINT wagons_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouse_accrual_acks + ADD CONSTRAINT warehouse_accrual_acks_inventory_id_key UNIQUE (inventory_id); + +ALTER TABLE ONLY freight.warehouse_accrual_acks + ADD CONSTRAINT warehouse_accrual_acks_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouse_activity_log + ADD CONSTRAINT warehouse_activity_log_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouse_inventory_movement + ADD CONSTRAINT warehouse_inventory_movement_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouse_inventory + ADD CONSTRAINT warehouse_inventory_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouse_loadings + ADD CONSTRAINT warehouse_loadings_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouse_yard_cargo_types + ADD CONSTRAINT warehouse_yard_cargo_types_pkey PRIMARY KEY (yard_id, cargo_type_id); + +ALTER TABLE ONLY freight.warehouse_yards + ADD CONSTRAINT warehouse_yards_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouse_zones + ADD CONSTRAINT warehouse_zones_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warehouses + ADD CONSTRAINT warehouses_code_key UNIQUE (code); + +ALTER TABLE ONLY freight.warehouses + ADD CONSTRAINT warehouses_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.warranties + ADD CONSTRAINT warranties_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.work_orders + ADD CONSTRAINT work_orders_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.yard_distances + ADD CONSTRAINT yard_distances_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY freight.yard_facilities + ADD CONSTRAINT yard_facilities_pkey PRIMARY KEY (id); + +CREATE INDEX "IDX_1101fbe8e745d91b6bd0747b58" ON freight.ff_clients USING btree (forwarder_company_id); + +CREATE INDEX "IDX_1b3738f728d42fc9aca8d4ac19" ON freight.companies USING btree (tin); + +CREATE INDEX "IDX_1e57cd6c6afae8f303847f159d" ON freight.companies USING btree (type); + +CREATE INDEX "IDX_26d2a16e263ab4fe0a4c8e9195" ON freight.external_profiles USING btree (company_id); + +CREATE INDEX "IDX_30228dd283a0f14486346e1db9" ON freight.company_profiles USING btree (company_id); + +CREATE INDEX "IDX_CARGO_TYPES_DISPLAY_ORDER" ON freight.cargo_types USING btree (display_order); + +CREATE INDEX "IDX_CARGO_TYPES_IS_ACTIVE" ON freight.cargo_types USING btree (is_active); + +CREATE INDEX "IDX_CARGO_TYPES_PARENT_GROUP_ID" ON freight.cargo_types USING btree (parent_group_id); + +CREATE INDEX "IDX_FAYDA_SESSIONS_EXPIRES_AT" ON freight.fayda_verification_sessions USING btree (expires_at); + +CREATE INDEX "IDX_FAYDA_SESSIONS_IAM_USER_ID" ON freight.fayda_verification_sessions USING btree (iam_user_id); + +CREATE INDEX "IDX_FILES_RESOURCE_LOOKUP" ON freight.files USING btree (resource, resource_id) WHERE (deleted_at IS NULL); + +CREATE INDEX "IDX_FLEET_EVENTS_DRIVER" ON freight.fleet_events USING btree (driver_id, created_at); + +CREATE INDEX "IDX_FLEET_EVENTS_VEHICLE" ON freight.fleet_events USING btree (vehicle_id, created_at); + +CREATE INDEX "IDX_FM_VEHICLE_ASSIGNMENTS_VEHICLE" ON freight.first_mile_vehicle_assignments USING btree (vehicle_id); + +CREATE INDEX "IDX_GPS_DEVICES_VEHICLE" ON freight.gps_devices USING btree (vehicle_id); + +CREATE INDEX "IDX_GPS_POSITIONS_DEVICE_TIME" ON freight.gps_positions USING btree (device_id, gps_time); + +CREATE INDEX "IDX_GPS_POSITIONS_VEHICLE_TIME" ON freight.gps_positions USING btree (vehicle_id, gps_time); + +CREATE INDEX "IDX_INCIDENTS_DRIVER" ON freight.incidents USING btree (driver_id, occurred_at); + +CREATE INDEX "IDX_INCIDENTS_VEHICLE" ON freight.incidents USING btree (vehicle_id, occurred_at); + +CREATE INDEX "IDX_LM_VEHICLE_ASSIGNMENTS_VEHICLE" ON freight.last_mile_vehicle_assignments USING btree (vehicle_id); + +CREATE INDEX "IDX_NOTIFICATIONS_RECIPIENT_CREATED" ON freight.notifications USING btree (recipient_user_id, created_at); + +CREATE INDEX "IDX_NOTIFICATIONS_RECIPIENT_UNREAD" ON freight.notifications USING btree (recipient_user_id, is_read); + +CREATE INDEX "IDX_SERVICE_TYPES_DISPLAY_ORDER" ON freight.service_types USING btree (display_order); + +CREATE INDEX "IDX_SERVICE_TYPES_IS_ACTIVE" ON freight.service_types USING btree (is_active); + +CREATE UNIQUE INDEX "IDX_SUPPORT_CONV_COMPANY" ON freight.support_conversations USING btree (company_id) WHERE (deleted_at IS NULL); + +CREATE INDEX "IDX_SUPPORT_CONV_LASTMSG" ON freight.support_conversations USING btree (last_message_at); + +CREATE INDEX "IDX_SUPPORT_MSG_CONV_CREATED" ON freight.support_messages USING btree (conversation_id, created_at); + +CREATE INDEX "IDX_af4d6bac2844d39095b2056e97" ON freight.ff_clients USING btree (client_company_id); + +CREATE INDEX "IDX_b658cd8f6876645a05c0366a56" ON freight.external_profiles USING btree (user_id); + +CREATE INDEX "IDX_booking_cargo_modifier_rate_id" ON freight.booking_cargo_modifier USING btree (rate_id); + +CREATE INDEX "IDX_booking_container_allocations_booking_id" ON freight.booking_container_allocations USING btree (booking_id); + +CREATE INDEX "IDX_booking_container_allocations_vehicle_id" ON freight.booking_container_allocations USING btree (vehicle_id); + +CREATE INDEX "IDX_booking_container_units_grn" ON freight.booking_container_units USING btree (grn_number); + +CREATE INDEX "IDX_booking_handovers_booking" ON freight.booking_handovers USING btree (booking_id); + +CREATE UNIQUE INDEX "IDX_cargo_types_code" ON freight.cargo_types USING btree (code); + +CREATE INDEX "IDX_cargoes_container_id" ON freight.cargoes USING btree (container_id); + +CREATE INDEX "IDX_companies_kind" ON freight.companies USING btree (kind); + +CREATE INDEX "IDX_container_types_is_active" ON freight.container_types USING btree (is_active); + +CREATE UNIQUE INDEX "IDX_container_types_size_code" ON freight.container_types USING btree (code); + +CREATE INDEX "IDX_containers_wagon_id" ON freight.containers USING btree (wagon_id); + +CREATE INDEX "IDX_customer_truck_assignments_booking" ON freight.customer_truck_assignments USING btree (booking_id); + +CREATE INDEX "IDX_customer_truck_containers_assignment" ON freight.customer_truck_containers USING btree (assignment_id); + +CREATE INDEX "IDX_customer_truck_departed" ON freight.customer_truck_assignments USING btree (booking_id, departed_at) WHERE (deleted_at IS NULL); + +CREATE INDEX "IDX_dc808839c3d64c763cc7f6ff68" ON freight.company_profiles USING btree (type); + +CREATE INDEX "IDX_e669a91069a3aa982b2c99c256" ON freight.train_composition_removal_logs USING btree (schedule_id); + +CREATE INDEX "IDX_facility_handling_events_booking" ON freight.facility_handling_events USING btree (booking_id); + +CREATE INDEX "IDX_facility_handling_events_grn" ON freight.facility_handling_events USING btree (grn_number) WHERE (grn_number IS NOT NULL); + +CREATE INDEX "IDX_facility_handling_events_yard" ON freight.facility_handling_events USING btree (yard_id); + +CREATE INDEX "IDX_files_open_change_request" ON freight.files USING btree (resource, resource_id) WHERE (((review_status)::text = 'change_requested'::text) AND (deleted_at IS NULL)); + +CREATE INDEX "IDX_files_version_history" ON freight.files USING btree (resource, resource_id, code, created_at DESC); + +CREATE INDEX "IDX_first_mile_booking_id" ON freight.first_mile USING btree (booking_id); + +CREATE INDEX "IDX_first_mile_container_allocations_first_mile_id" ON freight.first_mile_container_allocations USING btree (first_mile_id); + +CREATE INDEX "IDX_first_mile_container_allocations_vehicle_id" ON freight.first_mile_container_allocations USING btree (vehicle_id); + +CREATE INDEX "IDX_first_mile_status" ON freight.first_mile USING btree (status); + +CREATE INDEX "IDX_first_mile_vehicle_id" ON freight.first_mile USING btree (vehicle_id); + +CREATE INDEX "IDX_freight_customers_email" ON freight.customers USING btree (email); + +CREATE INDEX "IDX_freight_files_resource" ON freight.files USING btree (resource_id, resource); + +CREATE INDEX "IDX_freight_files_resource_code" ON freight.files USING btree (resource_id, resource, code); + +CREATE INDEX "IDX_last_mile_booking_id" ON freight.last_mile USING btree (booking_id); + +CREATE INDEX "IDX_last_mile_container_allocations_last_mile_id" ON freight.last_mile_container_allocations USING btree (last_mile_id); + +CREATE INDEX "IDX_last_mile_container_allocations_vehicle_id" ON freight.last_mile_container_allocations USING btree (vehicle_id); + +CREATE INDEX "IDX_last_mile_status" ON freight.last_mile USING btree (status); + +CREATE INDEX "IDX_last_mile_vehicle_containers_assignment" ON freight.last_mile_vehicle_containers USING btree (assignment_id); + +CREATE INDEX "IDX_last_mile_vehicle_id" ON freight.last_mile USING btree (vehicle_id); + +CREATE INDEX "IDX_locomotive_current_yard_id" ON freight.locomotives USING btree (current_yard_id); + +CREATE INDEX "IDX_maintenance_intervals_vehicle_type" ON freight.maintenance_intervals USING btree (vehicle_id, maintenance_type); + +CREATE INDEX "IDX_parts_category" ON freight.parts USING btree (category); + +CREATE INDEX "IDX_priority_rules_is_active" ON freight.priority_rules USING btree (is_active); + +CREATE INDEX "IDX_rates_destination_yard_id" ON freight.rates USING btree (destination_yard_id); + +CREATE INDEX "IDX_rates_origin_yard_id" ON freight.rates USING btree (origin_yard_id); + +CREATE INDEX "IDX_rates_trigger" ON freight.rates USING btree (trigger); + +CREATE INDEX "IDX_routes_status" ON freight.routes USING btree (status); + +CREATE UNIQUE INDEX "IDX_service_types_code" ON freight.service_types USING btree (code); + +CREATE INDEX "IDX_swal_train_id" ON freight.schedule_wagon_adjustment_logs USING btree (train_id); + +CREATE INDEX "IDX_swal_train_schedule_id" ON freight.schedule_wagon_adjustment_logs USING btree (train_schedule_id); + +CREATE INDEX "IDX_train_sets_train_id" ON freight.train_sets USING btree (train_id); + +CREATE INDEX "IDX_trains_current_yard_id" ON freight.trains USING btree (current_yard_id); + +CREATE INDEX "IDX_wagon_current_yard_id" ON freight.wagons USING btree (current_yard_id); + +CREATE INDEX "IDX_wagon_movements_schedule" ON freight.wagon_movements USING btree (train_schedule_id); + +CREATE INDEX "IDX_wagon_movements_wagon_occurred" ON freight.wagon_movements USING btree (wagon_id, occurred_at); + +CREATE INDEX "IDX_wagons_current_location_yard_id" ON freight.wagons USING btree (current_location_yard_id); + +CREATE INDEX "IDX_wagons_train_id" ON freight.wagons USING btree (train_id); + +CREATE INDEX "IDX_warranties_vehicle_id_expiry_date" ON freight.warranties USING btree (vehicle_id, expiry_date); + +CREATE INDEX "IDX_weight_limit_rules_container_type_id" ON freight.weight_limit_rules USING btree (container_type_id); + +CREATE INDEX "IDX_work_orders_vehicle_id_status" ON freight.work_orders USING btree (vehicle_id, status); + +CREATE UNIQUE INDEX "UQ_DRIVERS_EMAIL_ACTIVE" ON freight.drivers USING btree (email) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_DRIVERS_FAYDA_SUB_ACTIVE" ON freight.drivers USING btree (fayda_sub) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_DRIVERS_LICENSE_ACTIVE" ON freight.drivers USING btree (license_number) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_DRIVERS_PHONE_ACTIVE" ON freight.drivers USING btree (phone_number) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_booking_handovers_booking_edr_truck" ON freight.booking_handovers USING btree (booking_id, edr_assignment_id) WHERE ((deleted_at IS NULL) AND (edr_assignment_id IS NOT NULL)); + +CREATE UNIQUE INDEX "UQ_booking_handovers_booking_truck" ON freight.booking_handovers USING btree (booking_id, truck_assignment_id) WHERE ((deleted_at IS NULL) AND (truck_assignment_id IS NOT NULL)); + +CREATE UNIQUE INDEX "UQ_customer_truck_containers_booking_number" ON freight.customer_truck_containers USING btree (booking_id, container_number) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_dropdown_options_setting_value" ON freight.dropdown_options USING btree (setting_id, value); + +CREATE UNIQUE INDEX "UQ_dropdown_settings_code" ON freight.dropdown_settings USING btree (code); + +CREATE UNIQUE INDEX "UQ_file_upload_fields_setting_file_key" ON freight.file_upload_fields USING btree (setting_id, file_key); + +CREATE UNIQUE INDEX "UQ_file_upload_settings_code" ON freight.file_upload_settings USING btree (code); + +CREATE UNIQUE INDEX "UQ_last_mile_vehicle_container" ON freight.last_mile_vehicle_containers USING btree (last_mile_id, container_number) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_locomotives_name_active" ON freight.locomotives USING btree (lower(btrim((name)::text))) WHERE ((deleted_at IS NULL) AND (name IS NOT NULL) AND (btrim((name)::text) <> ''::text)); + +CREATE UNIQUE INDEX "UQ_maintenance_intervals_vehicle_type_item" ON freight.maintenance_intervals USING btree (vehicle_id, maintenance_type, COALESCE(service_item, ''::character varying)) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_priority_rules_code" ON freight.priority_rules USING btree (code); + +CREATE UNIQUE INDEX "UQ_rates_pattern" ON freight.rates USING btree (rate_type, COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid), COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid), COALESCE(trade_direction, ''::character varying), COALESCE(origin_yard_id, '00000000-0000-0000-0000-000000000000'::uuid), COALESCE(destination_yard_id, '00000000-0000-0000-0000-000000000000'::uuid), rate_unit) WHERE ((deleted_at IS NULL) AND ((status)::text <> 'SUPERSEDED'::text)); + +CREATE UNIQUE INDEX "UQ_shipping_lines_code" ON freight.shipping_lines USING btree (code); + +CREATE UNIQUE INDEX "UQ_train_locomotives_train_loco" ON freight.train_locomotives USING btree (train_id, locomotive_id); + +CREATE UNIQUE INDEX "UQ_train_set_locomotives_set_loco" ON freight.train_set_locomotives USING btree (train_set_id, locomotive_id); + +CREATE UNIQUE INDEX "UQ_trains_export_train_number" ON freight.trains USING btree (export_train_number) WHERE (export_train_number IS NOT NULL); + +CREATE UNIQUE INDEX "UQ_trains_import_train_number" ON freight.trains USING btree (import_train_number) WHERE (import_train_number IS NOT NULL); + +CREATE UNIQUE INDEX "UQ_trains_train_number" ON freight.trains USING btree (train_number) WHERE (train_number IS NOT NULL); + +CREATE UNIQUE INDEX "UQ_wagons_wagon_number_active" ON freight.wagons USING btree (wagon_number) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_weight_limit_rules_pattern" ON freight.weight_limit_rules USING btree (container_type_id, trade_direction) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_yard_facility_yard" ON freight.yard_facilities USING btree (yard_id) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_yards_code" ON freight.yards USING btree (code); + +CREATE UNIQUE INDEX "UQ_yards_code_active" ON freight.yards USING btree (lower(TRIM(BOTH FROM code))) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX "UQ_yards_label_active" ON freight.yards USING btree (lower(TRIM(BOTH FROM label))) WHERE (deleted_at IS NULL); + +CREATE INDEX idx_asset_acquisitions_vehicle_date ON freight.asset_acquisitions USING btree (vehicle_id, acquisition_date); + +CREATE INDEX idx_asset_disposals_vehicle_date ON freight.asset_disposals USING btree (vehicle_id, disposal_date); + +CREATE INDEX idx_booking_batch_offers_booking ON freight.booking_batch_offers USING btree (booking_id); + +CREATE INDEX idx_booking_batch_offers_schedule ON freight.booking_batch_offers USING btree (train_schedule_id); + +CREATE INDEX idx_booking_batch_offers_status ON freight.booking_batch_offers USING btree (status); + +CREATE INDEX idx_booking_contract_signatures_booking_id ON freight.booking_contract_signatures USING btree (booking_id); + +CREATE INDEX idx_booking_document_review_booking ON freight.booking_document_review USING btree (booking_id); + +CREATE INDEX idx_booking_document_review_status ON freight.booking_document_review USING btree (status); + +CREATE INDEX idx_booking_requests_contract ON freight.booking_requests USING btree (contract_id); + +CREATE INDEX idx_booking_requests_contract_status ON freight.booking_requests USING btree (contract_id, status); + +CREATE INDEX idx_booking_requests_status ON freight.booking_requests USING btree (status); + +CREATE INDEX idx_booking_review_note_booking_id ON freight.booking_review_note USING btree (booking_id); + +CREATE INDEX idx_bookings_booking_type ON freight.bookings USING btree (booking_type); + +CREATE INDEX idx_bookings_company_id ON freight.bookings USING btree (company_id); + +CREATE INDEX idx_bookings_company_profile_id ON freight.bookings USING btree (company_profile_id); + +CREATE INDEX idx_bookings_contract ON freight.bookings USING btree (contract_id); + +CREATE INDEX idx_bookings_is_government ON freight.bookings USING btree (is_government) WHERE ((is_government = true) AND (deleted_at IS NULL)); + +CREATE INDEX idx_bookings_route_day ON freight.bookings USING btree (origin_yard_id, destination_yard_id, scheduled_date, status) WHERE (deleted_at IS NULL); + +CREATE INDEX idx_bookings_scheduling_status ON freight.bookings USING btree (scheduling_status); + +CREATE INDEX idx_bookings_train_schedule_id ON freight.bookings USING btree (train_schedule_id) WHERE (deleted_at IS NULL); + +CREATE INDEX idx_clearance_incidents_booking ON freight.clearance_incidents USING btree (booking_id); + +CREATE INDEX idx_clearance_milestones_booking ON freight.clearance_milestones USING btree (booking_id); + +CREATE INDEX idx_clearance_milestones_contract ON freight.clearance_milestones USING btree (contract_id); + +CREATE INDEX idx_clearance_milestones_region ON freight.clearance_milestones USING btree (owner_region, status); + +CREATE INDEX idx_company_change_request_company ON freight.company_change_request USING btree (company_id); + +CREATE INDEX idx_company_change_request_status ON freight.company_change_request USING btree (status); + +CREATE INDEX idx_company_revisions_company ON freight.company_revisions USING btree (company_id); + +CREATE INDEX idx_compliance_records_expiry_date ON freight.compliance_records USING btree (expiry_date); + +CREATE INDEX idx_compliance_records_type ON freight.compliance_records USING btree (type); + +CREATE INDEX idx_compliance_records_vehicle_id ON freight.compliance_records USING btree (vehicle_id); + +CREATE INDEX idx_contract_approval_steps_contract ON freight.contract_approval_steps USING btree (contract_id); + +CREATE INDEX idx_contract_cargo_scope_contract ON freight.contract_cargo_scope USING btree (contract_id); + +CREATE INDEX idx_contract_clearance_cycles_contract ON freight.contract_clearance_cycles USING btree (contract_id); + +CREATE INDEX idx_contract_doc_review_contract ON freight.contract_document_review USING btree (contract_id); + +CREATE INDEX idx_contract_doc_review_status ON freight.contract_document_review USING btree (status); + +CREATE INDEX idx_contract_document_revisions_contract ON freight.contract_document_revisions USING btree (contract_id, created_at DESC); + +CREATE INDEX idx_contract_rate_snapshots_contract ON freight.contract_rate_snapshots USING btree (contract_id); + +CREATE INDEX idx_contract_review_notes_contract ON freight.contract_review_notes USING btree (contract_id); + +CREATE INDEX idx_contract_routes_contract ON freight.contract_routes USING btree (contract_id); + +CREATE INDEX idx_contract_signatures_contract ON freight.contract_signatures USING btree (contract_id); + +CREATE INDEX idx_contracts_company ON freight.contracts USING btree (company_id); + +CREATE INDEX idx_contracts_kind ON freight.contracts USING btree (contract_kind); + +CREATE INDEX idx_contracts_status ON freight.contracts USING btree (status); + +CREATE INDEX idx_contracts_valid_until ON freight.contracts USING btree (contract_valid_until); + +CREATE INDEX idx_djibouti_incidents_booking ON freight.djibouti_import_incidents USING btree (booking_id); + +CREATE INDEX idx_djibouti_incidents_container ON freight.djibouti_import_incidents USING btree (container_number); + +CREATE INDEX idx_djibouti_incidents_type ON freight.djibouti_import_incidents USING btree (incident_type); + +CREATE INDEX idx_drivers_email ON freight.drivers USING btree (email); + +CREATE INDEX idx_drivers_license_number ON freight.drivers USING btree (license_number); + +CREATE INDEX idx_drivers_phone_number ON freight.drivers USING btree (phone_number); + +CREATE INDEX idx_drivers_status ON freight.drivers USING btree (status); + +CREATE INDEX idx_empty_returns_booking ON freight.empty_container_returns USING btree (booking_id); + +CREATE INDEX idx_empty_returns_container ON freight.empty_container_returns USING btree (container_number); + +CREATE INDEX idx_empty_returns_status ON freight.empty_container_returns USING btree (status); + +CREATE UNIQUE INDEX idx_facilities_code ON freight.facilities USING btree (code); + +CREATE INDEX idx_facilities_status ON freight.facilities USING btree (facility_status); + +CREATE INDEX idx_files_resource_lookup ON freight.files USING btree (resource, resource_id); + +CREATE INDEX idx_fuel_consumption_vehicle_month ON freight.fuel_consumption USING btree (vehicle_id, month); + +CREATE INDEX idx_fuel_purchases_date ON freight.fuel_purchases USING btree (purchase_date); + +CREATE INDEX idx_fuel_purchases_vehicle ON freight.fuel_purchases USING btree (vehicle_id); + +CREATE INDEX idx_import_customs_booking ON freight.import_customs_finalizations USING btree (booking_id); + +CREATE INDEX idx_import_customs_risk ON freight.import_customs_finalizations USING btree (customs_risk); + +CREATE INDEX idx_import_djibouti_operations_schedule ON freight.import_djibouti_operations USING btree (train_schedule_id); + +CREATE INDEX idx_interchange_documents_direction ON freight.interchange_documents USING btree (direction); + +CREATE INDEX idx_interchange_documents_schedule ON freight.interchange_documents USING btree (schedule_id); + +CREATE INDEX idx_interchange_documents_status ON freight.interchange_documents USING btree (status); + +CREATE INDEX idx_interchange_items_booking ON freight.interchange_document_items USING btree (booking_id); + +CREATE INDEX idx_interchange_items_document ON freight.interchange_document_items USING btree (interchange_document_id); + +CREATE INDEX idx_invoice_lines_invoice ON freight.invoice_lines USING btree (invoice_id); + +CREATE INDEX idx_invoices_company ON freight.invoices USING btree (company_id); + +CREATE INDEX idx_invoices_company_profile ON freight.invoices USING btree (company_profile_id); + +CREATE INDEX idx_invoices_source ON freight.invoices USING btree (source, source_id); + +CREATE INDEX idx_invoices_status ON freight.invoices USING btree (status); + +CREATE INDEX idx_locomotives_status ON freight.locomotives USING btree (status); + +CREATE INDEX idx_maintenance_costs_vehicle_date ON freight.maintenance_costs USING btree (vehicle_id, incurred_date); + +CREATE INDEX idx_maintenance_schedules_vehicle_date ON freight.maintenance_schedules USING btree (vehicle_id, scheduled_date); + +CREATE INDEX idx_payment_webhook_events_merchant_order_id ON freight.payment_webhook_events USING btree (merchant_order_id); + +CREATE INDEX idx_prcr_status ON freight.priority_rule_change_requests USING btree (status); + +CREATE INDEX idx_priority_configs_currency_type ON freight.priority_configs USING btree (currency, type); + +CREATE INDEX idx_priority_configs_type_active ON freight.priority_configs USING btree (type, is_active); + +CREATE INDEX idx_rcr_status ON freight.rate_change_requests USING btree (status); + +CREATE INDEX idx_route_milestones_route_id ON freight.route_milestones USING btree (route_id); + +CREATE INDEX idx_route_milestones_yard_id ON freight.route_milestones USING btree (yard_id); + +CREATE INDEX idx_routes_destination_yard_id ON freight.routes USING btree (destination_yard_id); + +CREATE INDEX idx_routes_origin_yard_id ON freight.routes USING btree (origin_yard_id); + +CREATE INDEX idx_scheduling_events_train_schedule_id ON freight.scheduling_events USING btree (train_schedule_id) WHERE (deleted_at IS NULL); + +CREATE INDEX idx_train_checkpoint_events_schedule ON freight.train_checkpoint_events USING btree (train_schedule_id, sequence_no) WHERE (deleted_at IS NULL); + +CREATE INDEX idx_train_schedules_booking_window_status ON freight.train_schedules USING btree (booking_window_status) WHERE (deleted_at IS NULL); + +CREATE INDEX idx_train_schedules_departure_status ON freight.train_schedules USING btree (scheduled_departure_date, status); + +CREATE INDEX idx_train_schedules_route_id ON freight.train_schedules USING btree (route_id); + +CREATE INDEX idx_train_schedules_train_number ON freight.train_schedules USING btree (train_number); + +CREATE INDEX idx_train_schedules_window_phase ON freight.train_schedules USING btree (window_phase) WHERE (window_phase IS NOT NULL); + +CREATE INDEX idx_train_set_wagons_physical_wagon ON freight.train_set_wagons USING btree (physical_wagon_id); + +CREATE INDEX idx_train_sets_status ON freight.train_sets USING btree (status); + +CREATE INDEX idx_user_trade_access_user_id ON freight.user_trade_access USING btree (user_id); + +CREATE INDEX idx_vehicles_manufacturer ON freight.vehicles USING btree (manufacturer); + +CREATE INDEX idx_vehicles_plate_number ON freight.vehicles USING btree (plate_number); + +CREATE INDEX idx_vehicles_registration_number ON freight.vehicles USING btree (registration_number); + +CREATE INDEX idx_vehicles_status ON freight.vehicles USING btree (status); + +CREATE INDEX idx_vehicles_vehicle_type ON freight.vehicles USING btree (vehicle_type); + +CREATE INDEX idx_wabl_booking ON freight.wagon_allocation_bulk_loads USING btree (booking_id); + +CREATE INDEX idx_waci_allocation ON freight.wagon_allocation_container_items USING btree (wagon_booking_allocation_id); + +CREATE INDEX idx_wagon_booking_allocations_booking ON freight.wagon_booking_allocations USING btree (booking_id); + +CREATE INDEX idx_wagons_current_train_schedule_id ON freight.wagons USING btree (current_train_schedule_id); + +CREATE INDEX idx_wagons_train_set_wagon_id ON freight.wagons USING btree (train_set_wagon_id); + +CREATE INDEX idx_war_active ON freight.warehouse_allocation_rules USING btree (is_active); + +CREATE INDEX idx_war_priority ON freight.warehouse_allocation_rules USING btree (priority); + +CREATE INDEX idx_warehouse_activity_log_activity_type ON freight.warehouse_activity_log USING btree (activity_type); + +CREATE INDEX idx_warehouse_activity_log_inventory_id ON freight.warehouse_activity_log USING btree (inventory_id); + +CREATE INDEX idx_warehouse_activity_log_warehouse_id ON freight.warehouse_activity_log USING btree (warehouse_id); + +CREATE INDEX idx_warehouse_inventory_booking_id ON freight.warehouse_inventory USING btree (booking_id); + +CREATE INDEX idx_warehouse_inventory_cargo_id ON freight.warehouse_inventory USING btree (cargo_id); + +CREATE INDEX idx_warehouse_inventory_container_id ON freight.warehouse_inventory USING btree (container_id); + +CREATE INDEX idx_warehouse_inventory_goods_id ON freight.warehouse_inventory USING btree (goods_id); + +CREATE INDEX idx_warehouse_inventory_grn_number ON freight.warehouse_inventory USING btree (grn_number) WHERE (grn_number IS NOT NULL); + +CREATE INDEX idx_warehouse_inventory_inspection_status ON freight.warehouse_inventory USING btree (inspection_status); + +CREATE INDEX idx_warehouse_inventory_movement_inventory_id ON freight.warehouse_inventory_movement USING btree (inventory_id); + +CREATE INDEX idx_warehouse_inventory_status ON freight.warehouse_inventory USING btree (status); + +CREATE INDEX idx_warehouse_inventory_warehouse_id ON freight.warehouse_inventory USING btree (warehouse_id); + +CREATE INDEX idx_warehouse_inventory_yard_id ON freight.warehouse_inventory USING btree (yard_id); + +CREATE INDEX idx_warehouse_inventory_zone_id ON freight.warehouse_inventory USING btree (zone_id); + +CREATE INDEX idx_warehouse_loadings_booking_id ON freight.warehouse_loadings USING btree (booking_id); + +CREATE INDEX idx_warehouse_loadings_inventory_id ON freight.warehouse_loadings USING btree (warehouse_inventory_id); + +CREATE INDEX idx_warehouse_loadings_train_schedule ON freight.warehouse_loadings USING btree (train_schedule_id) WHERE (train_schedule_id IS NOT NULL); + +CREATE INDEX idx_warehouse_loadings_wagon_id ON freight.warehouse_loadings USING btree (wagon_id); + +CREATE INDEX idx_warehouse_yards_status ON freight.warehouse_yards USING btree (status); + +CREATE INDEX idx_warehouse_yards_type ON freight.warehouse_yards USING btree (type); + +CREATE INDEX idx_warehouse_yards_warehouse_id ON freight.warehouse_yards USING btree (warehouse_id); + +CREATE INDEX idx_warehouse_zones_status ON freight.warehouse_zones USING btree (status); + +CREATE INDEX idx_warehouse_zones_type ON freight.warehouse_zones USING btree (type); + +CREATE INDEX idx_warehouse_zones_yard_id ON freight.warehouse_zones USING btree (yard_id); + +CREATE INDEX idx_warehouses_station_id ON freight.warehouses USING btree (station_id); + +CREATE INDEX idx_warehouses_status ON freight.warehouses USING btree (status); + +CREATE INDEX idx_warehouses_type ON freight.warehouses USING btree (type); + +CREATE INDEX idx_wfr_active ON freight.warehouse_fee_rules USING btree (is_active); + +CREATE INDEX idx_wfr_type ON freight.warehouse_fee_rules USING btree (rule_type); + +CREATE INDEX idx_wir_booking ON freight.warehouse_inspection_reports USING btree (booking_id); + +CREATE INDEX idx_wir_inventory ON freight.warehouse_inspection_reports USING btree (inventory_id); + +CREATE INDEX idx_wir_status ON freight.warehouse_inspection_reports USING btree (inspection_status); + +CREATE INDEX idx_wm_moved_by ON freight.wagon_movements USING btree (moved_by_user_id); + +CREATE INDEX idx_wm_transfer_request ON freight.wagon_movements USING btree (transfer_request_id); + +CREATE INDEX idx_wtr_status_from_yard ON freight.wagon_transfer_requests USING btree (status, from_yard_id); + +CREATE INDEX idx_yard_distances_from_yard ON freight.yard_distances USING btree (from_yard_id); + +CREATE INDEX idx_yard_distances_to_yard ON freight.yard_distances USING btree (to_yard_id); + +CREATE INDEX ix_transit_agents_is_active ON freight.transit_agents USING btree (is_active); + +CREATE INDEX ix_truck_types_is_active ON freight.truck_types USING btree (is_active); + +CREATE UNIQUE INDEX uq_booking_document_review_doc ON freight.booking_document_review USING btree (booking_id, setting_code, file_key); + +CREATE UNIQUE INDEX uq_clearance_milestone_booking ON freight.clearance_milestones USING btree (booking_id, milestone_code) WHERE (booking_id IS NOT NULL); + +CREATE UNIQUE INDEX uq_clearance_milestone_cycle ON freight.clearance_milestones USING btree (clearance_cycle_id, milestone_code) WHERE (clearance_cycle_id IS NOT NULL); + +CREATE UNIQUE INDEX uq_interchange_active_schedule_direction ON freight.interchange_documents USING btree (schedule_id, direction) WHERE ((schedule_id IS NOT NULL) AND ((status)::text <> 'CANCELLED'::text) AND (deleted_at IS NULL)); + +CREATE UNIQUE INDEX uq_one_active_booking_per_one_time_contract ON freight.bookings USING btree (contract_id) WHERE (((status)::text <> ALL ((ARRAY['EXPIRED'::character varying, 'CANCELLED'::character varying, 'COMPLETED'::character varying, 'REJECTED'::character varying])::text[])) AND (contract_id IS NOT NULL) AND ((contract_kind)::text = 'ONE_TIME'::text)); + +CREATE UNIQUE INDEX uq_rcr_one_pending_per_rate ON freight.rate_change_requests USING btree (rate_id) WHERE (((status)::text = 'PENDING'::text) AND (deleted_at IS NULL)); + +CREATE UNIQUE INDEX uq_yard_distances_pair ON freight.yard_distances USING btree (from_yard_id, to_yard_id) WHERE (deleted_at IS NULL); + +CREATE UNIQUE INDEX ux_train_schedules_reference ON freight.train_schedules USING btree (reference) WHERE (reference IS NOT NULL); + +CREATE UNIQUE INDEX ux_truck_types_code ON freight.truck_types USING btree (code); + +CREATE UNIQUE INDEX ux_vehicles_vin ON freight.vehicles USING btree (vin) WHERE (vin IS NOT NULL); +`; + +/** + * Reference/seed rows, keyed by table. Exported so a targeted re-seed (see + * `scripts/seed-edr-wagons.ts`) can replay a single table without the rest. + * Insertion order is irrelevant — the FK constraints below are added afterwards. + */ +export const FREIGHT_SEED_SQL: Record = { + approval_rules: ` +INSERT INTO freight.approval_rules (id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at, deleted_at) VALUES ('dd31712c-25ef-4ca1-a4d2-435e219fcd7e', false, 1, 'LINE_STAFF', 'Review & Approve', NULL, '2026-08-05 07:31:05.116257+00', '2026-08-05 07:31:05.116257+00', NULL); +INSERT INTO freight.approval_rules (id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at, deleted_at) VALUES ('3e1d0f06-65cb-467e-bf16-a43d47e546bd', false, 2, 'DIRECTOR', 'Final Signature', 'LINE_STAFF', '2026-08-05 07:31:05.116257+00', '2026-08-05 07:31:05.116257+00', NULL); +INSERT INTO freight.approval_rules (id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at, deleted_at) VALUES ('8f818e32-4c51-4e74-859b-9268e39e320c', true, 1, 'DIRECTOR', 'Review & Approve', 'LINE_STAFF', '2026-08-05 07:31:05.116257+00', '2026-08-05 07:31:05.116257+00', NULL); +INSERT INTO freight.approval_rules (id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at, deleted_at) VALUES ('8c55dcfd-607a-4f86-be71-f3b81c4bc044', true, 2, 'CEO', 'Final Signature', NULL, '2026-08-05 07:31:05.116257+00', '2026-08-05 07:31:05.116257+00', NULL); +`, + cargo_types: ` +INSERT INTO freight.cargo_types (id, cargo_type_name, parent_group_id, requires_director_approval, is_active, display_order, created_at, updated_at, deleted_at, code, unit_of_measure, has_lashing, items_per_wagon_map, tons_per_wagon_map) VALUES ('8ed5e27a-44e8-4448-ba73-b435788b0a51', 'General Cargo', NULL, false, true, 1, '2026-08-05 07:31:04.600266+00', '2026-08-05 07:31:04.600266+00', NULL, 'GENERAL', NULL, false, NULL, NULL); +INSERT INTO freight.cargo_types (id, cargo_type_name, parent_group_id, requires_director_approval, is_active, display_order, created_at, updated_at, deleted_at, code, unit_of_measure, has_lashing, items_per_wagon_map, tons_per_wagon_map) VALUES ('d529f2c1-b7b4-406c-9d40-16bfdffdd26c', 'Fertilizer', NULL, false, true, 1, '2026-08-05 07:31:09.200884+00', '2026-08-05 07:31:09.200884+00', NULL, 'FERTILIZER', 'PER_TON', false, NULL, NULL); +INSERT INTO freight.cargo_types (id, cargo_type_name, parent_group_id, requires_director_approval, is_active, display_order, created_at, updated_at, deleted_at, code, unit_of_measure, has_lashing, items_per_wagon_map, tons_per_wagon_map) VALUES ('ca196126-7b3b-4879-ab40-5ee7953d9ec5', 'Coffee', NULL, false, true, 1, '2026-08-05 07:31:09.200884+00', '2026-08-05 07:31:09.200884+00', NULL, 'COFFEE', 'PER_TON', false, NULL, NULL); +INSERT INTO freight.cargo_types (id, cargo_type_name, parent_group_id, requires_director_approval, is_active, display_order, created_at, updated_at, deleted_at, code, unit_of_measure, has_lashing, items_per_wagon_map, tons_per_wagon_map) VALUES ('9edeb30b-bca0-49bf-abe5-4ed26e6b9353', 'Tea', NULL, false, true, 1, '2026-08-05 07:31:09.200884+00', '2026-08-05 07:31:09.200884+00', NULL, 'TEA', 'PER_TON', false, NULL, NULL); +`, + companies: ` +INSERT INTO freight.companies (id, name, type, status, tin, vat_number, fan_number, country, address, phone, email, website, attributes, created_at, updated_at, deleted_at, contact_person_name, contact_person_phone, general_manager_name, general_manager_email, general_manager_phone, nationality, licence_number, status_description, date_registered, renewed_from, renewal_date, renewed_to, region, zone, woreda, kebele, house_no, etrade_phone, kind, approved_at) VALUES ('0a1b0001-0000-4000-8000-000000000001', 'Federal Government of Ethiopia', 'customer', 'active', '0000000001', NULL, NULL, 'Ethiopia', NULL, '+251111000001', 'procurement@gov.et', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'government', NULL); +INSERT INTO freight.companies (id, name, type, status, tin, vat_number, fan_number, country, address, phone, email, website, attributes, created_at, updated_at, deleted_at, contact_person_name, contact_person_phone, general_manager_name, general_manager_email, general_manager_phone, nationality, licence_number, status_description, date_registered, renewed_from, renewal_date, renewed_to, region, zone, woreda, kebele, house_no, etrade_phone, kind, approved_at) VALUES ('0a1b0002-0000-4000-8000-000000000002', 'Ministry of National Defense', 'customer', 'active', '0000000002', NULL, NULL, 'Ethiopia', NULL, '+251111000002', 'logistics@mod.gov.et', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'government', NULL); +INSERT INTO freight.companies (id, name, type, status, tin, vat_number, fan_number, country, address, phone, email, website, attributes, created_at, updated_at, deleted_at, contact_person_name, contact_person_phone, general_manager_name, general_manager_email, general_manager_phone, nationality, licence_number, status_description, date_registered, renewed_from, renewal_date, renewed_to, region, zone, woreda, kebele, house_no, etrade_phone, kind, approved_at) VALUES ('0a1b0003-0000-4000-8000-000000000003', 'Ethiopian Roads Administration', 'customer', 'active', '0000000003', NULL, NULL, 'Ethiopia', NULL, '+251111000003', 'supply@era.gov.et', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'government', NULL); +INSERT INTO freight.companies (id, name, type, status, tin, vat_number, fan_number, country, address, phone, email, website, attributes, created_at, updated_at, deleted_at, contact_person_name, contact_person_phone, general_manager_name, general_manager_email, general_manager_phone, nationality, licence_number, status_description, date_registered, renewed_from, renewal_date, renewed_to, region, zone, woreda, kebele, house_no, etrade_phone, kind, approved_at) VALUES ('0a1b0004-0000-4000-8000-000000000004', 'Ministry of Agriculture', 'customer', 'active', '0000000004', NULL, NULL, 'Ethiopia', NULL, '+251111000004', 'imports@moa.gov.et', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'government', NULL); +INSERT INTO freight.companies (id, name, type, status, tin, vat_number, fan_number, country, address, phone, email, website, attributes, created_at, updated_at, deleted_at, contact_person_name, contact_person_phone, general_manager_name, general_manager_email, general_manager_phone, nationality, licence_number, status_description, date_registered, renewed_from, renewal_date, renewed_to, region, zone, woreda, kebele, house_no, etrade_phone, kind, approved_at) VALUES ('0a1b0005-0000-4000-8000-000000000005', 'Ministry of Trade and Regional Integration', 'customer', 'active', '0000000005', NULL, NULL, 'Ethiopia', NULL, '+251111000005', 'trade@motri.gov.et', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'government', NULL); +INSERT INTO freight.companies (id, name, type, status, tin, vat_number, fan_number, country, address, phone, email, website, attributes, created_at, updated_at, deleted_at, contact_person_name, contact_person_phone, general_manager_name, general_manager_email, general_manager_phone, nationality, licence_number, status_description, date_registered, renewed_from, renewal_date, renewed_to, region, zone, woreda, kebele, house_no, etrade_phone, kind, approved_at) VALUES ('0a1b0006-0000-4000-8000-000000000006', 'Ethiopian Disaster Risk Management Commission', 'customer', 'active', '0000000006', NULL, NULL, 'Ethiopia', NULL, '+251111000006', 'relief@edrmc.gov.et', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'government', NULL); +`, + company_profiles: ` +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0001-0000-4000-8000-000000000001', '0a1b0001-0000-4000-8000-000000000001', 'importer', 'IM-90001', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0001-0000-4000-8000-000000000002', '0a1b0001-0000-4000-8000-000000000001', 'exporter', 'EX-90001', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0002-0000-4000-8000-000000000001', '0a1b0002-0000-4000-8000-000000000002', 'importer', 'IM-90002', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0002-0000-4000-8000-000000000002', '0a1b0002-0000-4000-8000-000000000002', 'exporter', 'EX-90002', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0003-0000-4000-8000-000000000001', '0a1b0003-0000-4000-8000-000000000003', 'importer', 'IM-90003', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0003-0000-4000-8000-000000000002', '0a1b0003-0000-4000-8000-000000000003', 'exporter', 'EX-90003', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0004-0000-4000-8000-000000000001', '0a1b0004-0000-4000-8000-000000000004', 'importer', 'IM-90004', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0004-0000-4000-8000-000000000002', '0a1b0004-0000-4000-8000-000000000004', 'exporter', 'EX-90004', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0005-0000-4000-8000-000000000001', '0a1b0005-0000-4000-8000-000000000005', 'importer', 'IM-90005', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0005-0000-4000-8000-000000000002', '0a1b0005-0000-4000-8000-000000000005', 'exporter', 'EX-90005', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0006-0000-4000-8000-000000000001', '0a1b0006-0000-4000-8000-000000000006', 'importer', 'IM-90006', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +INSERT INTO freight.company_profiles (id, company_id, type, reference, status, business_license, attributes, created_at, updated_at, deleted_at, business_license_files, review_note, reviewed_by, reviewed_at) VALUES ('0b1c0006-0000-4000-8000-000000000002', '0a1b0006-0000-4000-8000-000000000006', 'exporter', 'EX-90006', 'active', NULL, NULL, '2026-08-05 07:31:07.431166+00', '2026-08-05 07:31:07.431166+00', NULL, NULL, NULL, NULL, NULL); +`, + contract_templates: ` +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('6bc99976-d5f7-4b4d-82a0-f0340f3e7cf7', 'IMPORT_BULK_CUSTOMS', 'Bulk Import Contract (with customs clearing)', 'Import of bulk cargo (e.g. steel billets) from Djibouti (DMP/Nagad) to Galaan Multipurpose Port with customs clearance and optional last-mile delivery. Customs clearing is performed by the Service Provider.', 'Bulk Cargo Transportation and Customs Clearance Services', '["The Client has agreed to engage the Service Provider for transportation and customs clearance services for bulk cargo, including first-mile transport to the railway station at Djibouti, loading at either DMP or Nagad Railway Station (Djibouti), port/rail terminal handling, loading onto the train, railway transport to Galaan Multipurpose Port (GMP) in Ethiopia, unloading at the destination port from train to load directly on truck, onward transportation to the Client''s site (excluding truck loading at Djibouti and truck unloading at the Client destination where last-mile service is undertaken by the Service Provider), and all related documentation.", "The Service Provider has agreed to provide the requested services in accordance with the terms and conditions of this Agreement."]', '[{"id": "objective", "body": "The objective of this contract is to provide the Client with integrated logistics services for the transportation of bulk cargo, including:\\n- First-mile transportation in Djibouti from the Client''s designated cargo location to the selected railway station (DMP or Nagad).\\n- Port handling and loading onto railway wagons.\\n- Railway transport from DMP and/or Nagad Railway freight station (Djibouti) to Galaan Multipurpose Port.\\n- Customs clearance in Djibouti and Ethiopia.\\n- Unloading from train at the destination port to load directly on truck.\\n- Last-mile delivery by truck to the Client''s delivery site where the last-mile service is undertaken by the Service Provider.\\nThe truck loading at Djibouti and the truck unloading at the Client''s delivery site shall be the responsibility of the Client.", "order": 1, "title": "Objective of the Services"}, {"id": "client-obligations", "body": "Provide written/email/electronic instructions specifying the cargo volume and the selected loading station (DMP or Nagad) for each shipment.\\nPrepare and submit all necessary documents and permits to enable smooth service execution.\\nEnsure cargo readiness in compliance with specifications (including weight, size, and contour restrictions).\\nHandle truck loading at Djibouti Free Zone/Old Port/DMP and any other designated cargo location at Djibouti, and truck unloading at the delivery site.\\nEnsure safety and proper securing of cargo during truck handling.\\nSubmit all required documents necessary for customs clearance and cargo release within one (1) calendar day from the date of request or notification by the Service Provider.\\nUpon receipt of the wagon allocation list and train schedule from the Service Provider, ensure that the cargo is transferred to the designated loading freight station and made ready for loading within two (2) days prior to wagon arrival. Any delay beyond this period resulting from Client-related issues shall be subject to a charge of USD 56 per wagon per day, or part thereof, until the cargo is made available for loading.\\nUpon arrival of the train at Galaan Multipurpose Port (GMP), offload cargo from wagons within twenty-four (24) hours of train arrival. Where the Client undertakes last-mile transportation, the Client may arrange sufficient trucks at the time of train arrival to enable direct loading of cargo from wagons to trucks.\\nIn the event the Client is unable to provide trucks for the collection of cargo within twenty-four (24) hours of train arrival, the Service Provider shall have the right to handle and reposition the cargo to any location it deems appropriate, and shall not be held responsible for any loss, shortage, or damage arising from such repositioning.\\nAny additional handling, re-handling, or repeated loading operations performed by the Service Provider shall be charged as double handling fees at a rate of USD 4 per ton, payable by the Client.\\nIf stored, the full cargo must be collected from the Galaan Multipurpose Port compound within three (3) days from the time of train arrival at the port.\\nIf the Client fails to collect the cargo within the specified period, the Client shall be liable to pay demurrage charges of USD 2 per day per ton, calculated based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date of payment.\\nWhere the last-mile service is provided by the Service Provider, unload the cargo from the truck at the delivery site within the agreed time frame.\\nDesignate authorized representatives (with valid power of attorney) for handover at origin and destination.\\nSettle demurrage payments within ten (10) calendar days from the date the Service Provider issues a claim.\\nPay the Service Provider one hundred percent (100%) of the contract price in advance for each train set in accordance with the pricing article of this Agreement.\\nContact the Service Provider to obtain confirmation prior to booking and proceeding with payment.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Provide first-mile transportation in Djibouti from the Client''s designated location to the selected railway station (DMP or Nagad).\\nCarry out port handling and loading onto railway wagons.\\nProvide railway transportation from DMP/Nagad (Djibouti) railway freight station to Galaan Multipurpose Port.\\nPerform unloading at Galaan Multipurpose Port (GMP) to load directly on truck.\\nPerform customs clearance in Djibouti and Ethiopia, including border station procedures.\\nPrepare and submit all required transport documentation.\\nProvide cargo insurance coverage for each supplied wagon.\\nNotify the Client of train schedules, wagon numbers, and expected arrival times in advance.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "Neither party shall be liable for delays or non-performance caused by force majeure events beyond their reasonable control.\\nForce majeure shall be interpreted in accordance with the Ethiopian Civil Code.", "order": 4, "title": "Force Majeure"}, {"id": "liability", "body": "The Service Provider shall be fully responsible for any loss, shortage, or damage to cargo that occurs after it has been taken over until delivery to the Client''s delivery site.\\nCompensation shall be based on the market value of the cargo, in accordance with applicable laws.", "order": 5, "title": "Liabilities Related to Damages and Losses"}, {"id": "pricing", "body": "The applicable railway freight, Djibouti handling, and any additional service and surcharge rates for this contract are set out in the Rate Schedule immediately below, expressed as unit prices per origin → destination lane and per service.\\nEach wagon shall be loaded up to a maximum of seventy (70) metric tons; for billing purposes one full wagon shall be deemed equivalent to this volume.\\nWhere lashing materials and wood are provided by the Service Provider, they shall be charged at the applicable rate set out in the Rate Schedule; the provision continues until the cargo reaches and is fully unloaded at the designated destination station.\\nThe price for last-mile delivery, where not listed in the Rate Schedule, shall be determined once the cargo departs from the loading point and shall be communicated to the Client by official email upon the Client''s request.\\nPayments shall be made 100% in advance in USD.", "order": 6, "title": "Contract Price and Payment Terms"}, {"id": "contract-documents", "body": "The following documents form part of this contract:\\n- This Contract Agreement.\\n- Any amendments made to this Agreement.\\n- Minutes of negotiation (if any).", "order": 7, "title": "Contract Documents"}, {"id": "documentation", "body": "The Service Provider shall deliver the following to the Client:\\n- Freight Carriage Acceptance Sheet of the Addis Ababa–Djibouti Railway.\\n- Notice of transportation and miscellaneous charges.\\n- Summary of payment request as per the agreed tariff, if required.", "order": 8, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider shall certify the taking over of goods in the Freight Carriage Acceptance Sheet.\\nThis document shall serve as prima facie evidence of receipt of the cargo.\\nUpon delivery of the cargo to the Client, the Cargo Handover Out Voucher signed by both parties shall serve as evidence of cargo receipt.", "order": 9, "title": "Consignment Notes"}, {"id": "termination", "body": "This contract may be terminated:\\n- By mutual consent.\\n- Upon completion of the agreed contract period or cargo volume.\\n- For breach of fundamental provisions, with one-week prior written notice.", "order": 10, "title": "Termination of Contract"}, {"id": "effectiveness", "body": "This Agreement becomes effective on the date it is signed by both parties.", "order": 11, "title": "Contract Effectiveness"}, {"id": "duration", "body": "The contract is valid until August 31, {{contractYear}} from the date of effectiveness, extendable by mutual agreement.", "order": 12, "title": "Duration"}, {"id": "disputes", "body": "Disputes shall first be settled amicably.\\nIf unresolved, disputes shall be referred to the competent Federal Court of Ethiopia in Addis Ababa.\\nThe governing law shall be the laws of the Federal Democratic Republic of Ethiopia.", "order": 13, "title": "Settlement of Disputes"}, {"id": "customs-clearing", "body": "The Service Provider shall carry out customs clearing on behalf of the Client for the cargo covered by this Agreement, including declaration, lodgement, and follow-up at the customs stations of Djibouti and Ethiopia as applicable to the agreed corridor.\\nThe Service Provider shall act only within the authority granted by the Client and shall not amend a declaration without the Client''s written instruction.\\nCustoms duties, taxes, and any government charges assessed on the cargo remain payable by the Client and are not included in the freight price; the Service Provider shall settle them on the Client''s behalf only where the Client has placed the corresponding funds in advance.\\nThe Service Provider shall hand over all customs documents obtained in the course of clearing to the Client upon completion of each shipment.", "order": 14, "title": "Customs Clearing Services"}, {"id": "customs-client-duties", "body": "Grant the Service Provider a duly signed and stamped power of attorney authorising it to act as the Client''s customs agent for the duration of this Agreement.\\nSubmit every document required for declaration (commercial invoice, packing list, bill of lading or airway bill, permits, certificates of origin, and any authority-specific licence) within one (1) calendar day of the Service Provider''s request.\\nWarrant that the declared description, quantity, value, and tariff classification of the cargo are complete and accurate.\\nBear any penalty, demurrage, storage, or re-inspection cost arising from incorrect, incomplete, or late Client-supplied information or documentation.\\nSettle assessed duties and taxes within the period notified by the Service Provider, failing which the Service Provider may suspend clearing and the cargo shall remain at the Client''s risk and cost.", "order": 15, "title": "Client Obligations for Customs Clearing"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('b4dcb3fb-43f9-47f1-a97d-a7af210f05eb', 'IMPORT_BULK_NO_CUSTOMS', 'Bulk Import Contract (without customs clearing)', 'Import of bulk cargo (e.g. steel billets) from Djibouti (DMP/Nagad) to Galaan Multipurpose Port with customs clearance and optional last-mile delivery. Customs clearing is handled by the Client.', 'Bulk Cargo Transportation and Customs Clearance Services', '["The Client has agreed to engage the Service Provider for transportation and customs clearance services for bulk cargo, including first-mile transport to the railway station at Djibouti, loading at either DMP or Nagad Railway Station (Djibouti), port/rail terminal handling, loading onto the train, railway transport to Galaan Multipurpose Port (GMP) in Ethiopia, unloading at the destination port from train to load directly on truck, onward transportation to the Client''s site (excluding truck loading at Djibouti and truck unloading at the Client destination where last-mile service is undertaken by the Service Provider), and all related documentation.", "The Service Provider has agreed to provide the requested services in accordance with the terms and conditions of this Agreement."]', '[{"id": "objective", "body": "The objective of this contract is to provide the Client with integrated logistics services for the transportation of bulk cargo, including:\\n- First-mile transportation in Djibouti from the Client''s designated cargo location to the selected railway station (DMP or Nagad).\\n- Port handling and loading onto railway wagons.\\n- Railway transport from DMP and/or Nagad Railway freight station (Djibouti) to Galaan Multipurpose Port.\\n- Customs clearance in Djibouti and Ethiopia.\\n- Unloading from train at the destination port to load directly on truck.\\n- Last-mile delivery by truck to the Client''s delivery site where the last-mile service is undertaken by the Service Provider.\\nThe truck loading at Djibouti and the truck unloading at the Client''s delivery site shall be the responsibility of the Client.", "order": 1, "title": "Objective of the Services"}, {"id": "client-obligations", "body": "Provide written/email/electronic instructions specifying the cargo volume and the selected loading station (DMP or Nagad) for each shipment.\\nPrepare and submit all necessary documents and permits to enable smooth service execution.\\nEnsure cargo readiness in compliance with specifications (including weight, size, and contour restrictions).\\nHandle truck loading at Djibouti Free Zone/Old Port/DMP and any other designated cargo location at Djibouti, and truck unloading at the delivery site.\\nEnsure safety and proper securing of cargo during truck handling.\\nSubmit all required documents necessary for customs clearance and cargo release within one (1) calendar day from the date of request or notification by the Service Provider.\\nUpon receipt of the wagon allocation list and train schedule from the Service Provider, ensure that the cargo is transferred to the designated loading freight station and made ready for loading within two (2) days prior to wagon arrival. Any delay beyond this period resulting from Client-related issues shall be subject to a charge of USD 56 per wagon per day, or part thereof, until the cargo is made available for loading.\\nUpon arrival of the train at Galaan Multipurpose Port (GMP), offload cargo from wagons within twenty-four (24) hours of train arrival. Where the Client undertakes last-mile transportation, the Client may arrange sufficient trucks at the time of train arrival to enable direct loading of cargo from wagons to trucks.\\nIn the event the Client is unable to provide trucks for the collection of cargo within twenty-four (24) hours of train arrival, the Service Provider shall have the right to handle and reposition the cargo to any location it deems appropriate, and shall not be held responsible for any loss, shortage, or damage arising from such repositioning.\\nAny additional handling, re-handling, or repeated loading operations performed by the Service Provider shall be charged as double handling fees at a rate of USD 4 per ton, payable by the Client.\\nIf stored, the full cargo must be collected from the Galaan Multipurpose Port compound within three (3) days from the time of train arrival at the port.\\nIf the Client fails to collect the cargo within the specified period, the Client shall be liable to pay demurrage charges of USD 2 per day per ton, calculated based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date of payment.\\nWhere the last-mile service is provided by the Service Provider, unload the cargo from the truck at the delivery site within the agreed time frame.\\nDesignate authorized representatives (with valid power of attorney) for handover at origin and destination.\\nSettle demurrage payments within ten (10) calendar days from the date the Service Provider issues a claim.\\nPay the Service Provider one hundred percent (100%) of the contract price in advance for each train set in accordance with the pricing article of this Agreement.\\nContact the Service Provider to obtain confirmation prior to booking and proceeding with payment.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Provide first-mile transportation in Djibouti from the Client''s designated location to the selected railway station (DMP or Nagad).\\nCarry out port handling and loading onto railway wagons.\\nProvide railway transportation from DMP/Nagad (Djibouti) railway freight station to Galaan Multipurpose Port.\\nPerform unloading at Galaan Multipurpose Port (GMP) to load directly on truck.\\nPerform customs clearance in Djibouti and Ethiopia, including border station procedures.\\nPrepare and submit all required transport documentation.\\nProvide cargo insurance coverage for each supplied wagon.\\nNotify the Client of train schedules, wagon numbers, and expected arrival times in advance.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "Neither party shall be liable for delays or non-performance caused by force majeure events beyond their reasonable control.\\nForce majeure shall be interpreted in accordance with the Ethiopian Civil Code.", "order": 4, "title": "Force Majeure"}, {"id": "liability", "body": "The Service Provider shall be fully responsible for any loss, shortage, or damage to cargo that occurs after it has been taken over until delivery to the Client''s delivery site.\\nCompensation shall be based on the market value of the cargo, in accordance with applicable laws.", "order": 5, "title": "Liabilities Related to Damages and Losses"}, {"id": "pricing", "body": "The applicable railway freight, Djibouti handling, and any additional service and surcharge rates for this contract are set out in the Rate Schedule immediately below, expressed as unit prices per origin → destination lane and per service.\\nEach wagon shall be loaded up to a maximum of seventy (70) metric tons; for billing purposes one full wagon shall be deemed equivalent to this volume.\\nWhere lashing materials and wood are provided by the Service Provider, they shall be charged at the applicable rate set out in the Rate Schedule; the provision continues until the cargo reaches and is fully unloaded at the designated destination station.\\nThe price for last-mile delivery, where not listed in the Rate Schedule, shall be determined once the cargo departs from the loading point and shall be communicated to the Client by official email upon the Client''s request.\\nPayments shall be made 100% in advance in USD.", "order": 6, "title": "Contract Price and Payment Terms"}, {"id": "contract-documents", "body": "The following documents form part of this contract:\\n- This Contract Agreement.\\n- Any amendments made to this Agreement.\\n- Minutes of negotiation (if any).", "order": 7, "title": "Contract Documents"}, {"id": "documentation", "body": "The Service Provider shall deliver the following to the Client:\\n- Freight Carriage Acceptance Sheet of the Addis Ababa–Djibouti Railway.\\n- Notice of transportation and miscellaneous charges.\\n- Summary of payment request as per the agreed tariff, if required.", "order": 8, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider shall certify the taking over of goods in the Freight Carriage Acceptance Sheet.\\nThis document shall serve as prima facie evidence of receipt of the cargo.\\nUpon delivery of the cargo to the Client, the Cargo Handover Out Voucher signed by both parties shall serve as evidence of cargo receipt.", "order": 9, "title": "Consignment Notes"}, {"id": "termination", "body": "This contract may be terminated:\\n- By mutual consent.\\n- Upon completion of the agreed contract period or cargo volume.\\n- For breach of fundamental provisions, with one-week prior written notice.", "order": 10, "title": "Termination of Contract"}, {"id": "effectiveness", "body": "This Agreement becomes effective on the date it is signed by both parties.", "order": 11, "title": "Contract Effectiveness"}, {"id": "duration", "body": "The contract is valid until August 31, {{contractYear}} from the date of effectiveness, extendable by mutual agreement.", "order": 12, "title": "Duration"}, {"id": "disputes", "body": "Disputes shall first be settled amicably.\\nIf unresolved, disputes shall be referred to the competent Federal Court of Ethiopia in Addis Ababa.\\nThe governing law shall be the laws of the Federal Democratic Republic of Ethiopia.", "order": 13, "title": "Settlement of Disputes"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('23cc0059-6258-4f16-8c84-6670751cfbc6', 'EXPORT_BULK_CUSTOMS', 'Bulk Export Contract (with customs clearing)', 'Export of bulk cargo (e.g. livestock) by railway from Ethiopian loading stations to Nagad railway freight yard, Djibouti. Customs clearing is performed by the Service Provider.', 'Bulk Cargo Transportation Service by Railway', '["The Client has agreed to deliver bulk cargo to the Service Provider for transport from the agreed Ethiopian loading station to Nagad railway freight yard using the Addis Ababa–Djibouti railway line.", "The Service Provider has agreed to provide the service to transport the bulk cargo from the agreed loading station to Nagad railway freight yard."]', '[{"id": "objective", "body": "To undertake the railway transportation of bulk cargo from the agreed Ethiopian loading station to Nagad railway freight yard.", "order": 1, "title": "Objective of the Service"}, {"id": "client-obligations", "body": "Give written instruction to the Service Provider to transport a minimum of one wagon of cargo; the wagon request shall be made at least five (5) days in advance for each wagon.\\nMaintain detailed information incorporating type, weight, and destination of the cargo ready for shipment, and notify the Service Provider or its nominated agent by notice, email, or fax.\\nPrepare the necessary documents and facilities to make the cargo ready for transport.\\nNote the allowable transport period of the cargo: the maximum time range during which the goods maintain their condition without any problem. The allowable transport period must be at least two (2) days longer than the delivery period.\\nEnsure cargo is properly loaded and fastened in the wagons, provide the necessary lashing and barriers for loading as per the instruction of the departure station, and bear responsibility for the condition of the cargo during the transport period.\\nSupply the necessary provisions for the cargo for each wagon and assign a responsible person to travel with the train to check the status of the cargo during transport, where the nature of the cargo so requires.\\nSupply the minimum amount of cargo available for at least one wagon.\\nExecute loading, lashing, and preparing barriers on wagons at the loading station and provide the complete documents/bill to the Service Provider within one (1) calendar day.\\nFor each extra calendar day used for loading cargo and completing documents at the loading station, pay the wagon-occupied fee per the pricing article; the fee shall be paid within ten (10) calendar days from the date the Service Provider claims it, failing which compensation is payable calculated on the basis of the Commercial Bank of Ethiopia interest rate for the delay period.\\nBe responsible for safety matters, and indemnify and hold the Service Provider harmless against all consequences resulting from accidents arising from or associated with the loading and unloading process.\\nExecute and cover the cost of loading and unloading of cargo at both the loading station and Nagad railway freight yard.\\nFollow up that the cargo is loaded and unloaded on time.\\nDelegate representatives at both ends to consign and receive cargo with signature and stamp. Representatives shall hold a duly signed and stamped power of attorney and shall produce their ID or passport when consigning or receiving the cargo.\\nPrepare the necessary facilities to take over the transported cargo at Nagad freight yard upon arrival by issuing handover documents.\\nTake the transported cargo out of the wagons at Nagad freight yard within one (1) calendar day starting from the day following the notice of arrival.\\nPay the wagon-occupied fee per the pricing article for delays of more than one (1) calendar day at Nagad railway freight yard due to the fault of the Client in resolving customs or third-party claims or any other causes.\\nAfter the wagon list is submitted to the Client, if a wagon is not loaded due to the fault of the Client, pay 100% of the transportation price per wagon for each unloaded wagon.\\nPay the Service Provider 100% of the contract price in advance for each wagon.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Provide the list and identification numbers of wagons with sequence and locomotives at least twenty-four (24) hours in advance to the Client, with any correction at least twelve (12) hours before arrival at destination.\\nTransport the cargo from the loading station to Nagad railway freight yard.\\nProvide safe transportation of the cargo throughout the transit.\\nPresent customs clearance documents and mobilize the rolling stock as needed.\\nProvide the wagons assigned for the freight at the agreed place and time, and follow up that the cargo is loaded on time.\\nTransport and deliver the cargo taken over, in the condition received, within two (2) calendar days to Nagad railway freight yard.\\nWhere a wagon carrying cargo stops due to accident or mechanical problem, promptly notify the nearby customs station, police office, and the Client. A wagon stopped in Ethiopia due to mechanical defect shall be maintained within four (4) calendar days; within Nagad (Djibouti) territory within twelve (12) calendar days. In case of accident where the problem cannot be solved within one (1) calendar day and the wagon is not operational, the Service Provider shall have the cargo carried and delivered by another wagon, and shall provide an accident or defect report issued by the local police office regarding the sustained damage.\\nBuy a cargo liability insurance policy for each supplied wagon.\\nProvide wagon cleaning service and charge the cleaning fee based on actual expenditure.\\nIf the Client fails or refuses to receive the cargo beyond the allowable transport period, the Service Provider has the right to handle the cargo.\\nNeither party shall be liable for any indirect or consequential loss sustained by the other in connection with this Agreement.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "The parties have no obligation to pay demurrage or any other compensation if they have failed to discharge their obligations due to force majeure.\\nForce majeure shall be deemed to exist when the contract is not performed due to any event beyond the reasonable control of a party which prevents that party from complying with its obligations under this Agreement, including but not limited to:\\n- Acts of God (such as, but not limited to, fires, explosions, earthquakes, drought, tidal waves, and floods).\\n- War, hostilities (whether war is declared or not), invasion, acts of foreign enemies, mobilization, requisition, or embargo.\\n- Rebellion, revolution, insurrection, military or usurped power, or civil war.\\n- Contamination by radioactivity from any nuclear fuel or nuclear waste.\\n- Riot, commotion, strikes, go-slows, lockouts, or disorder.\\n- Acts of terrorism.\\nA party wishing to claim protection in respect of a force majeure event shall, as soon as possible following the occurrence or commencement of the event, notify the other party of its nature and expected duration, and shall thereafter keep the other party informed until it is able to perform its obligations under this Agreement.", "order": 4, "title": "Force Majeure"}, {"id": "pricing", "body": "The price of bulk cargo transportation from the loading station to Nagad, together with any applicable demurrage and surcharge rates, is set out in the Rate Schedule immediately below, expressed as unit prices per origin → destination lane and per wagon.\\nPayment for transport services shall be made in Birr based on the selling price of USD to Birr on the date of payment set by the Commercial Bank of Ethiopia.\\nIf there is an increment or decrement of the USD exchange rate to Birr between the date of payment and the date the wagon/train number is provided to the Client, either the Client shall make the additional payment to the Service Provider or the Service Provider shall refund the difference from the initial payment to the Client.\\nThe cost of loading at the loading station and unloading at Nagad shall be covered by the Client and is not part of this contract agreement.\\nThe Client shall pay 100% of the contract price in advance.\\nThe Client shall pay a demurrage fee for occupied wagons at the rate set out in the Rate Schedule for the applicable occupancy band.\\nDemurrage payment shall be made in Birr based on the selling price of USD to Birr set by the Commercial Bank of Ethiopia on the date of the demurrage occurrence.", "order": 5, "title": "Contract Price and Terms of Payment"}, {"id": "contract-documents", "body": "The following documents shall constitute the contract between the Client and the Service Provider:\\n- Amendments made to this contract (if any).\\n- This Contract Agreement.\\n- Final minutes of negotiation (if any).\\nIf there is any discrepancy between the documents, they shall be interpreted with priority in the order listed above.", "order": 6, "title": "Contract Documents"}, {"id": "documentation", "body": "The following documents shall be delivered to the Client upon request for settlement:\\n- Consignment Note (cargo handover document to the Client).\\n- Summary of payment request of the Service Provider prepared as per the agreed tariff.", "order": 7, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider must certify the taking over of the goods on the duplicates of the consignment note in an appropriate manner and return the duplicate to the Client.\\nA consignment note shall be prima facie evidence of the receipt of the goods by the Service Provider and of the kind, number, and weight of the goods.", "order": 8, "title": "Consignment Notes"}, {"id": "termination", "body": "The contract may be terminated:\\n- Upon mutual consent of the parties.\\n- Upon completion of the contract period.\\n- If either or both parties breach a fundamental provision of the contract, upon prior legal notice delivered by either party.", "order": 9, "title": "Termination of Contract"}, {"id": "effectiveness", "body": "The contract shall come into full force and effect on the date when all of the following are accomplished:\\n- The contract is signed by the Client and the Service Provider.\\n- The Service Provider has received the advance payment of 100% of the contract price for each train set of cargo.", "order": 10, "title": "Contract Effectiveness"}, {"id": "cargo-amount", "body": "The minimum cargo to be transported shall be one wagon.", "order": 11, "title": "Cargo Amount"}, {"id": "duration", "body": "The contract shall last for three (3) months starting from the date of contract signing, with possible extension upon mutual agreement.", "order": 12, "title": "Duration of Contract"}, {"id": "disputes", "body": "If a dispute arises between the parties, they shall exert efforts to settle their differences amicably.\\nIf the parties fail to settle their disputes amicably, the case shall be taken to the competent Federal Court of law presiding in Addis Ababa.\\nThe governing law shall be the laws of the Federal Democratic Republic of Ethiopia.", "order": 13, "title": "Settlement of Disputes"}, {"id": "customs-clearing", "body": "The Service Provider shall carry out customs clearing on behalf of the Client for the cargo covered by this Agreement, including declaration, lodgement, and follow-up at the customs stations of Djibouti and Ethiopia as applicable to the agreed corridor.\\nThe Service Provider shall act only within the authority granted by the Client and shall not amend a declaration without the Client''s written instruction.\\nCustoms duties, taxes, and any government charges assessed on the cargo remain payable by the Client and are not included in the freight price; the Service Provider shall settle them on the Client''s behalf only where the Client has placed the corresponding funds in advance.\\nThe Service Provider shall hand over all customs documents obtained in the course of clearing to the Client upon completion of each shipment.", "order": 14, "title": "Customs Clearing Services"}, {"id": "customs-client-duties", "body": "Grant the Service Provider a duly signed and stamped power of attorney authorising it to act as the Client''s customs agent for the duration of this Agreement.\\nSubmit every document required for declaration (commercial invoice, packing list, bill of lading or airway bill, permits, certificates of origin, and any authority-specific licence) within one (1) calendar day of the Service Provider''s request.\\nWarrant that the declared description, quantity, value, and tariff classification of the cargo are complete and accurate.\\nBear any penalty, demurrage, storage, or re-inspection cost arising from incorrect, incomplete, or late Client-supplied information or documentation.\\nSettle assessed duties and taxes within the period notified by the Service Provider, failing which the Service Provider may suspend clearing and the cargo shall remain at the Client''s risk and cost.", "order": 15, "title": "Client Obligations for Customs Clearing"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('87b640a0-dd01-46bf-a20f-7859ff77d4d3', 'EXPORT_BULK_NO_CUSTOMS', 'Bulk Export Contract (without customs clearing)', 'Export of bulk cargo (e.g. livestock) by railway from Ethiopian loading stations to Nagad railway freight yard, Djibouti. Customs clearing is handled by the Client.', 'Bulk Cargo Transportation Service by Railway', '["The Client has agreed to deliver bulk cargo to the Service Provider for transport from the agreed Ethiopian loading station to Nagad railway freight yard using the Addis Ababa–Djibouti railway line.", "The Service Provider has agreed to provide the service to transport the bulk cargo from the agreed loading station to Nagad railway freight yard."]', '[{"id": "objective", "body": "To undertake the railway transportation of bulk cargo from the agreed Ethiopian loading station to Nagad railway freight yard.", "order": 1, "title": "Objective of the Service"}, {"id": "client-obligations", "body": "Give written instruction to the Service Provider to transport a minimum of one wagon of cargo; the wagon request shall be made at least five (5) days in advance for each wagon.\\nMaintain detailed information incorporating type, weight, and destination of the cargo ready for shipment, and notify the Service Provider or its nominated agent by notice, email, or fax.\\nPrepare the necessary documents and facilities to make the cargo ready for transport.\\nNote the allowable transport period of the cargo: the maximum time range during which the goods maintain their condition without any problem. The allowable transport period must be at least two (2) days longer than the delivery period.\\nEnsure cargo is properly loaded and fastened in the wagons, provide the necessary lashing and barriers for loading as per the instruction of the departure station, and bear responsibility for the condition of the cargo during the transport period.\\nSupply the necessary provisions for the cargo for each wagon and assign a responsible person to travel with the train to check the status of the cargo during transport, where the nature of the cargo so requires.\\nSupply the minimum amount of cargo available for at least one wagon.\\nExecute loading, lashing, and preparing barriers on wagons at the loading station and provide the complete documents/bill to the Service Provider within one (1) calendar day.\\nFor each extra calendar day used for loading cargo and completing documents at the loading station, pay the wagon-occupied fee per the pricing article; the fee shall be paid within ten (10) calendar days from the date the Service Provider claims it, failing which compensation is payable calculated on the basis of the Commercial Bank of Ethiopia interest rate for the delay period.\\nBe responsible for safety matters, and indemnify and hold the Service Provider harmless against all consequences resulting from accidents arising from or associated with the loading and unloading process.\\nExecute and cover the cost of loading and unloading of cargo at both the loading station and Nagad railway freight yard.\\nFollow up that the cargo is loaded and unloaded on time.\\nDelegate representatives at both ends to consign and receive cargo with signature and stamp. Representatives shall hold a duly signed and stamped power of attorney and shall produce their ID or passport when consigning or receiving the cargo.\\nPrepare the necessary facilities to take over the transported cargo at Nagad freight yard upon arrival by issuing handover documents.\\nTake the transported cargo out of the wagons at Nagad freight yard within one (1) calendar day starting from the day following the notice of arrival.\\nPay the wagon-occupied fee per the pricing article for delays of more than one (1) calendar day at Nagad railway freight yard due to the fault of the Client in resolving customs or third-party claims or any other causes.\\nAfter the wagon list is submitted to the Client, if a wagon is not loaded due to the fault of the Client, pay 100% of the transportation price per wagon for each unloaded wagon.\\nPay the Service Provider 100% of the contract price in advance for each wagon.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Provide the list and identification numbers of wagons with sequence and locomotives at least twenty-four (24) hours in advance to the Client, with any correction at least twelve (12) hours before arrival at destination.\\nTransport the cargo from the loading station to Nagad railway freight yard.\\nProvide safe transportation of the cargo throughout the transit.\\nPresent customs clearance documents and mobilize the rolling stock as needed.\\nProvide the wagons assigned for the freight at the agreed place and time, and follow up that the cargo is loaded on time.\\nTransport and deliver the cargo taken over, in the condition received, within two (2) calendar days to Nagad railway freight yard.\\nWhere a wagon carrying cargo stops due to accident or mechanical problem, promptly notify the nearby customs station, police office, and the Client. A wagon stopped in Ethiopia due to mechanical defect shall be maintained within four (4) calendar days; within Nagad (Djibouti) territory within twelve (12) calendar days. In case of accident where the problem cannot be solved within one (1) calendar day and the wagon is not operational, the Service Provider shall have the cargo carried and delivered by another wagon, and shall provide an accident or defect report issued by the local police office regarding the sustained damage.\\nBuy a cargo liability insurance policy for each supplied wagon.\\nProvide wagon cleaning service and charge the cleaning fee based on actual expenditure.\\nIf the Client fails or refuses to receive the cargo beyond the allowable transport period, the Service Provider has the right to handle the cargo.\\nNeither party shall be liable for any indirect or consequential loss sustained by the other in connection with this Agreement.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "The parties have no obligation to pay demurrage or any other compensation if they have failed to discharge their obligations due to force majeure.\\nForce majeure shall be deemed to exist when the contract is not performed due to any event beyond the reasonable control of a party which prevents that party from complying with its obligations under this Agreement, including but not limited to:\\n- Acts of God (such as, but not limited to, fires, explosions, earthquakes, drought, tidal waves, and floods).\\n- War, hostilities (whether war is declared or not), invasion, acts of foreign enemies, mobilization, requisition, or embargo.\\n- Rebellion, revolution, insurrection, military or usurped power, or civil war.\\n- Contamination by radioactivity from any nuclear fuel or nuclear waste.\\n- Riot, commotion, strikes, go-slows, lockouts, or disorder.\\n- Acts of terrorism.\\nA party wishing to claim protection in respect of a force majeure event shall, as soon as possible following the occurrence or commencement of the event, notify the other party of its nature and expected duration, and shall thereafter keep the other party informed until it is able to perform its obligations under this Agreement.", "order": 4, "title": "Force Majeure"}, {"id": "pricing", "body": "The price of bulk cargo transportation from the loading station to Nagad, together with any applicable demurrage and surcharge rates, is set out in the Rate Schedule immediately below, expressed as unit prices per origin → destination lane and per wagon.\\nPayment for transport services shall be made in Birr based on the selling price of USD to Birr on the date of payment set by the Commercial Bank of Ethiopia.\\nIf there is an increment or decrement of the USD exchange rate to Birr between the date of payment and the date the wagon/train number is provided to the Client, either the Client shall make the additional payment to the Service Provider or the Service Provider shall refund the difference from the initial payment to the Client.\\nThe cost of loading at the loading station and unloading at Nagad shall be covered by the Client and is not part of this contract agreement.\\nThe Client shall pay 100% of the contract price in advance.\\nThe Client shall pay a demurrage fee for occupied wagons at the rate set out in the Rate Schedule for the applicable occupancy band.\\nDemurrage payment shall be made in Birr based on the selling price of USD to Birr set by the Commercial Bank of Ethiopia on the date of the demurrage occurrence.", "order": 5, "title": "Contract Price and Terms of Payment"}, {"id": "contract-documents", "body": "The following documents shall constitute the contract between the Client and the Service Provider:\\n- Amendments made to this contract (if any).\\n- This Contract Agreement.\\n- Final minutes of negotiation (if any).\\nIf there is any discrepancy between the documents, they shall be interpreted with priority in the order listed above.", "order": 6, "title": "Contract Documents"}, {"id": "documentation", "body": "The following documents shall be delivered to the Client upon request for settlement:\\n- Consignment Note (cargo handover document to the Client).\\n- Summary of payment request of the Service Provider prepared as per the agreed tariff.", "order": 7, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider must certify the taking over of the goods on the duplicates of the consignment note in an appropriate manner and return the duplicate to the Client.\\nA consignment note shall be prima facie evidence of the receipt of the goods by the Service Provider and of the kind, number, and weight of the goods.", "order": 8, "title": "Consignment Notes"}, {"id": "termination", "body": "The contract may be terminated:\\n- Upon mutual consent of the parties.\\n- Upon completion of the contract period.\\n- If either or both parties breach a fundamental provision of the contract, upon prior legal notice delivered by either party.", "order": 9, "title": "Termination of Contract"}, {"id": "effectiveness", "body": "The contract shall come into full force and effect on the date when all of the following are accomplished:\\n- The contract is signed by the Client and the Service Provider.\\n- The Service Provider has received the advance payment of 100% of the contract price for each train set of cargo.", "order": 10, "title": "Contract Effectiveness"}, {"id": "cargo-amount", "body": "The minimum cargo to be transported shall be one wagon.", "order": 11, "title": "Cargo Amount"}, {"id": "duration", "body": "The contract shall last for three (3) months starting from the date of contract signing, with possible extension upon mutual agreement.", "order": 12, "title": "Duration of Contract"}, {"id": "disputes", "body": "If a dispute arises between the parties, they shall exert efforts to settle their differences amicably.\\nIf the parties fail to settle their disputes amicably, the case shall be taken to the competent Federal Court of law presiding in Addis Ababa.\\nThe governing law shall be the laws of the Federal Democratic Republic of Ethiopia.", "order": 13, "title": "Settlement of Disputes"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('c6051eea-ae34-49aa-9c42-6d9bc6e30d85', 'INTERCITY_BULK', 'Bulk Intercity Contract', 'Domestic (intercity) bulk cargo transportation by railway between Ethiopian freight yards, e.g. Dire Dawa to Sebeta.', 'Bulk Cargo Transportation Service by Railway (Intercity)', '["The Client has requested the Service Provider to transport bulk cargo between the agreed Ethiopian railway freight yards using the Ethio–Djibouti Railway.", "The Service Provider has accepted the Client''s request to render the said transportation service."]', '[{"id": "objective", "body": "The Service Provider shall undertake the railway transportation of bulk cargo from the agreed origin railway freight yard to the agreed destination railway freight yard.", "order": 1, "title": "Objective of the Contract"}, {"id": "client-obligations", "body": "Provide written instructions to the Service Provider to transport a minimum of sixteen (16) wagons of cargo per consignment.\\nPrepare all necessary documents, including laboratory tests from the pertinent organ and off-taking contract where applicable, and the facilities required to sign the contract and make the cargo ready for transport.\\nAssign representatives at the origin yard and other stations, as required, to hand over the cargo to the Service Provider and handle transit clearance if required.\\nTransport the cargo to the designated loading points at the origin yard.\\nBe responsible for cargo handling: loading at the origin yard and unloading at the destination yard, in accordance with the standards set by the EDR operations and technical terms.\\nMake advance payment to the Service Provider for services in accordance with the payment terms and conditions of this contract.\\nFollow up to ensure that the cargo is loaded and unloaded on time.\\nDelegate representatives at the cargo destination to immediately receive the transported cargo.\\nEnsure representatives are duly authorized with a power of attorney, signed and stamped by the Client, and present valid identification (ID or passport) when consigning or receiving cargo.\\nMaintain detailed information including item, weight, and destination of the cargo, and communicate the same to the Service Provider or its nominated agent via written notice, email, or fax.\\nPrepare the necessary facilities to immediately take over the transported cargo at the destination upon arrival and provide sufficient trucks at the destination freight yard for unloading from railway wagons.\\nUpon arrival of the train/wagon at the unloading site, sign the train arrival confirmation sheet to acknowledge the arrival time.\\nInspect the loaded wagons jointly with the Service Provider and EDR at the loading yard, and again with the customs agent (if required) and the Service Provider at the destination yard.\\nAfter receiving the cargo, sign the Freight Carriage Acceptance Sheet (copies II, III, and IV) immediately to confirm delivery.\\nCompensate the Service Provider or any third party for actual loss or damage caused to persons, property, or wagons during unloading where such damage is attributable to the Client''s fault.\\nEach consignment (train) shall be granted three (3) hours of free time at the loading station and one (1) day at the unloading station. For each additional 3 hours of loading or parking the Client shall pay ETB 5,000 (five thousand) per wagon, and for each additional day of unloading ETB 5,000 (five thousand) per wagon per day.\\nBear demurrage charges of ETB 5,000 (five thousand) per wagon per 3 hours for delays exceeding three (3) hours at any station resulting from the Client''s failure to resolve customs or third-party claims.\\nPay 100% of the transport price in advance. Any additional charges or fees shall be paid within ten (10) calendar days after submission of the Service Provider''s payment request.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Provide the necessary train(s) to execute the transportation service under this contract, and furnish the Client with the list and identification numbers of wagons and locomotives at least 24 hours in advance, with corrections (if any) communicated at least 12 hours before the expected time of arrival at destination.\\nProvide pre-arrival notification including the train number to the discharging terminal and customs at least 24/12 hours before train arrival.\\nTransport the cargo from origin to destination within two (2) days from completion of loading (time counting starts upon completion of documentation and loading).\\nProvide safe transportation of the cargo throughout transit.\\nDeliver the cargo to the Client at the destination railway freight yard in the same condition as received.\\nPurchase a cargo liability insurance policy for each wagon transported.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "Neither party shall be liable due to a force majeure event.\\nFor the purposes of this contract, force majeure shall mean any unforeseeable event or circumstance beyond the reasonable control of the affected party which absolutely prevents the performance of the contract, including but not limited to natural disasters, war, civil commotion, strikes, government actions, epidemics, or interruption of railway operations due to accidents or infrastructure failure.\\nThe affected party shall notify the other party in writing within a reasonable period not exceeding two (2) hours after the occurrence of the force majeure event, providing evidence and details of the impact on performance and the mitigating steps taken.", "order": 4, "title": "Force Majeure"}, {"id": "liability", "body": "The Service Provider will be responsible for any loss, shortage, or damage occurring to the cargo it has received.", "order": 5, "title": "Liabilities Related to Damages and Losses"}, {"id": "pricing", "body": "The price for transporting cargo from the origin freight yard to the destination freight yard is set out in the Rate Schedule immediately below, expressed as a unit price per origin → destination lane and per wagon.\\nEach wagon shall be loaded with a maximum of 70 (seventy) metric tons.\\nPayment for transport services may be made in Ethiopian Birr, based on the Commercial Bank of Ethiopia''s official selling exchange rate of USD to Birr on the date of payment.\\nIf the exchange rate changes between the payment and the wagon assignment date, payment adjustments will be made accordingly.\\nThe contract price shall include the cost of railway transportation from the origin freight yard to the destination freight yard.\\nExcluded cost: cargo handling (loading and unloading) is not included in the contract price and shall remain the sole responsibility of the Client.", "order": 6, "title": "Contract Price"}, {"id": "contract-documents", "body": "The following documents shall constitute the contract between the Client and the Service Provider:\\n- Amendments made to this contract (if any).\\n- This Contract Agreement.\\n- Final minutes of negotiation (if any).\\nIf there is any discrepancy between the documents, they shall be interpreted with priority in the order listed above.", "order": 7, "title": "Contract Documents"}, {"id": "documentation", "body": "The following documents shall be delivered to the Client by the Service Provider to collect and settle payment:\\n- Freight Carriage Acceptance Sheet of the Ethio-Djibouti Railway.\\n- Notice of collecting transportation and miscellaneous charges of the Ethio-Djibouti Railway (if any).\\n- Summary of payment request of the Service Provider prepared as per the agreed tariff.\\n- Railway Waybill.", "order": 8, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider must certify the taking over of the goods on copy III (kept by the consignee for future reference) of the Freight Carriage Acceptance Sheet of the Ethio-Djibouti Railway in an appropriate manner and provide it to the Client.\\nThe Freight Carriage Acceptance Sheet shall be prima facie evidence of the receipt of the goods by the Service Provider and of the kind, number, and weight of the goods.", "order": 9, "title": "Consignment Notes"}, {"id": "termination", "body": "The contract may be terminated:\\n- Upon mutual consent of the parties.\\n- Upon completion of the contract period or amount of cargo, whichever comes first.\\n- If either or both parties breach a fundamental provision of the contract, upon one-week prior legal notice delivered by either party.", "order": 10, "title": "Termination of Contract"}, {"id": "duration", "body": "The contract duration shall be three (3) months from the date of effectiveness of the contract, with possible extension upon mutual agreement of the parties.", "order": 11, "title": "Duration of Contract"}, {"id": "disputes", "body": "If a dispute arises between the parties, they shall exert efforts to settle their differences amicably.\\nIf the parties fail to settle their dispute amicably, the case shall be taken to the competent Federal Court of law presiding in Addis Ababa.\\nThe governing law shall be the laws of the Federal Democratic Republic of Ethiopia.", "order": 12, "title": "Settlement of Disputes"}, {"id": "effectiveness", "body": "The contract shall come into full force and effect on the date when the contract is signed by both parties.", "order": 13, "title": "Contract Effectiveness"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('5fba1daf-4e75-407d-855c-73c0545e7b46', 'IMPORT_CONTAINER_CUSTOMS', 'Container Import Contract (with customs clearing)', 'Import container transport by railway from SGTD (Djibouti) to Dire Dawa, Modjo dry port, or Galaan Multipurpose Port, with empty-container return. Customs clearing is performed by the Service Provider.', 'Import Container Transport Service by Railway', '["The Client has requested and agreed to the transportation of container cargo from SGTD railway freight station at Djibouti to Dire Dawa, Modjo dry port, and Galaan Multipurpose Port (GMP), and the return of empty containers from Dire Dawa, Modjo dry port, and Galaan Multipurpose Port (GMP) to SGTD railway freight station using the Addis Ababa–Djibouti railway line.", "The Service Provider has agreed to transport the container cargo as per the terms of this contract."]', '[{"id": "objective", "body": "To provide railway transportation services for 40ft and/or 20ft full containers from SGTD to Dire Dawa, Modjo dry port, and/or Galaan Multipurpose Port (GMP), and empty container return from Dire Dawa, Modjo dry port, and Galaan Multipurpose Port (GMP) to SGTD.\\nThe scope of the services comprises:\\n- Railway transport service.\\n- Cargo handling at Galaan Multipurpose Port (GMP).", "order": 1, "title": "Objective and Scope of the Services"}, {"id": "client-obligations", "body": "Give written/email/electronic shipment instructions to the Service Provider for transportation of container cargo from SGTD to Dire Dawa, Modjo dry port, and/or Galaan Multipurpose Port (GMP).\\nPrepare all necessary documents and facilities for shipment.\\nEnsure the following minimum supply of containers per shipment based on the loading terminal and destination:\\n- Minimum of twenty-five (25) 40ft containers or fifty (50) 20ft containers to Modjo dry port.\\n- Minimum of ten (10) 40ft containers or twenty (20) 20ft containers to Dire Dawa dry port.\\n- Minimum of one (1) 40ft container or two (2) TEU to Galaan Multipurpose Port (GMP).\\nOne flat wagon must carry either one 40ft container or two 20ft containers.\\nIf two 20ft containers are loaded on one flat wagon, their weight difference must not exceed 10 tons.\\nEnsure timely loading and unloading of cargo.\\nAssign representatives at both ends to oversee container handover and ensure the necessary arrangements for cargo reception at the destination upon arrival.\\nMaintain and provide detailed cargo information (type, weight, destination, etc.).\\nBe responsible for cargo handling, loading, and unloading of both empty and full containers at Modjo, Dire Dawa dry port, and SGTD.\\nBook wagons at least five (5) days in advance.\\nEnsure containers are ready one day before the planned loading date.\\nSubmit all required documents to the Djibouti Nagad station at least 24 hours in advance before starting to load. Failure to submit the documents within the stipulated time shall result in the following demurrage charges, calculated as a percentage of the booked wagon price:\\n- Delay of up to twelve (12) hours: 20% of the booked wagon price.\\n- Delay exceeding twelve (12) hours but not more than one (1) day: 50% of the booked wagon price.\\n- Delay of more than one (1) day: 100% of the booked wagon price.\\nCollect the full container from Galaan Multipurpose Port (GMP) within three (3) calendar days from the day following the arrival notice.\\nIf the Client fails to collect the container within the specified period, the Service Provider shall have the right to reposition the container to any location it deems appropriate; in such case, the Service Provider shall not be held responsible for any damage or loss arising from such repositioning.\\nIf the Client fails to collect the container from Galaan Multipurpose Port within the specified period, the Client shall be liable to pay demurrage charges of 15 USD per day per 20ft container and 27 USD per day per 40ft container, calculated based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date of payment.\\nIn the event the Client fails to collect the container(s) within the specified period, the Client shall be liable to pay double handling charges of 27 USD per 20ft container per handling or 40 USD per 40ft container per handling, calculated based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date of payment.\\nIf empty containers cannot be offloaded from the train upon arrival at SGTD due to any Client-related issue, the Client shall be liable for the applicable penalty charges.\\nPenalty charges for delay at SGTD/Nagad upon train arrival: 20 USD per day per 20ft container; 33 USD per day per 40ft container.\\nCollect the containers within one day at Dire Dawa and Modjo dry port, or make the necessary payment to the dry port as per the standard of the dry port.\\nFor returning empty containers, deliver to Dire Dawa dry port, Modjo dry port, or Galaan Multipurpose Port.\\nOnce the empty containers are returned from the Client''s premises and stored at Dire Dawa/Modjo dry port while awaiting train allocation for return to SGTD, any demurrage and/or storage charges incurred from the dry port thereafter shall not be the responsibility or liability of the Service Provider; the Client shall be solely responsible for settling such charges.\\nProvide clean empty containers that meet SGTD standards. If the port refuses to take over an empty container because of inside cleanliness problems, additional cleaning costs incurred due to non-compliance will be borne by the Client.\\nEnsure containers are structurally intact and meet weight distribution requirements.\\nProhibited cargo: cargo covered with tarpaulin is not allowed due to safety risks.\\nNotify the Service Provider forty-eight (48) hours in advance before wagon booking if transporting hazardous or valuable goods.\\nIf a booked wagon is not loaded due to Client-related issues, including but not limited to a damaged container, missing lock, unpaid demurrage, port system errors, or incomplete documentation and submission, the Client shall be charged 100% of the total price of the reserved wagon.\\nRefund requests before wagon booking require an official request, with a 3% administrative fee deducted. If the refund is due to the Service Provider, the Client shall receive a full refund. Refunds not requested and completed within six months shall be considered waived.\\nPay 100% of the transportation fee in advance for each train set.\\nSettle additional penalties due to non-compliance within ten (10) days of invoice issuance.\\nLate payment incurs a penalty of an additional 10%.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Assign the necessary voyage based on the operational schedule and cargo demand, and notify the train schedule 48 hours in advance.\\nProvide a list of wagons/voyage or train number 24 hours in advance and update corrections 12 hours before arrival.\\nProvide safe transportation of the containers.\\nDeliver the cargo within two (2) days after train departure, provided that all required documents are submitted on time and no unforeseen circumstances or events occur.\\nReturn empty containers from Dire Dawa, Modjo, and Galaan Multipurpose Port to SGTD within seven (7) calendar days of receipt.\\nIn the event of export cargo operations, the Service Provider may prioritize the loading of export containers during the loading of empty containers and the unloading of import containers from the train at Galaan Multipurpose Port.\\nThe Service Provider reserves the right to refuse the transportation of any cargo classified as dangerous goods.\\nIf any operational, technical, or mechanical problem occurs throughout the transit, notify customs and arrange cargo transfer within 4 days if the incident occurs in Ethiopia, or within 6 days if it occurs in Djibouti.\\nProvide accident or defect reports if needed.\\nBuy cargo liability insurance for each wagon.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "Neither party shall be liable for delays or non-performance caused by force majeure events beyond their reasonable control.\\nForce majeure shall be interpreted in accordance with the Ethiopian Civil Code.", "order": 4, "title": "Force Majeure"}, {"id": "pricing", "body": "The railway transportation rate for each corridor (per one 40ft container or per two 20ft containers, with or without empty return where applicable) is set out in the Rate Schedule immediately below, expressed as a unit price per origin → destination lane and per container.\\nIf cargo exceeds 40 tons per two 20ft containers of gross weight, additional charges apply proportionally.\\nGross weight shall be the total sum of cargo, packing, and container tare weight.\\nPayment for any additional tonnage shall be made in advance before the container is loaded onto the wagon.\\nThe price of loading and unloading and container handling at Modjo, Dire Dawa dry port, and SGTD container railway freight yard is not part of this contract; it is the Client''s responsibility.\\nAdditional costs (if applicable):\\n- Last-mile delivery service by truck from Galaan Multipurpose Port or Modjo to Addis Ababa or Modjo and surrounding areas shall incur an additional cost, fully covered by the Client.\\n- For clients utilizing EDR''s last-mile logistics services, the applicable charges shall vary based on the cargo movement route.\\n- The charge for last-mile delivery from Galaan Multipurpose Port and Modjo dry port shall be communicated to the Client by the Service Provider after receiving the necessary details regarding the cargo destination, type, and weight.\\nAll payments shall be made one hundred percent (100%) in advance in United States Dollars (USD).", "order": 5, "title": "Contract Price and Terms of Payment"}, {"id": "contract-documents", "body": "The following documents constitute this contract:\\n- Amendments (if any).\\n- This Contract Agreement.\\n- Final minutes of negotiation (if any).\\nIf there is any discrepancy between the documents, they shall be interpreted with priority in the order listed above.", "order": 6, "title": "Contract Documents"}, {"id": "documentation", "body": "Equipment Interchange Receipt of SGTD, Railway Waybill, Container Carriage Acceptance Sheet, and incidental charges (if any).\\nPayment summary as per the agreed contract price (if required).", "order": 7, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider must certify the taking over and handing over of the container cargo on the Freight Carriage Acceptance Sheet of EDR in an appropriate manner and provide it to the Client.\\nThe Freight Carriage Acceptance Sheet shall be prima facie evidence of the receipt of the container cargo by the Service Provider and of the kind, number, and weight of the goods.\\nUpon delivery of the cargo to the Client, the Cargo Handover Out Voucher signed by both parties shall serve as evidence of cargo receipt.", "order": 8, "title": "Consignment Notes"}, {"id": "amendment", "body": "This contract can be amended by mutual agreement.\\nNotwithstanding the above, the Service Provider may revise transport tariffs due to operational and regulatory changes by providing at least five (5) working days'' prior written notice to the Client.", "order": 9, "title": "Amendment"}, {"id": "termination", "body": "The contract may be terminated:\\n- By mutual agreement.\\n- Upon completion of the contract period or agreed cargo shipments.\\n- If either party breaches fundamental terms.", "order": 10, "title": "Termination of Contract"}, {"id": "effectiveness", "body": "The contract is valid once signed by both parties.", "order": 11, "title": "Contract Effectiveness"}, {"id": "duration", "body": "Valid until August 31, {{contractYear}}, with a possible extension upon mutual agreement.", "order": 12, "title": "Contract Period"}, {"id": "disputes", "body": "Disputes shall be settled amicably.\\nIf unresolved, disputes shall be taken to the Federal Court in Addis Ababa.", "order": 13, "title": "Settlement of Disputes"}, {"id": "customs-clearing", "body": "The Service Provider shall carry out customs clearing on behalf of the Client for the cargo covered by this Agreement, including declaration, lodgement, and follow-up at the customs stations of Djibouti and Ethiopia as applicable to the agreed corridor.\\nThe Service Provider shall act only within the authority granted by the Client and shall not amend a declaration without the Client''s written instruction.\\nCustoms duties, taxes, and any government charges assessed on the cargo remain payable by the Client and are not included in the freight price; the Service Provider shall settle them on the Client''s behalf only where the Client has placed the corresponding funds in advance.\\nThe Service Provider shall hand over all customs documents obtained in the course of clearing to the Client upon completion of each shipment.", "order": 14, "title": "Customs Clearing Services"}, {"id": "customs-client-duties", "body": "Grant the Service Provider a duly signed and stamped power of attorney authorising it to act as the Client''s customs agent for the duration of this Agreement.\\nSubmit every document required for declaration (commercial invoice, packing list, bill of lading or airway bill, permits, certificates of origin, and any authority-specific licence) within one (1) calendar day of the Service Provider''s request.\\nWarrant that the declared description, quantity, value, and tariff classification of the cargo are complete and accurate.\\nBear any penalty, demurrage, storage, or re-inspection cost arising from incorrect, incomplete, or late Client-supplied information or documentation.\\nSettle assessed duties and taxes within the period notified by the Service Provider, failing which the Service Provider may suspend clearing and the cargo shall remain at the Client''s risk and cost.", "order": 15, "title": "Client Obligations for Customs Clearing"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('8cd8e805-178a-4faa-bdbb-2e7ac5f75e7c', 'IMPORT_CONTAINER_NO_CUSTOMS', 'Container Import Contract (without customs clearing)', 'Import container transport by railway from SGTD (Djibouti) to Dire Dawa, Modjo dry port, or Galaan Multipurpose Port, with empty-container return. Customs clearing is handled by the Client.', 'Import Container Transport Service by Railway', '["The Client has requested and agreed to the transportation of container cargo from SGTD railway freight station at Djibouti to Dire Dawa, Modjo dry port, and Galaan Multipurpose Port (GMP), and the return of empty containers from Dire Dawa, Modjo dry port, and Galaan Multipurpose Port (GMP) to SGTD railway freight station using the Addis Ababa–Djibouti railway line.", "The Service Provider has agreed to transport the container cargo as per the terms of this contract."]', '[{"id": "objective", "body": "To provide railway transportation services for 40ft and/or 20ft full containers from SGTD to Dire Dawa, Modjo dry port, and/or Galaan Multipurpose Port (GMP), and empty container return from Dire Dawa, Modjo dry port, and Galaan Multipurpose Port (GMP) to SGTD.\\nThe scope of the services comprises:\\n- Railway transport service.\\n- Cargo handling at Galaan Multipurpose Port (GMP).", "order": 1, "title": "Objective and Scope of the Services"}, {"id": "client-obligations", "body": "Give written/email/electronic shipment instructions to the Service Provider for transportation of container cargo from SGTD to Dire Dawa, Modjo dry port, and/or Galaan Multipurpose Port (GMP).\\nPrepare all necessary documents and facilities for shipment.\\nEnsure the following minimum supply of containers per shipment based on the loading terminal and destination:\\n- Minimum of twenty-five (25) 40ft containers or fifty (50) 20ft containers to Modjo dry port.\\n- Minimum of ten (10) 40ft containers or twenty (20) 20ft containers to Dire Dawa dry port.\\n- Minimum of one (1) 40ft container or two (2) TEU to Galaan Multipurpose Port (GMP).\\nOne flat wagon must carry either one 40ft container or two 20ft containers.\\nIf two 20ft containers are loaded on one flat wagon, their weight difference must not exceed 10 tons.\\nEnsure timely loading and unloading of cargo.\\nAssign representatives at both ends to oversee container handover and ensure the necessary arrangements for cargo reception at the destination upon arrival.\\nMaintain and provide detailed cargo information (type, weight, destination, etc.).\\nBe responsible for cargo handling, loading, and unloading of both empty and full containers at Modjo, Dire Dawa dry port, and SGTD.\\nBook wagons at least five (5) days in advance.\\nEnsure containers are ready one day before the planned loading date.\\nSubmit all required documents to the Djibouti Nagad station at least 24 hours in advance before starting to load. Failure to submit the documents within the stipulated time shall result in the following demurrage charges, calculated as a percentage of the booked wagon price:\\n- Delay of up to twelve (12) hours: 20% of the booked wagon price.\\n- Delay exceeding twelve (12) hours but not more than one (1) day: 50% of the booked wagon price.\\n- Delay of more than one (1) day: 100% of the booked wagon price.\\nCollect the full container from Galaan Multipurpose Port (GMP) within three (3) calendar days from the day following the arrival notice.\\nIf the Client fails to collect the container within the specified period, the Service Provider shall have the right to reposition the container to any location it deems appropriate; in such case, the Service Provider shall not be held responsible for any damage or loss arising from such repositioning.\\nIf the Client fails to collect the container from Galaan Multipurpose Port within the specified period, the Client shall be liable to pay demurrage charges of 15 USD per day per 20ft container and 27 USD per day per 40ft container, calculated based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date of payment.\\nIn the event the Client fails to collect the container(s) within the specified period, the Client shall be liable to pay double handling charges of 27 USD per 20ft container per handling or 40 USD per 40ft container per handling, calculated based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date of payment.\\nIf empty containers cannot be offloaded from the train upon arrival at SGTD due to any Client-related issue, the Client shall be liable for the applicable penalty charges.\\nPenalty charges for delay at SGTD/Nagad upon train arrival: 20 USD per day per 20ft container; 33 USD per day per 40ft container.\\nCollect the containers within one day at Dire Dawa and Modjo dry port, or make the necessary payment to the dry port as per the standard of the dry port.\\nFor returning empty containers, deliver to Dire Dawa dry port, Modjo dry port, or Galaan Multipurpose Port.\\nOnce the empty containers are returned from the Client''s premises and stored at Dire Dawa/Modjo dry port while awaiting train allocation for return to SGTD, any demurrage and/or storage charges incurred from the dry port thereafter shall not be the responsibility or liability of the Service Provider; the Client shall be solely responsible for settling such charges.\\nProvide clean empty containers that meet SGTD standards. If the port refuses to take over an empty container because of inside cleanliness problems, additional cleaning costs incurred due to non-compliance will be borne by the Client.\\nEnsure containers are structurally intact and meet weight distribution requirements.\\nProhibited cargo: cargo covered with tarpaulin is not allowed due to safety risks.\\nNotify the Service Provider forty-eight (48) hours in advance before wagon booking if transporting hazardous or valuable goods.\\nIf a booked wagon is not loaded due to Client-related issues, including but not limited to a damaged container, missing lock, unpaid demurrage, port system errors, or incomplete documentation and submission, the Client shall be charged 100% of the total price of the reserved wagon.\\nRefund requests before wagon booking require an official request, with a 3% administrative fee deducted. If the refund is due to the Service Provider, the Client shall receive a full refund. Refunds not requested and completed within six months shall be considered waived.\\nPay 100% of the transportation fee in advance for each train set.\\nSettle additional penalties due to non-compliance within ten (10) days of invoice issuance.\\nLate payment incurs a penalty of an additional 10%.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Assign the necessary voyage based on the operational schedule and cargo demand, and notify the train schedule 48 hours in advance.\\nProvide a list of wagons/voyage or train number 24 hours in advance and update corrections 12 hours before arrival.\\nProvide safe transportation of the containers.\\nDeliver the cargo within two (2) days after train departure, provided that all required documents are submitted on time and no unforeseen circumstances or events occur.\\nReturn empty containers from Dire Dawa, Modjo, and Galaan Multipurpose Port to SGTD within seven (7) calendar days of receipt.\\nIn the event of export cargo operations, the Service Provider may prioritize the loading of export containers during the loading of empty containers and the unloading of import containers from the train at Galaan Multipurpose Port.\\nThe Service Provider reserves the right to refuse the transportation of any cargo classified as dangerous goods.\\nIf any operational, technical, or mechanical problem occurs throughout the transit, notify customs and arrange cargo transfer within 4 days if the incident occurs in Ethiopia, or within 6 days if it occurs in Djibouti.\\nProvide accident or defect reports if needed.\\nBuy cargo liability insurance for each wagon.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "Neither party shall be liable for delays or non-performance caused by force majeure events beyond their reasonable control.\\nForce majeure shall be interpreted in accordance with the Ethiopian Civil Code.", "order": 4, "title": "Force Majeure"}, {"id": "pricing", "body": "The railway transportation rate for each corridor (per one 40ft container or per two 20ft containers, with or without empty return where applicable) is set out in the Rate Schedule immediately below, expressed as a unit price per origin → destination lane and per container.\\nIf cargo exceeds 40 tons per two 20ft containers of gross weight, additional charges apply proportionally.\\nGross weight shall be the total sum of cargo, packing, and container tare weight.\\nPayment for any additional tonnage shall be made in advance before the container is loaded onto the wagon.\\nThe price of loading and unloading and container handling at Modjo, Dire Dawa dry port, and SGTD container railway freight yard is not part of this contract; it is the Client''s responsibility.\\nAdditional costs (if applicable):\\n- Last-mile delivery service by truck from Galaan Multipurpose Port or Modjo to Addis Ababa or Modjo and surrounding areas shall incur an additional cost, fully covered by the Client.\\n- For clients utilizing EDR''s last-mile logistics services, the applicable charges shall vary based on the cargo movement route.\\n- The charge for last-mile delivery from Galaan Multipurpose Port and Modjo dry port shall be communicated to the Client by the Service Provider after receiving the necessary details regarding the cargo destination, type, and weight.\\nAll payments shall be made one hundred percent (100%) in advance in United States Dollars (USD).", "order": 5, "title": "Contract Price and Terms of Payment"}, {"id": "contract-documents", "body": "The following documents constitute this contract:\\n- Amendments (if any).\\n- This Contract Agreement.\\n- Final minutes of negotiation (if any).\\nIf there is any discrepancy between the documents, they shall be interpreted with priority in the order listed above.", "order": 6, "title": "Contract Documents"}, {"id": "documentation", "body": "Equipment Interchange Receipt of SGTD, Railway Waybill, Container Carriage Acceptance Sheet, and incidental charges (if any).\\nPayment summary as per the agreed contract price (if required).", "order": 7, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider must certify the taking over and handing over of the container cargo on the Freight Carriage Acceptance Sheet of EDR in an appropriate manner and provide it to the Client.\\nThe Freight Carriage Acceptance Sheet shall be prima facie evidence of the receipt of the container cargo by the Service Provider and of the kind, number, and weight of the goods.\\nUpon delivery of the cargo to the Client, the Cargo Handover Out Voucher signed by both parties shall serve as evidence of cargo receipt.", "order": 8, "title": "Consignment Notes"}, {"id": "amendment", "body": "This contract can be amended by mutual agreement.\\nNotwithstanding the above, the Service Provider may revise transport tariffs due to operational and regulatory changes by providing at least five (5) working days'' prior written notice to the Client.", "order": 9, "title": "Amendment"}, {"id": "termination", "body": "The contract may be terminated:\\n- By mutual agreement.\\n- Upon completion of the contract period or agreed cargo shipments.\\n- If either party breaches fundamental terms.", "order": 10, "title": "Termination of Contract"}, {"id": "effectiveness", "body": "The contract is valid once signed by both parties.", "order": 11, "title": "Contract Effectiveness"}, {"id": "duration", "body": "Valid until August 31, {{contractYear}}, with a possible extension upon mutual agreement.", "order": 12, "title": "Contract Period"}, {"id": "disputes", "body": "Disputes shall be settled amicably.\\nIf unresolved, disputes shall be taken to the Federal Court in Addis Ababa.", "order": 13, "title": "Settlement of Disputes"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('9b2636ec-3ad4-4f26-9133-6a6c9fc3f798', 'EXPORT_CONTAINER_CUSTOMS', 'Container Export Contract (with customs clearing)', 'Export container transport, freight forwarding, and customs clearing from Galaan Multipurpose Port or Modjo dry port to SGTD container freight station (Djibouti). Customs clearing is performed by the Service Provider.', 'Export Container Transport, Freight Forwarding and Customs Clearing Service', '["The parties have agreed on the following services: rail transport, customs clearance, transit work, freight forwarding, and handling of container cargo."]', '[{"id": "objective", "body": "Customs clearance (Ethiopia side):\\n- Processing of export declarations.\\n- Coordination with the Ethiopian Customs Authority for clearance.\\n- Ensuring compliance with all export regulations.\\nRail transport:\\n- Transportation of containers from Galaan Multipurpose Port (GMP) or Modjo dry port to SGTD container freight station.\\nDjibouti transit and handling:\\n- Customs clearance in Djibouti.\\n- Coordination with Djibouti port and transit authorities.\\n- Freight forwarding and last-mile facilitation as required.\\nExcluded costs:\\n- Shore handling.\\n- Shifting of containers from SGTD to DMP or DMP to SGTD port.", "order": 1, "title": "Objective and Scope of the Services"}, {"id": "client-obligations", "body": "Give written/email instructions to the Service Provider for transportation of containers from Galaan Multipurpose Port (GMP) and/or Modjo to Djibouti.\\nSupply a minimum of two (2) 20ft containers (or an equivalent load to fill one flat wagon).\\nSubmit all forwarding service booking requests at least seventy-two (72) hours prior to the scheduled train departure and no later than 7 days before the vessel cut-off time, whichever is applicable.\\nFor clients requiring first-mile service, submit a first-mile service request notice no less than seventy-two (72) hours in advance.\\nComplete and submit accurate export documents as per the request of the Service Provider; payment must be submitted at least 36 hours before train departure.\\nDeliver all cargo to the designated loading port or freight station at least three (3) hours prior to the scheduled train loading time.\\nFailure to meet the stated deadlines may result in cancellation of the booking and transfer arrangements; any resulting delays, penalties, or additional costs shall be the sole responsibility of the Client.\\nIf the Client fails to deliver the container, fails to provide the requested documents for completing export documents as instructed above, or cancels after wagon reservation, the Client shall pay USD 150.00 per wagon as a penalty, after notification.\\nContainers must have four (4) undamaged corners.\\nOne flat wagon must carry either one 40ft container or two 20ft containers.\\nIf two 20ft containers are loaded on one flat wagon, their weight difference must not exceed 10 tons.\\nThe gross weight of the container must not exceed the standard loading capacity indicated on the container; the Client is responsible for ensuring full compliance with the maximum allowable load.\\nProhibited cargo: cargo covered with tarpaulin is not allowed due to safety risks.\\nIf the cargo to be transported is dangerous and/or valuable goods, notify the Service Provider 48 (forty-eight) hours before the wagon booking for further discussion and decision.\\nRefund requests before wagon booking require an official request, with a 3% administrative fee deducted. If the refund is due to the Service Provider, the Client shall receive a full refund. Refunds not requested and completed within six months shall be considered waived.\\nAny delay caused by missing or incorrect documents shall be the Client''s responsibility.\\n100% of the transportation and customs clearance fee must be paid in advance.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Complete gate pass processing and submit to Nagad Station for each shipment within eighteen (18) hours after train departure.\\nThe Service Provider reserves the right to refuse the transportation of any cargo classified as dangerous goods.\\nMaintain cargo liability insurance for railway transport; additional insurance for port handling or last-mile transport shall be the Client''s responsibility.\\nThe Service Provider is not liable for customs penalties or demurrage due to delays beyond its control.\\nNotify the Client immediately, in writing, of any delays, port issues, or customs holds.\\nThe Service Provider shall not be liable for:\\n- Inherent defects of the cargo.\\n- Improper packing or loading conducted by the Client.\\n- Customs-related delays.\\n- Delays caused by force majeure events.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "Neither party shall be liable for delays or non-performance caused by force majeure events beyond their reasonable control.\\nForce majeure shall be interpreted in accordance with the Ethiopian Civil Code.", "order": 4, "title": "Force Majeure"}, {"id": "pricing", "body": "The railway transportation charges and the freight forwarding and customs clearance charges for each corridor (per 40ft container and per two 20ft containers) are set out in the Rate Schedule immediately below, expressed as unit prices per origin → destination lane and per container.\\nWhere the total cargo weight exceeds fifty (50) metric tons per two (2) 20ft containers, an additional charge shall apply for each excess metric ton at the overweight rate set out in the Rate Schedule.\\nFor consolidated containers containing more than one (1) shipping document, the first document shall be included under the agreed contract rate; any additional document within the same container shall be subject to the extra-document charge set out in the Rate Schedule.\\nPayment must be supported by an official receipt before cargo departs from Galaan Multipurpose Port/Modjo.\\nIf the Client uses PIL Shipping Line, any local charge incurred will be covered by the Client as per the invoice issued by the shipping line.\\nIf storage or demurrage occurs due to Client-related issues (delay in document submission, payment delay, or any other Client-related reason), the Client shall pay the corresponding charges; charges apply per day after the free storage period, based on the invoice and SGTD tariff.\\nDuring export season, EDR may provide seasonal export support through the facilitation of empty containers.\\nAdditional costs (if applicable):\\n- First-mile delivery service by truck within Addis Ababa or Modjo and surrounding areas, originating from warehouses or any other places designated by the Client, shall incur an additional cost fully covered by the Client.\\n- For clients utilizing EDR''s first- or last-mile logistics services, the applicable charges shall vary based on the cargo movement route.\\n- The charge for first-mile delivery to Galaan Multipurpose Port and Modjo dry port shall be communicated to the Client by the Service Provider after receiving the necessary details regarding the cargo origin, type, and weight.\\n- For any vessel outbound charges, IMO charges, or other fees not included in the port handling payment, the Service Provider shall request the Client to settle the required amount based on the official receipt issued by the port or the shipping line.\\nPayment terms:\\n- All charges, including rail transport and customs clearance charges, remain 100% payable in advance.\\n- Payments shall be calculated based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date the wagon or train number is provided.\\n- If the exchange rate changes between the payment and the wagon assignment date, adjustments will be made accordingly.\\n- Any additional costs incurred due to customs issues or port delays shall be borne by the Client and paid based on actual costs, supported by official receipts, within 10 days.\\n- Late payment incurs a penalty of 10%.", "order": 5, "title": "Pricing and Payment Terms"}, {"id": "contract-documents", "body": "The following documents shall constitute the contract between the Client and the Service Provider:\\n- Any amendments made to this contract (if applicable).\\n- This Contract Agreement.\\n- Final minutes of negotiation (if applicable).\\nIn the event of any discrepancy between these documents, they shall be interpreted with priority in the order listed above.", "order": 6, "title": "Contract Documents"}, {"id": "documentation", "body": "Consignment Note (cargo handover document).\\nPayment summary prepared as per the agreed tariff, if required.", "order": 7, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider must certify the taking over and handing over of the container cargo on the Freight Carriage Acceptance Sheet of EDR in an appropriate manner and provide it to the Client.\\nThe Freight Carriage Acceptance Sheet shall be prima facie evidence of the receipt of the container cargo by the Service Provider and of the kind, number, and weight of the goods.\\nUpon delivery of the cargo to the Client, the Cargo Handover Out Voucher signed by both parties shall serve as evidence of cargo receipt.", "order": 8, "title": "Consignment Notes"}, {"id": "amendment", "body": "This contract can be amended by mutual agreement.\\nNotwithstanding the above, the Service Provider may revise transport tariffs due to operational and regulatory changes by providing at least five (5) working days'' prior written notice to the Client.", "order": 9, "title": "Amendment"}, {"id": "termination", "body": "The contract may be terminated:\\n- By mutual agreement.\\n- Upon completion of the contract period or agreed cargo shipments.\\n- If either party breaches fundamental terms.\\nIf terminated for cause, the terminating party must issue a 15-day written notice specifying the breach and allow an opportunity to cure, if applicable.", "order": 10, "title": "Termination of Contract"}, {"id": "effectiveness", "body": "The contract is valid once signed by both parties.", "order": 11, "title": "Contract Effectiveness"}, {"id": "duration", "body": "Valid until August 31, {{contractYear}}, with a possible extension upon mutual agreement.", "order": 12, "title": "Contract Period"}, {"id": "disputes", "body": "Disputes shall be settled amicably.\\nIf amicable settlement fails, disputes shall be submitted to the Federal Court located in Addis Ababa.\\nThe signatories confirm that they are fully authorized to sign and execute this Contract Agreement; the power of attorney of the signatories for the parties is enclosed with this contract agreement.", "order": 13, "title": "Settlement of Disputes"}, {"id": "customs-clearing", "body": "The Service Provider shall carry out customs clearing on behalf of the Client for the cargo covered by this Agreement, including declaration, lodgement, and follow-up at the customs stations of Djibouti and Ethiopia as applicable to the agreed corridor.\\nThe Service Provider shall act only within the authority granted by the Client and shall not amend a declaration without the Client''s written instruction.\\nCustoms duties, taxes, and any government charges assessed on the cargo remain payable by the Client and are not included in the freight price; the Service Provider shall settle them on the Client''s behalf only where the Client has placed the corresponding funds in advance.\\nThe Service Provider shall hand over all customs documents obtained in the course of clearing to the Client upon completion of each shipment.", "order": 14, "title": "Customs Clearing Services"}, {"id": "customs-client-duties", "body": "Grant the Service Provider a duly signed and stamped power of attorney authorising it to act as the Client''s customs agent for the duration of this Agreement.\\nSubmit every document required for declaration (commercial invoice, packing list, bill of lading or airway bill, permits, certificates of origin, and any authority-specific licence) within one (1) calendar day of the Service Provider''s request.\\nWarrant that the declared description, quantity, value, and tariff classification of the cargo are complete and accurate.\\nBear any penalty, demurrage, storage, or re-inspection cost arising from incorrect, incomplete, or late Client-supplied information or documentation.\\nSettle assessed duties and taxes within the period notified by the Service Provider, failing which the Service Provider may suspend clearing and the cargo shall remain at the Client''s risk and cost.", "order": 15, "title": "Client Obligations for Customs Clearing"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('79b1c17d-ae24-4945-98c5-ec3f58823ce7', 'EXPORT_CONTAINER_NO_CUSTOMS', 'Container Export Contract (without customs clearing)', 'Export container transport, freight forwarding, and customs clearing from Galaan Multipurpose Port or Modjo dry port to SGTD container freight station (Djibouti). Customs clearing is handled by the Client.', 'Export Container Transport, Freight Forwarding and Customs Clearing Service', '["The parties have agreed on the following services: rail transport, customs clearance, transit work, freight forwarding, and handling of container cargo."]', '[{"id": "objective", "body": "Customs clearance (Ethiopia side):\\n- Processing of export declarations.\\n- Coordination with the Ethiopian Customs Authority for clearance.\\n- Ensuring compliance with all export regulations.\\nRail transport:\\n- Transportation of containers from Galaan Multipurpose Port (GMP) or Modjo dry port to SGTD container freight station.\\nDjibouti transit and handling:\\n- Customs clearance in Djibouti.\\n- Coordination with Djibouti port and transit authorities.\\n- Freight forwarding and last-mile facilitation as required.\\nExcluded costs:\\n- Shore handling.\\n- Shifting of containers from SGTD to DMP or DMP to SGTD port.", "order": 1, "title": "Objective and Scope of the Services"}, {"id": "client-obligations", "body": "Give written/email instructions to the Service Provider for transportation of containers from Galaan Multipurpose Port (GMP) and/or Modjo to Djibouti.\\nSupply a minimum of two (2) 20ft containers (or an equivalent load to fill one flat wagon).\\nSubmit all forwarding service booking requests at least seventy-two (72) hours prior to the scheduled train departure and no later than 7 days before the vessel cut-off time, whichever is applicable.\\nFor clients requiring first-mile service, submit a first-mile service request notice no less than seventy-two (72) hours in advance.\\nComplete and submit accurate export documents as per the request of the Service Provider; payment must be submitted at least 36 hours before train departure.\\nDeliver all cargo to the designated loading port or freight station at least three (3) hours prior to the scheduled train loading time.\\nFailure to meet the stated deadlines may result in cancellation of the booking and transfer arrangements; any resulting delays, penalties, or additional costs shall be the sole responsibility of the Client.\\nIf the Client fails to deliver the container, fails to provide the requested documents for completing export documents as instructed above, or cancels after wagon reservation, the Client shall pay USD 150.00 per wagon as a penalty, after notification.\\nContainers must have four (4) undamaged corners.\\nOne flat wagon must carry either one 40ft container or two 20ft containers.\\nIf two 20ft containers are loaded on one flat wagon, their weight difference must not exceed 10 tons.\\nThe gross weight of the container must not exceed the standard loading capacity indicated on the container; the Client is responsible for ensuring full compliance with the maximum allowable load.\\nProhibited cargo: cargo covered with tarpaulin is not allowed due to safety risks.\\nIf the cargo to be transported is dangerous and/or valuable goods, notify the Service Provider 48 (forty-eight) hours before the wagon booking for further discussion and decision.\\nRefund requests before wagon booking require an official request, with a 3% administrative fee deducted. If the refund is due to the Service Provider, the Client shall receive a full refund. Refunds not requested and completed within six months shall be considered waived.\\nAny delay caused by missing or incorrect documents shall be the Client''s responsibility.\\n100% of the transportation and customs clearance fee must be paid in advance.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Complete gate pass processing and submit to Nagad Station for each shipment within eighteen (18) hours after train departure.\\nThe Service Provider reserves the right to refuse the transportation of any cargo classified as dangerous goods.\\nMaintain cargo liability insurance for railway transport; additional insurance for port handling or last-mile transport shall be the Client''s responsibility.\\nThe Service Provider is not liable for customs penalties or demurrage due to delays beyond its control.\\nNotify the Client immediately, in writing, of any delays, port issues, or customs holds.\\nThe Service Provider shall not be liable for:\\n- Inherent defects of the cargo.\\n- Improper packing or loading conducted by the Client.\\n- Customs-related delays.\\n- Delays caused by force majeure events.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "Neither party shall be liable for delays or non-performance caused by force majeure events beyond their reasonable control.\\nForce majeure shall be interpreted in accordance with the Ethiopian Civil Code.", "order": 4, "title": "Force Majeure"}, {"id": "pricing", "body": "The railway transportation charges and the freight forwarding and customs clearance charges for each corridor (per 40ft container and per two 20ft containers) are set out in the Rate Schedule immediately below, expressed as unit prices per origin → destination lane and per container.\\nWhere the total cargo weight exceeds fifty (50) metric tons per two (2) 20ft containers, an additional charge shall apply for each excess metric ton at the overweight rate set out in the Rate Schedule.\\nFor consolidated containers containing more than one (1) shipping document, the first document shall be included under the agreed contract rate; any additional document within the same container shall be subject to the extra-document charge set out in the Rate Schedule.\\nPayment must be supported by an official receipt before cargo departs from Galaan Multipurpose Port/Modjo.\\nIf the Client uses PIL Shipping Line, any local charge incurred will be covered by the Client as per the invoice issued by the shipping line.\\nIf storage or demurrage occurs due to Client-related issues (delay in document submission, payment delay, or any other Client-related reason), the Client shall pay the corresponding charges; charges apply per day after the free storage period, based on the invoice and SGTD tariff.\\nDuring export season, EDR may provide seasonal export support through the facilitation of empty containers.\\nAdditional costs (if applicable):\\n- First-mile delivery service by truck within Addis Ababa or Modjo and surrounding areas, originating from warehouses or any other places designated by the Client, shall incur an additional cost fully covered by the Client.\\n- For clients utilizing EDR''s first- or last-mile logistics services, the applicable charges shall vary based on the cargo movement route.\\n- The charge for first-mile delivery to Galaan Multipurpose Port and Modjo dry port shall be communicated to the Client by the Service Provider after receiving the necessary details regarding the cargo origin, type, and weight.\\n- For any vessel outbound charges, IMO charges, or other fees not included in the port handling payment, the Service Provider shall request the Client to settle the required amount based on the official receipt issued by the port or the shipping line.\\nPayment terms:\\n- All charges, including rail transport and customs clearance charges, remain 100% payable in advance.\\n- Payments shall be calculated based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date the wagon or train number is provided.\\n- If the exchange rate changes between the payment and the wagon assignment date, adjustments will be made accordingly.\\n- Any additional costs incurred due to customs issues or port delays shall be borne by the Client and paid based on actual costs, supported by official receipts, within 10 days.\\n- Late payment incurs a penalty of 10%.", "order": 5, "title": "Pricing and Payment Terms"}, {"id": "contract-documents", "body": "The following documents shall constitute the contract between the Client and the Service Provider:\\n- Any amendments made to this contract (if applicable).\\n- This Contract Agreement.\\n- Final minutes of negotiation (if applicable).\\nIn the event of any discrepancy between these documents, they shall be interpreted with priority in the order listed above.", "order": 6, "title": "Contract Documents"}, {"id": "documentation", "body": "Consignment Note (cargo handover document).\\nPayment summary prepared as per the agreed tariff, if required.", "order": 7, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider must certify the taking over and handing over of the container cargo on the Freight Carriage Acceptance Sheet of EDR in an appropriate manner and provide it to the Client.\\nThe Freight Carriage Acceptance Sheet shall be prima facie evidence of the receipt of the container cargo by the Service Provider and of the kind, number, and weight of the goods.\\nUpon delivery of the cargo to the Client, the Cargo Handover Out Voucher signed by both parties shall serve as evidence of cargo receipt.", "order": 8, "title": "Consignment Notes"}, {"id": "amendment", "body": "This contract can be amended by mutual agreement.\\nNotwithstanding the above, the Service Provider may revise transport tariffs due to operational and regulatory changes by providing at least five (5) working days'' prior written notice to the Client.", "order": 9, "title": "Amendment"}, {"id": "termination", "body": "The contract may be terminated:\\n- By mutual agreement.\\n- Upon completion of the contract period or agreed cargo shipments.\\n- If either party breaches fundamental terms.\\nIf terminated for cause, the terminating party must issue a 15-day written notice specifying the breach and allow an opportunity to cure, if applicable.", "order": 10, "title": "Termination of Contract"}, {"id": "effectiveness", "body": "The contract is valid once signed by both parties.", "order": 11, "title": "Contract Effectiveness"}, {"id": "duration", "body": "Valid until August 31, {{contractYear}}, with a possible extension upon mutual agreement.", "order": 12, "title": "Contract Period"}, {"id": "disputes", "body": "Disputes shall be settled amicably.\\nIf amicable settlement fails, disputes shall be submitted to the Federal Court located in Addis Ababa.\\nThe signatories confirm that they are fully authorized to sign and execute this Contract Agreement; the power of attorney of the signatories for the parties is enclosed with this contract agreement.", "order": 13, "title": "Settlement of Disputes"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +INSERT INTO freight.contract_templates (id, code, name, description, document_title, whereas_clauses, articles, is_active, created_at, updated_at, deleted_at) VALUES ('cfbe197f-0e08-4af1-8622-8442fda8a170', 'INTERCITY_CONTAINER', 'Container Intercity Contract', 'Domestic (intercity) container transport by railway between Ethiopian terminals — Galaan Multipurpose Port, Modjo dry port, and Dire Dawa — including empty repositioning.', 'Intercity Container Transport Service by Railway', '["The Client has requested and agreed to the transportation of container cargo between the agreed Ethiopian railway terminals (Galaan Multipurpose Port (GMP), Modjo dry port, and Dire Dawa), including the repositioning of empty containers between those terminals, using the Addis Ababa–Djibouti railway line within Ethiopia.", "The Service Provider has agreed to transport the container cargo as per the terms of this contract."]', '[{"id": "objective", "body": "To provide domestic railway transportation services for 40ft and/or 20ft full containers between the agreed Ethiopian terminals (Galaan Multipurpose Port (GMP), Modjo dry port, and Dire Dawa), and the repositioning of empty containers between those terminals.\\nThe scope of the services comprises:\\n- Railway transport service between the agreed origin and destination terminals.\\n- Cargo handling at Galaan Multipurpose Port (GMP).", "order": 1, "title": "Objective and Scope of the Services"}, {"id": "client-obligations", "body": "Give written/email/electronic shipment instructions to the Service Provider for transportation of container cargo between the agreed terminals.\\nPrepare all necessary documents and facilities for shipment.\\nEnsure the minimum supply of containers per shipment agreed with the Service Provider for the selected loading terminal and destination.\\nOne flat wagon must carry either one 40ft container or two 20ft containers.\\nIf two 20ft containers are loaded on one flat wagon, their weight difference must not exceed 10 tons.\\nEnsure timely loading and unloading of cargo.\\nAssign representatives at both ends to oversee container handover and ensure the necessary arrangements for cargo reception at the destination upon arrival.\\nMaintain and provide detailed cargo information (type, weight, destination, etc.).\\nBe responsible for cargo handling, loading, and unloading of both empty and full containers at Modjo and Dire Dawa dry port.\\nBook wagons at least five (5) days in advance.\\nEnsure containers are ready one day before the planned loading date.\\nCollect the full container from Galaan Multipurpose Port (GMP) within three (3) calendar days from the day following the arrival notice.\\nIf the Client fails to collect the container within the specified period, the Service Provider shall have the right to reposition the container to any location it deems appropriate; in such case, the Service Provider shall not be held responsible for any damage or loss arising from such repositioning.\\nIf the Client fails to collect the container from Galaan Multipurpose Port within the specified period, the Client shall be liable to pay demurrage charges of 15 USD per day per 20ft container and 27 USD per day per 40ft container, calculated based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date of payment.\\nIn the event the Client fails to collect the container(s) within the specified period, the Client shall be liable to pay double handling charges of 27 USD per 20ft container per handling or 40 USD per 40ft container per handling.\\nCollect the containers within one day at Dire Dawa and Modjo dry port, or make the necessary payment to the dry port as per the standard of the dry port.\\nOnce empty containers are returned from the Client''s premises and stored at a dry port while awaiting train allocation, any demurrage and/or storage charges incurred from the dry port thereafter shall be the sole responsibility of the Client.\\nProvide clean empty containers that meet the receiving terminal''s standards; additional cleaning costs incurred due to non-compliance will be borne by the Client.\\nEnsure containers are structurally intact and meet weight distribution requirements.\\nProhibited cargo: cargo covered with tarpaulin is not allowed due to safety risks.\\nNotify the Service Provider forty-eight (48) hours in advance before wagon booking if transporting hazardous or valuable goods.\\nIf a booked wagon is not loaded due to Client-related issues, including but not limited to a damaged container, missing lock, unpaid demurrage, port system errors, or incomplete documentation and submission, the Client shall be charged 100% of the total price of the reserved wagon.\\nRefund requests before wagon booking require an official request, with a 3% administrative fee deducted. If the refund is due to the Service Provider, the Client shall receive a full refund. Refunds not requested and completed within six months shall be considered waived.\\nPay 100% of the transportation fee in advance for each train set.\\nSettle additional penalties due to non-compliance within ten (10) days of invoice issuance.\\nLate payment incurs a penalty of an additional 10%.", "order": 2, "title": "Obligations of the Client"}, {"id": "provider-obligations", "body": "Assign the necessary voyage based on the operational schedule and cargo demand, and notify the train schedule 48 hours in advance.\\nProvide a list of wagons/voyage or train number 24 hours in advance and update corrections 12 hours before arrival.\\nProvide safe transportation of the containers.\\nDeliver the cargo within two (2) days after train departure, provided that all required documents are submitted on time and no unforeseen circumstances or events occur.\\nIn the event of export cargo operations, the Service Provider may prioritize the loading of export containers during the loading of empty containers and the unloading of containers from the train at Galaan Multipurpose Port.\\nThe Service Provider reserves the right to refuse the transportation of any cargo classified as dangerous goods.\\nIf any operational, technical, or mechanical problem occurs throughout the transit, notify the Client and the relevant authorities and arrange cargo transfer within four (4) days.\\nProvide accident or defect reports if needed.\\nBuy cargo liability insurance for each wagon.", "order": 3, "title": "Obligations of the Service Provider"}, {"id": "force-majeure", "body": "Neither party shall be liable for delays or non-performance caused by force majeure events beyond their reasonable control.\\nForce majeure shall be interpreted in accordance with the Ethiopian Civil Code.", "order": 4, "title": "Force Majeure"}, {"id": "pricing", "body": "The applicable rate per 40ft container or per two (2) 20ft containers for the agreed route is set out in the Rate Schedule immediately below, expressed as a unit price per origin → destination lane and per container.\\nIf cargo exceeds 40 tons per two 20ft containers of gross weight, additional charges apply proportionally.\\nGross weight shall be the total sum of cargo, packing, and container tare weight.\\nPayment for any additional tonnage shall be made in advance before the container is loaded onto the wagon.\\nThe price of loading and unloading and container handling at Modjo and Dire Dawa dry port is not part of this contract; it is the Client''s responsibility.\\nAdditional costs (if applicable):\\n- Last-mile delivery service by truck from the destination terminal to the Client''s premises shall incur an additional cost, fully covered by the Client.\\n- For clients utilizing EDR''s last-mile logistics services, the applicable charges shall vary based on the cargo movement route and shall be communicated to the Client by the Service Provider after receiving the necessary details regarding the cargo destination, type, and weight.\\nAll payments shall be made one hundred percent (100%) in advance.\\nPayment may be made in Ethiopian Birr based on the Commercial Bank of Ethiopia''s daily selling exchange rate on the date the wagon or train number is provided; if the exchange rate changes between the payment and the wagon assignment date, adjustments will be made accordingly.", "order": 5, "title": "Contract Price and Terms of Payment"}, {"id": "contract-documents", "body": "The following documents constitute this contract:\\n- Amendments (if any).\\n- This Contract Agreement.\\n- Final minutes of negotiation (if any).\\nIf there is any discrepancy between the documents, they shall be interpreted with priority in the order listed above.", "order": 6, "title": "Contract Documents"}, {"id": "documentation", "body": "Equipment Interchange Receipt, Railway Waybill, Container Carriage Acceptance Sheet, and incidental charges (if any).\\nPayment summary as per the agreed contract price (if required).", "order": 7, "title": "Documentation Requirements"}, {"id": "consignment-notes", "body": "The Service Provider must certify the taking over and handing over of the container cargo on the Freight Carriage Acceptance Sheet of EDR in an appropriate manner and provide it to the Client.\\nThe Freight Carriage Acceptance Sheet shall be prima facie evidence of the receipt of the container cargo by the Service Provider and of the kind, number, and weight of the goods.\\nUpon delivery of the cargo to the Client, the Cargo Handover Out Voucher signed by both parties shall serve as evidence of cargo receipt.", "order": 8, "title": "Consignment Notes"}, {"id": "amendment", "body": "This contract can be amended by mutual agreement.\\nNotwithstanding the above, the Service Provider may revise transport tariffs due to operational and regulatory changes by providing at least five (5) working days'' prior written notice to the Client.", "order": 9, "title": "Amendment"}, {"id": "termination", "body": "The contract may be terminated:\\n- By mutual agreement.\\n- Upon completion of the contract period or agreed cargo shipments.\\n- If either party breaches fundamental terms.", "order": 10, "title": "Termination of Contract"}, {"id": "effectiveness", "body": "The contract is valid once signed by both parties.", "order": 11, "title": "Contract Effectiveness"}, {"id": "duration", "body": "Valid until August 31, {{contractYear}}, with a possible extension upon mutual agreement.", "order": 12, "title": "Contract Period"}, {"id": "disputes", "body": "Disputes shall be settled amicably.\\nIf unresolved, disputes shall be taken to the Federal Court in Addis Ababa.", "order": 13, "title": "Settlement of Disputes"}]', true, '2026-08-05 07:31:08.510384+00', '2026-08-05 07:31:08.510384+00', NULL); +`, + dropdown_settings: ` +INSERT INTO freight.dropdown_settings (id, code, label, description, multiple, meta, created_at, updated_at, deleted_at) VALUES ('7d01044c-0f2b-497d-ad1a-781c1b82f2b1', 'general_contract_period', 'General Contract Period (months)', 'How many months a general contract stays open for ordering after activation.', false, NULL, '2026-08-05 07:31:06.89436+00', '2026-08-05 07:31:06.89436+00', NULL); +INSERT INTO freight.dropdown_settings (id, code, label, description, multiple, meta, created_at, updated_at, deleted_at) VALUES ('a24f1cf9-4c8b-4347-9454-417dd425925c', 'contract_validity_periods', 'Contract Validity Periods (days)', 'Validity durations (in days) a staff can choose when accepting a submitted contract.', false, NULL, '2026-08-05 07:31:06.898804+00', '2026-08-05 07:31:06.898804+00', NULL); +INSERT INTO freight.dropdown_settings (id, code, label, description, multiple, meta, created_at, updated_at, deleted_at) VALUES ('52a2969a-d720-4c4b-87f5-e710ca5008f2', 'ro_vessel_min_days', 'RO vessel minimum lead time (days)', 'Minimum days between today and the vessel departure date on an export Release Order.', false, NULL, '2026-08-05 07:31:07.906349+00', '2026-08-05 07:31:07.906349+00', NULL); +INSERT INTO freight.dropdown_settings (id, code, label, description, multiple, meta, created_at, updated_at, deleted_at) VALUES ('e1fb2dfd-f166-4dd7-8b93-339b296c487b', 'import_train_numbers', 'Import train numbers', 'Even IMPORT run numbers (Djibouti → Ethiopia) selectable when building a train. The paired export number is derived automatically (import − 1).', false, '{"clearable": true, "searchable": true}', '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +`, + dropdown_options: ` +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('ec8b7dce-6c1a-4a55-80e6-e455d743747f', '7d01044c-0f2b-497d-ad1a-781c1b82f2b1', '3', '3 months', NULL, false, 0, NULL, '2026-08-05 07:31:06.89436+00', '2026-08-05 07:31:06.89436+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('aedbb6db-51bc-4b0e-98e9-a366afb97fa2', 'a24f1cf9-4c8b-4347-9454-417dd425925c', '180', '6 months', NULL, false, 0, NULL, '2026-08-05 07:31:06.898804+00', '2026-08-05 07:31:06.898804+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('0abee7ce-91c7-4f09-afa9-e33aa8e3de58', 'a24f1cf9-4c8b-4347-9454-417dd425925c', '365', '1 year', NULL, false, 1, NULL, '2026-08-05 07:31:06.898804+00', '2026-08-05 07:31:06.898804+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('2191ce22-9746-47bf-b799-6355bf4e24d6', 'a24f1cf9-4c8b-4347-9454-417dd425925c', '730', '2 years', NULL, false, 2, NULL, '2026-08-05 07:31:06.898804+00', '2026-08-05 07:31:06.898804+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('bff86860-2ce7-43bb-a27a-b1ce1bbf41c8', '52a2969a-d720-4c4b-87f5-e710ca5008f2', '2', '2 days', NULL, false, 0, NULL, '2026-08-05 07:31:07.906349+00', '2026-08-05 07:31:07.906349+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('4e9c93ae-b530-4165-abec-6396b859b07a', '52a2969a-d720-4c4b-87f5-e710ca5008f2', '3', '3 days', NULL, false, 1, NULL, '2026-08-05 07:31:07.906349+00', '2026-08-05 07:31:07.906349+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('c5235b46-77c0-4525-8ff6-3e601575da3e', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8002', '8002', NULL, false, 0, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('7801c5fb-d32b-45ca-bf5b-2d5cf6151d85', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8102', '8102', NULL, false, 1, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('efc5f757-fe78-48a6-885f-e15240b204af', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8202', '8202', NULL, false, 2, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('a20cfe92-ebe3-4a61-9cdc-0da9026a54a2', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8302', '8302', NULL, false, 3, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('cb9e619e-8170-423e-80b4-b9a93812f197', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8402', '8402', NULL, false, 4, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('041fba86-4b7a-4d92-b2ad-fe1a36d74a5e', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8502', '8502', NULL, false, 5, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('a2c8bbd2-d538-4cb1-a68f-fa8e6a65fdda', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8602', '8602', NULL, false, 6, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('aac4f7ce-2cd1-4ae3-9f3f-3d88c29101c1', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8702', '8702', NULL, false, 7, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('832e6846-52ab-4945-83cf-9b085007f267', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8802', '8802', NULL, false, 8, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('fe334532-c484-4ba3-be02-bea841824b3e', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '8902', '8902', NULL, false, 9, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +INSERT INTO freight.dropdown_options (id, setting_id, value, label, note, is_disabled, display_order, meta, created_at, updated_at, deleted_at) VALUES ('252f6260-2940-4cc6-a758-348a27f75ac0', 'e1fb2dfd-f166-4dd7-8b93-339b296c487b', '9002', '9002', NULL, false, 10, NULL, '2026-08-05 07:31:09.035777+00', '2026-08-05 07:31:09.035777+00', NULL); +`, + exchange_settings: ` +INSERT INTO freight.exchange_settings (id, fallback_rate, fallback_source, last_synced_at, updated_by_id, created_at, updated_at, deleted_at) VALUES ('d095c92e-01c4-4ad7-bfe0-782124a9c4b6', 162.416500, 'AUTO', NULL, NULL, '2026-08-05 07:31:09.389185+00', '2026-08-05 07:31:09.389185+00', NULL); +`, + priority_configs: ` +INSERT INTO freight.priority_configs (id, type, label, currency, min_wagon_count, max_wagon_count, score_points, is_active, display_order, created_at, updated_at, deleted_at) VALUES ('56b3b5b4-e6f0-435c-b275-fe03cc6e100a', 'CUSTOMS', 'With customs 1–10 wagons', NULL, 1, 10, 7, true, 1, '2026-08-05 07:31:08.406818+00', '2026-08-05 07:31:08.406818+00', NULL); +INSERT INTO freight.priority_configs (id, type, label, currency, min_wagon_count, max_wagon_count, score_points, is_active, display_order, created_at, updated_at, deleted_at) VALUES ('79c1a2b1-e98f-4142-9093-56003d354f25', 'CUSTOMS', 'With customs 11–53 wagons', NULL, 11, 53, 15, true, 2, '2026-08-05 07:31:08.406818+00', '2026-08-05 07:31:08.406818+00', NULL); +`, + service_types: ` +INSERT INTO freight.service_types (id, service_name, description, can_be_booked_alone, includes_first_mile, includes_last_mile, includes_customs, is_active, display_order, created_at, updated_at, deleted_at, code) VALUES ('c5913b29-579c-4d09-a701-c9e9f20c9770', 'Rail Transport Only', NULL, true, false, false, false, true, 1, '2026-08-05 07:31:04.600266+00', '2026-08-05 07:31:04.600266+00', NULL, 'RAIL'); +`, + train_scheduling_global_rules: ` +INSERT INTO freight.train_scheduling_global_rules (id, max_train_length_meters, max_train_weight_tons, max_wagons_per_train, max_20ft_container_weight_tons, max_20ft_pair_weight_diff_tons, created_at, updated_at, deleted_at, import_window_lead_days, export_booking_lead_hours, window_open_hour, window_duration_hours, doc_review_minutes, payment_window_minutes, window_close_hour, import_close_offset_minutes, export_close_offset_minutes, export_payment_window_minutes) VALUES ('bf410357-b2ce-4c29-bb8e-1e1d7d8806a7', 760.00, 3500.000, 53, 30.000, 10.000, '2026-08-05 07:31:05.884714+00', '2026-08-05 07:31:05.884714+00', NULL, 3, 24, 8, 3.0000, 30, 60, 17, NULL, NULL, 60); +`, + truck_types: ` +INSERT INTO freight.truck_types (id, code, name, capacity_tons, has_trailer, description, is_active, created_at, updated_at, deleted_at) VALUES ('f38f5b28-96a3-4e23-b238-2770a33da95a', 'TRUCK', 'Truck', NULL, true, NULL, true, '2026-08-05 07:31:09.136545+00', '2026-08-05 07:31:09.136545+00', NULL); +INSERT INTO freight.truck_types (id, code, name, capacity_tons, has_trailer, description, is_active, created_at, updated_at, deleted_at) VALUES ('5fad6cd6-3b75-4ec8-b6cf-475d7943662d', 'TRAILER', 'Trailer', NULL, true, NULL, true, '2026-08-05 07:31:09.136545+00', '2026-08-05 07:31:09.136545+00', NULL); +INSERT INTO freight.truck_types (id, code, name, capacity_tons, has_trailer, description, is_active, created_at, updated_at, deleted_at) VALUES ('4e78fe5f-3c17-403d-9872-5c210f314d13', 'TANKER', 'Tanker', NULL, true, NULL, true, '2026-08-05 07:31:09.136545+00', '2026-08-05 07:31:09.136545+00', NULL); +INSERT INTO freight.truck_types (id, code, name, capacity_tons, has_trailer, description, is_active, created_at, updated_at, deleted_at) VALUES ('c45f7bd3-0807-4243-a215-59d79d81bc31', 'FLATBED', 'Flatbed', NULL, true, NULL, true, '2026-08-05 07:31:09.136545+00', '2026-08-05 07:31:09.136545+00', NULL); +INSERT INTO freight.truck_types (id, code, name, capacity_tons, has_trailer, description, is_active, created_at, updated_at, deleted_at) VALUES ('00ca6ed9-068c-4ef5-88e3-ad50796ae2a2', 'VAN', 'Van', NULL, false, NULL, true, '2026-08-05 07:31:09.136545+00', '2026-08-05 07:31:09.136545+00', NULL); +INSERT INTO freight.truck_types (id, code, name, capacity_tons, has_trailer, description, is_active, created_at, updated_at, deleted_at) VALUES ('2a9b9d14-490b-4575-b6d6-c2a3bd5f03c7', 'CAR', 'Car', NULL, false, NULL, true, '2026-08-05 07:31:09.136545+00', '2026-08-05 07:31:09.136545+00', NULL); +INSERT INTO freight.truck_types (id, code, name, capacity_tons, has_trailer, description, is_active, created_at, updated_at, deleted_at) VALUES ('27d7481e-6545-4bb5-b72e-b39839e56ac3', 'BUS', 'Bus', NULL, false, NULL, true, '2026-08-05 07:31:09.136545+00', '2026-08-05 07:31:09.136545+00', NULL); +INSERT INTO freight.truck_types (id, code, name, capacity_tons, has_trailer, description, is_active, created_at, updated_at, deleted_at) VALUES ('773c5365-1288-4999-b8b1-67d88f380e08', 'CASONI', 'Casoni (rigid, no trailer)', NULL, false, NULL, true, '2026-08-05 07:31:09.136545+00', '2026-08-05 07:31:09.136545+00', NULL); +`, + wagon_types: ` +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('2bd636b7-3951-4164-a2c6-fe364a8110c0', 'NW7', 'Double deck sedan wagon', 22.000, 26.066, '{vehicles,sedan}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, NULL, 37.100, false, NULL); +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('f7886975-ba6f-422d-8de4-e672c8482699', 'NW5', 'Flat wagon', 70.000, 13.966, '{container,steel,machinery}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, 1.300, 22.400, true, 30.480); +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('ba981b9b-972e-4301-a857-aafbb35612e0', 'PW2', 'Box wagon', 70.000, 17.066, '{"general cargo","break bulk"}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, 1.600, 25.200, false, NULL); +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('9fe9a541-6245-4d25-b17d-68aa5f74dcf5', 'GW2', 'Tank wagon', 70.000, 12.228, '{liquid,fuel}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, NULL, 23.000, false, NULL); +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('7b5d91ee-d36c-4383-a27b-3b65179d568d', 'CW4', 'Gondola covered wagon', 70.000, 13.976, '{"covered bulk cargo"}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, 1.300, 24.800, false, NULL); +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', 'CW3', 'Gondola open wagon', 70.000, 13.976, '{"open bulk cargo"}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, 1.300, 23.400, false, NULL); +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('20cd13b8-6992-4f9d-86ba-da6cfbbf2527', 'KW2', 'Hopper covered wagon', 69.000, 16.466, '{"bulk grains"}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, 1.500, 25.200, false, NULL); +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('02f1d786-e4c5-49a8-9e88-8d09f3e1a903', 'KW3', 'Hopper wagon open', 70.000, 14.400, '{coal,"bulk cargo"}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, NULL, 24.000, false, NULL); +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', 'NW6', 'Flat wagon (long)', 70.000, 18.560, '{"long cargo"}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, NULL, 25.300, false, NULL); +INSERT INTO freight.wagon_types (id, code, name, capacity_tons, length_meters, supported_load_types, is_active, created_at, updated_at, deleted_at, equated_length_m, tare_weight_tons, supports_container, max_container_gross_t) VALUES ('efcee536-852b-4532-a6c6-bd2dd4f5e10e', 'BW1', 'Refrigerated wagon', 38.000, 21.996, '{"refrigerated cargo"}', true, '2026-08-05 07:31:05.293615+00', '2026-08-05 07:31:06.108196+00', NULL, NULL, 32.100, false, NULL); +`, + yards: ` +INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at, deleted_at, has_facility) VALUES ('ef184178-12ee-4657-a13b-aabce861912f', 'KALITY', 'Kality Rail Terminal', 'Ethiopia', true, 1, '2026-08-05 07:31:04.600266+00', '2026-08-05 07:31:04.600266+00', NULL, false); +INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at, deleted_at, has_facility) VALUES ('4aa63253-57a2-4ab1-a783-09eb501a08a9', 'MOJO', 'Mojo Dry Port', 'Ethiopia', true, 2, '2026-08-05 07:31:04.600266+00', '2026-08-05 07:31:04.600266+00', NULL, false); +INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at, deleted_at, has_facility) VALUES ('b2c4fc10-2a24-4898-82f9-0a81f2783c7a', 'DIRE_DAWA', 'Dire Dawa Yard', 'Ethiopia', true, 3, '2026-08-05 07:31:04.600266+00', '2026-08-05 07:31:04.600266+00', NULL, false); +INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at, deleted_at, has_facility) VALUES ('d99d1607-e0c1-4af2-9f21-a76b2ad493ac', 'DJIB_PORT', 'Djibouti Port Terminal', 'Djibouti', true, 4, '2026-08-05 07:31:04.600266+00', '2026-08-05 07:31:04.600266+00', NULL, false); +INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at, deleted_at, has_facility) VALUES ('d8aa7ef7-7ec6-44bf-8342-88aec811ac6a', 'NAGAD', 'Nagad Terminal, Djibouti', 'Djibouti', true, 5, '2026-08-05 07:31:04.600266+00', '2026-08-05 07:31:04.600266+00', NULL, false); +INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at, deleted_at, has_facility) VALUES ('f07a71e1-0631-4779-b160-42f01060ca29', 'LEGACY_ORIGIN', 'Legacy Origin', 'Ethiopia', true, 99, '2026-08-05 07:31:04.600266+00', '2026-08-05 07:31:04.600266+00', NULL, false); +INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at, deleted_at, has_facility) VALUES ('f6962f85-305e-4e90-9226-f6cc191cb8f7', 'LEGACY_DEST', 'Legacy Destination', 'Ethiopia', true, 100, '2026-08-05 07:31:04.600266+00', '2026-08-05 07:31:04.600266+00', NULL, false); +INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at, deleted_at, has_facility) VALUES ('bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', 'DORALEH', 'Doraleh', 'Djibouti', true, 12, '2026-08-05 07:31:08.870506+00', '2026-08-05 07:31:08.870506+00', NULL, false); +`, + wagons: ` +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('efceade8-da1c-4990-b3a4-af59e74a4de5', 'ER0001', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f087a26c-ac84-453c-8bb9-e54a8928bcc4', 'ER0002', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('04ec17f9-b396-433b-bee3-a82de0c16bed', 'ER0003', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c7c605fd-d6d3-437f-b93c-a646751b5f0f', 'ER0004', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('feda7bce-0c97-4af4-bebc-19e4273c4828', 'ER0005', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5b9065d8-566f-4f0e-a584-4ff1115f64aa', 'ER0220', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('843b1eb5-5eb9-493f-a3e3-5b88cedb6128', 'ER0222', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d21f91c9-ebe2-4dcb-95d8-0bcaed76ab34', 'ER0223', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('092a99b6-1b56-426e-82a1-e446d64a0408', 'ER0226', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a462dbdd-59bc-46bb-bb64-094aab773a62', 'ER0228', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('65b77a30-cb40-489c-b7fe-158c4b0aab94', 'ER0230', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('db500cb6-f7dd-46cb-a732-2cc0b9fa4501', 'ER0235', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1353b30e-117f-4449-b00a-8d45adcf0ab4', 'ER0237', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a67a8541-9258-4049-97a5-5708790c6dba', 'ER0238', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('46d60f75-eafe-45ca-a0a7-3fb819272d9d', 'ER0239', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a4231843-d2ed-45c3-a0e5-f9adb226302f', 'ER0240', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5a54156e-3c4c-4f63-86a1-fc26f0cf71b0', 'ER0241', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c17def2a-9c74-4800-a9c7-ef6170bc0e8d', 'ER0242', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('75dfb8ba-ebc3-41bc-a937-97868d2d6c3f', 'ER0244', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6117b8b2-f7b3-416b-8ec6-a832b610ee5c', 'ER0245', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7231acc8-c44a-4578-973a-793902f23f8f', 'ER0246', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6665c422-1ca2-4c69-9893-cb46fe86cb3b', 'ER0247', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('95fa646f-cebb-4160-adec-86c21675d765', 'ER0248', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d4581d3f-18e8-4bf0-9a76-799a309db932', 'ER0249', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6597927a-f846-437c-9652-18893dc269c0', 'ER0252', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ae81a93e-10f1-40c6-b3b9-965155532157', 'ER0253', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4af8534c-f937-478a-9618-512271dc3f3b', 'ER0255', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2ea47018-c73f-48e6-b2fa-967ef5fd58b2', 'ER0256', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('93d6f746-8843-4907-bc2e-d9afbfc60517', 'ER0258', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('09d84453-9408-423e-b4c3-94687b7e13cb', 'ER0260', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('200e08b2-fa95-4e42-b25e-dff0b00fd73a', 'ER0261', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d2da6f6d-c094-4a50-b168-fa2bc7dc6497', 'ER0265', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fe9b0f33-d994-4872-9109-355a04b046d4', 'ER0267', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('898578dd-67b6-4fd6-8fbf-fd1c00dce2d4', 'ER0269', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5e680d5b-73cf-4b53-9ba5-c8115d6597c8', 'ER0295', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b6195ff9-a4ff-4908-a76f-7b37ebd58e27', 'ER0296', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('25ab7769-d72c-4ab0-b131-5d32b76a7652', 'ER0297', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4f3aa099-255f-4826-ac67-842c13db0c5a', 'ER0299', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9370378b-4610-4b81-8279-6c28bc4eb2fd', 'ER0301', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('81a7e9fd-82d6-4ff3-b57c-f8227d2e042b', 'ER0302', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('938cab4e-f51c-42c9-94e0-9ebd7ae71500', 'ER0303', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('108b055b-5308-4363-8709-c4d1baa1cf50', 'ER0304', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('01f0e001-4b3b-4b79-96b8-dc7d2d3cc974', 'ER0305', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('47f81b2c-be78-41e8-872d-9b2560e43a1a', 'ER0308', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('191f88c9-64d7-4ff3-9bb1-159b6cd3993c', 'ER0309', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('971ab325-5bc5-4dc9-b1e3-f244b40f3772', 'ER0311', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('197ca87b-1234-4422-a8a3-1b8d3e25f768', 'ER0312', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('16882d5a-4a81-4d37-93b0-e8b092b5efbc', 'ER0370', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('687aca49-626b-4221-a85f-fb4ef261c60f', 'ER0371', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cb807103-d185-4b18-9dd6-e3c4652220f1', 'ER0372', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3ba691b3-59d5-41dc-8372-77bfa58df017', 'ER0373', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e0fee328-7f50-41f0-aed2-2983fae71a0f', 'ER0374', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6efc8d31-f013-4c8e-9d80-8ecb118feb67', 'ER0375', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ead00684-c1ff-4436-9ff4-5a1c6c226b9d', 'ER0376', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fe38f46c-5435-4264-8645-91c3787be639', 'ER0377', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d4274ca2-a77f-42b9-b46b-2600d434b1da', 'ER0378', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ab0f4ce0-ff11-48db-a460-453b367dd0da', 'ER0379', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('daa1e05c-5160-4a4c-87f5-386b1ae8f6e5', 'ER0380', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f2a9e2cd-5f50-4b43-8838-40dfd44e8577', 'ER0381', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4798ff1b-0b5b-4e85-a1b7-ded6467e7678', 'ER0382', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('47d7370e-1f52-4096-bcba-78e83331fc9b', 'ER0383', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('778a0b73-e81d-4c7a-a3ec-f2f1e720deed', 'ER0384', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a47a9170-810b-41c7-a62d-5033df3af1fb', 'ER0385', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d0a2a138-be52-4546-ac1c-76b85e7b8af8', 'ER0386', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9917a998-e75f-4a2c-85ca-4221c286bbc7', 'ER0387', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('731e07f9-ab01-4b06-9f8f-28db7815e1a8', 'ER0388', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d5333661-a46d-4568-a183-2535fcf28c5f', 'ER0389', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6cd0288f-08d1-433f-85e4-6014b9927586', 'ER0390', '02f1d786-e4c5-49a8-9e88-8d09f3e1a903', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fe9002ef-4073-4624-ab2a-1638b117caf0', 'ER0401', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('35925577-66ab-4cb3-86e9-66a6cfaeab5b', 'ER0426', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d67a08fb-e3f3-4d0c-aafd-bae17640133b', 'ER0427', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1a15874a-4776-4729-87ab-927ccbaede70', 'ER0438', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8ec6681f-6d82-4f95-84ac-f8c4cc85e440', 'ER0439', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1828d437-9d34-4141-8d8b-9b381bbac55a', 'ER0457', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8dbea1a0-8c04-4387-9431-f031c00411b3', 'ER0472', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('65089613-a418-44b6-835c-15cd66e0c299', 'ER0482', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('77286c81-bd2a-4a45-abef-0134249269c0', 'ER0493', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3f34e798-2a95-438d-ab82-e31d460cd885', 'ER0495', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a8791b74-a567-4003-9858-319dc53adc19', 'ER0509', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('386d99ff-459b-492f-8b78-59cda4227b4a', 'ER0510', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('99a6eb73-d7b2-4aae-891e-30738143d99f', 'ER0523', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('eca6f4fb-3096-4e11-b444-ccd3df4b8b89', 'ER0532', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8566f603-592c-457c-83a0-2370074d1a40', 'ER0534', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c6b5e39b-106f-4241-ab38-29d7a62a6db8', 'ER0535', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0ff382b7-952d-46f6-a630-040daec1af24', 'ER0543', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c767da8a-638f-43db-9065-e33d558b4504', 'ER0549', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c210de55-b995-4ce1-bb55-fbb8d71f82d7', 'ER0579', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7ed8b8b8-74fe-4ac1-a7c3-41cf0fe75282', 'ER0581', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9f896f23-0527-4a86-8400-d2881409c721', 'ER0586', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('96d0767c-050e-40ae-8aff-7b66a0b717c8', 'ER0595', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('118a28cd-3309-45b7-b5fc-78dc8b6018e3', 'ER0604', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f7120939-932d-4de5-9212-b4590d163eb6', 'ER0609', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2fe4bf7f-4d09-4997-a894-5e2404f90207', 'ER0611', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('58622396-b1e0-420a-a369-72e1e21465fe', 'ER0632', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4c6746fc-2a3e-45af-b5f4-25ba4026757d', 'ER0639', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('68842cde-2cc7-408b-94ee-2723c0e3ba51', 'ER0657', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('65c7159d-d0c4-4134-9d8f-65164f91aaf6', 'ER0687', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5e8d0e19-08b9-4149-afed-87989cc63587', 'ER0688', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fdfdcdc5-524b-49a1-af38-2eaf3aac0bd1', 'ER0690', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('66c38cf9-26f9-4d53-96df-e7368ca3b29a', 'ER0696', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8c62e85c-dec3-4966-8815-b941822766c9', 'ER0701', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bd6a8ed1-a7ac-4bcd-8189-4866d6e41812', 'ER0708', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bb66f97c-eb52-441b-ac54-b86f4107da0e', 'ER0709', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('46ed9baf-1a6b-49ee-a318-37662e5a46ab', 'ER0710', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9319c6b6-df6b-4731-9bc5-17a0d15e29aa', 'ER0719', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('24e52947-9665-4ab1-99ed-2e6a2c21285e', 'ER0721', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b0f4aee1-3b2a-4bc2-8d00-a8a7f3e1c104', 'ER0722', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1ca7aca7-bad6-40fd-b935-2b22d6953f56', 'ER0727', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8349164a-3138-4791-a7bd-6608115072fa', 'ER0731', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a84261d8-3a7c-4ea8-b4e7-5644f00a1485', 'ER0742', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('603c83b4-f8fa-4886-8ae2-b451a974146f', 'ER0745', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0946cfd4-6b82-4f74-8af7-eaf94f60ff93', 'ER0750', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('07ed91db-0975-40c8-92b5-fdcf0dcdc187', 'ER0757', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0ff14a93-438f-4fc9-a30a-cdfbc418240d', 'ER0773', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c3424af3-0228-4933-9824-a4dd334c7b6e', 'ER0779', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b6d617bf-13b9-47ea-9651-a0194cd33ade', 'ER0783', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1ffe576b-1548-48cb-b95b-95fdfd8e3fca', 'ER0792', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('80e0df88-e536-4212-bbad-59422a3a7baf', 'ER0793', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3d25dd34-92a9-41c2-9bec-6e57ade1f91c', 'ER0813', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('209cede5-60c9-4518-bd34-a7a14c73fe92', 'ER0817', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('115be47f-b66d-4754-9cea-f548ae13669d', 'ER0828', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('dc7f0316-22e8-42ae-a973-0fafb2963947', 'ER0829', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5cd52b73-f604-47aa-bc0b-7f1c0b346de6', 'ER0834', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7191777c-7e93-43ee-801d-0f03ee8d93d6', 'ER0848', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8de34884-3cbe-44e3-87e2-82920e9eda66', 'ER0856', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fd677c07-3cfb-4725-bef4-5a6bb51aa965', 'ER0860', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1552469c-786a-441a-a9a5-2b6c8835526e', 'ER0862', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('421cedae-de56-455a-b657-cb372a02a716', 'ER0867', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f8dd2362-8cc2-4801-aa3c-9a74fcb60785', 'ER0869', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4b99149a-1d00-4c78-827f-1f8cb1da56c2', 'ER0871', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bb326a6f-8fac-4d0f-9018-ee2b70d1b6ba', 'ER0875', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c3038cee-05b2-4749-b643-4bf1fd1468d2', 'ER0892', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a67f0c02-e4a7-40ac-8391-f591d8ad4c14', 'ER0931', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ab052975-5b46-41ca-b0a9-7129e5547e14', 'ER0932', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e69db4d2-72f5-40d1-88e6-ff49f980b11f', 'ER0938', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('16899bf0-1de4-4e2d-8f3f-698ba0c8c166', 'ER0941', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('09426b78-7102-4e62-b9cd-87c856e91c76', 'ER0942', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('73949863-70ec-413f-8d7c-d36f823b893e', 'ER0943', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b3125715-ac8a-4991-9008-22b138ea9347', 'ER0944', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('13801ab0-008c-4703-b0f6-352549395ec8', 'ER0945', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3b684e22-05bc-47ae-9de7-a215d9665928', 'ER0946', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('481de8fb-4a6d-4342-adff-41a80ff40486', 'ER0947', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f70ef069-e6ab-4228-9de0-28ed2cfe347c', 'ER0948', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d0db2798-fcfa-455d-90ba-9127eacd4948', 'ER0949', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2af890c0-5d36-47f1-a8db-f7daaec14d16', 'ER0950', 'efcee536-852b-4532-a6c6-bd2dd4f5e10e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('600984af-0ca5-4698-9bfc-1f79429ec45d', 'ER0951', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f17ae313-86d7-4144-abd3-9c9aa598d095', 'ER0952', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5ec4629f-958f-4f8b-9c3c-6ca4f855f237', 'ER0953', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2e8d4acc-2da4-4b51-ad26-4037d36e01be', 'ER0954', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bf29c94a-1d74-48a4-a018-2c8874ba2b0c', 'ER0955', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c0bed9de-b81b-428f-9ae2-f3b8b78c7423', 'ER0956', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('40e20038-34e3-47b9-b848-22c43cf33f05', 'ER0957', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8d6157d2-5e15-4798-a349-3c74317e86b1', 'ER0958', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e80818c0-cf2d-4bd5-9f9a-79c048ac6934', 'ER0959', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d4cbefd6-7b32-4784-aa57-7194baaacd9a', 'ER0960', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('255bc431-8eef-4480-afaf-19f0f85a5250', 'ER0961', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3db764c7-ec43-4e3b-adc4-800d1bc6fd8c', 'ER0962', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('85f4d239-7dba-42aa-8059-4d40f48ac325', 'ER0963', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('19abb8bf-30dd-4004-abde-d9029246a623', 'ER0964', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0e334a57-668f-42e6-a6ed-25a0dd10f55b', 'ER0965', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ba362c78-2a8c-47ae-85bf-83daa770d357', 'ER0966', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2d235929-bb5b-4204-aba3-aa4413115043', 'ER0967', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d24e0168-688a-4f8c-9e24-6068c23664be', 'ER0968', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3cc124d3-1aff-4ca3-bf24-ad0d62cb881d', 'ER0969', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4a1db9e4-0fd0-458a-bfe7-8b450b85d654', 'ER0006', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('29533a21-fb3a-4399-8e90-7261b625a802', 'ER0007', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('13030dac-eb8d-41e6-a4a6-dbd5ca6f2575', 'ER0008', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9387846e-7952-435f-977e-a4d19760788f', 'ER0009', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f09fc4a0-9110-49e5-be90-f5f12d2cd1f7', 'ER0010', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('93941159-85af-439f-a8ca-03c616b5ae82', 'ER0011', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3e7574c3-2010-4b2e-b7dc-7e5e46be672f', 'ER0012', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b3893de5-096d-45d5-90b3-4d21b2eaff52', 'ER0013', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('82ebe4f6-1dd9-4b1e-bce2-53737bd39046', 'ER0014', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e23ddf96-690a-4d9a-b5cb-e677f1258c0f', 'ER0015', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cf4bea71-b8f3-49ae-8aa6-99759f83c96b', 'ER0016', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3b773879-0019-46cd-9d88-a86c0e783920', 'ER0017', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f32caed5-22e4-4ad7-b7bf-e4d2c7b58452', 'ER0018', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('03d4dd21-30c1-43ad-8ee6-9e80f76cbe5f', 'ER0019', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bcb6661a-7e2d-4619-9d35-60b1f87390b5', 'ER0020', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('36699e6f-c80d-42c6-a8e2-34416bafac4b', 'ER0021', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('45cb0daf-5635-4e8e-81bc-eb57a8ee31f9', 'ER0022', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9908a1e4-4ac9-4339-bf15-d718c0e4c1bf', 'ER0023', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c6ea8c9a-f7d9-4b3b-9c19-3efe66a99c5b', 'ER0024', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('082b3472-b360-4b6e-9eba-70bef8272791', 'ER0025', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('61cad62b-368d-4937-828b-09ba9d8875fa', 'ER0026', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0583627c-94f5-440b-b7d4-2e24ca9762ed', 'ER0027', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a4d31ece-005e-4ec5-87c0-0e0de97ca7ad', 'ER0028', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('11144213-f8a5-45d2-8d9c-2c1bc978a82e', 'ER0029', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('31b55af2-5b11-4952-9d07-078104ebeaac', 'ER0030', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('dc8fb0cf-1270-4784-8784-c85e4736e2b1', 'ER0031', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('25a9a57f-2338-4618-96fa-a1e56e442de7', 'ER0032', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a0942973-f0e8-4302-bffa-a821f23b4d54', 'ER0033', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('92f2c3e3-6a28-4933-9dca-a43e60116fe7', 'ER0034', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5a515a3c-73ca-4745-9e30-c5d962e373a1', 'ER0035', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4666e2db-f7c1-426a-a904-43052a876596', 'ER0036', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fd040b14-77ac-4772-88df-0a27dcfa6576', 'ER0037', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e7af13a2-2035-413b-8fe4-eab77193bdba', 'ER0038', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c144db12-9310-40f1-a876-3137fa1a8fa4', 'ER0039', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5523adee-261e-4a23-8e12-ebceb1009861', 'ER0040', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bd4d177c-99a3-4a4e-ab0d-bb4e64fe7aa2', 'ER0041', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9789b41a-4fec-45a6-a921-253fbc0f5e00', 'ER0042', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1d2f7268-2806-4ed3-8c9b-88b86a08e3bb', 'ER0043', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fbce9f69-8122-458e-915f-92a7196a74f4', 'ER0044', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ca40e343-c2b0-4d42-b1b2-d878dacee532', 'ER0045', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7838d70c-6780-449f-890d-43e016e86330', 'ER0046', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5eaae9e6-4038-4c7c-a54f-ea71290b45ae', 'ER0047', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8096a852-e1e5-49cd-8780-ef290fbff727', 'ER0048', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bd0c82dd-59a9-4d7b-8b39-c1a832cc9a53', 'ER0049', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('91644b7d-11fd-477e-b6cc-e558b29efacd', 'ER0050', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4f65b024-d98c-4c31-bcca-eadcf9c7a253', 'ER0051', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e28a8739-35b5-42b0-89e6-f52feef60f58', 'ER0052', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e9bdc4b9-8cc3-42bc-b9ac-41b1b14fe713', 'ER0053', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f9dd65c4-1b9c-4ee1-beff-328a8ba75b43', 'ER0054', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7ee755a2-297d-4a52-84fb-88bc55dd79f1', 'ER0055', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f79bd22d-3a3d-4bb7-932b-8c650fd0b7e5', 'ER0056', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('34ddabc2-a98d-4798-94e2-8c5cefb44ad8', 'ER0057', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cf0bbe40-b70b-4e41-8c70-2cba03059661', 'ER0058', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('25335eba-c84a-4f33-b42f-8b8edf5cad1a', 'ER0059', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ca1821bd-03fc-40b9-a25b-cdbfd065746b', 'ER0060', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9edffff5-691f-4d4b-b2ab-ced742d944a8', 'ER0061', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8918bfdd-4a54-4ea0-b62b-adf786c0012a', 'ER0062', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('de5d64ac-980e-4ec4-9535-d6f3e308b97f', 'ER0063', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('609e75f9-82ce-447d-a729-087117659383', 'ER0064', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('66bd4bf8-4d80-45ce-9d28-bd822e1e4c20', 'ER0065', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('01ab0fb9-f2da-4a65-b74e-2ffb70df7b22', 'ER0066', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c687f152-0680-4ed1-b650-e3591ceafa37', 'ER0067', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a3ca1605-05ab-43eb-b4c5-8b45b0ba5ed1', 'ER0068', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f2f1f255-8406-423a-a127-6172ed515759', 'ER0069', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('01018755-ee54-4937-8093-e8c9f08a0531', 'ER0070', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('031c8d2c-f82f-49fd-b253-30938749038b', 'ER0071', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6ff0dbdc-950e-4e8f-9c27-79c959212cff', 'ER0072', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6c278e04-c0e2-4ea5-817a-7bf51f28575d', 'ER0073', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c70b4928-7639-4498-9930-9ec2df6e8239', 'ER0074', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cd3c9e82-d861-4ba2-947b-32f7ad6ea84a', 'ER0075', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cc2ed6a7-6af2-4398-ab2a-4469bcfc8d09', 'ER0076', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cfeeb680-1fd7-4395-aab8-ee09ab40ef55', 'ER0077', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ea042b6f-a767-4dbf-be81-bbd3f5b504cb', 'ER0078', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('15c2cb66-21b3-4ad3-8984-5dc601cea9f8', 'ER0079', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1b9e5ec1-07a9-4e45-8bb6-9802dcb5f838', 'ER0080', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('094ea7d1-13dd-4447-a439-4013fc190d08', 'ER0081', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a0dadba3-50f5-4e84-ad0b-9da188f473c7', 'ER0082', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2e22c46d-b770-4670-9ee5-52a6bf0f6ec9', 'ER0083', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('813acf62-dbae-4c3e-b455-e5013a672412', 'ER0084', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e3adfe38-d991-4e99-a57d-61008b0a128d', 'ER0085', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7cf5cc8d-c448-4470-8c3f-8ba058305786', 'ER0086', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('89e28e6a-dd14-4c54-ae1d-ae14b5ab4909', 'ER0087', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('21defb90-af54-4f15-8162-5917edb28d0b', 'ER0088', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c6a31dc6-461e-4f56-b297-410898a9d868', 'ER0089', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('312a751d-13cc-4af9-abf8-ca23fa48ce12', 'ER0090', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('30fcd00a-1791-4099-842c-e78080cd115f', 'ER0091', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('33f3802c-157f-41b2-988b-54f059921d85', 'ER0092', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a8d7cfaf-245f-417e-b3d7-dee3083d6fdf', 'ER0093', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0f95499c-3880-4446-8f76-8a58ab906138', 'ER0094', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b4e883d4-0c74-4d0b-8ad3-2f5cf8f52ee0', 'ER0095', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('11975e34-284a-46c5-802d-9d35ce5e2712', 'ER0096', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('28d7a0f5-d2f1-443e-9f07-916923b28cd3', 'ER0097', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b2177901-e5fa-4b20-9d02-a0a2d42c4b44', 'ER0098', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3948f1ba-c5c3-40a8-94b1-41c7dd5330d7', 'ER0099', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('05e32a38-e115-4cd5-83b4-737e98d80116', 'ER0100', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7c85b7a1-1478-4820-bfa5-2846d05d45c7', 'ER0101', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1f62d884-427b-4e35-8637-4dc9e88aed50', 'ER0102', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('63e51c94-6188-4475-bfba-7e093b6d832d', 'ER0103', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c6e3254e-10aa-4546-8172-0570353819ad', 'ER0104', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d10a5010-ae63-4b06-bc1c-36b52a4270cd', 'ER0105', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d0fed052-881a-4bcd-91c6-306816c9a4b4', 'ER0106', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('de4d4d24-9f34-435a-bbf5-cf66cfb74443', 'ER0107', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('804f33fe-39ae-4475-be4f-b154252df8bc', 'ER0108', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b652f660-b5a7-457b-9a34-c029e07f672b', 'ER0109', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('58db2ca5-4dab-4adf-94ed-ca9ffc9bf117', 'ER0110', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ef921e85-659e-4c07-b1fc-f8f9ef2f414f', 'ER0111', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('542ff206-1f14-4ee2-ac6b-59217ac5e951', 'ER0112', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8b9e06a6-6f4f-4a89-babf-d8e7d3e603e5', 'ER0113', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c5e6278d-ab0c-4fa9-a1a9-97ab895d200b', 'ER0114', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('46f0bf81-8df9-47a3-ab8a-b2992418453c', 'ER0115', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c9f80b53-60de-4627-8772-11735499baee', 'ER0116', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('800e7511-9ca4-4505-aad1-8a60179aa459', 'ER0117', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('88e0ebac-f05a-476a-bf06-2898256fa000', 'ER0118', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ccd4ef12-70d4-4681-8bc9-6063f27e7698', 'ER0119', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bbeb1539-e049-4032-b2ff-6351157021a4', 'ER0120', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('30ab10e8-ff91-4bac-a399-abeaa0ba9b49', 'ER0121', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9a618481-ad8a-4a8c-9c8f-e5e9493ce5e1', 'ER0122', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('086774b9-56fe-4b49-ab45-adc881ce417d', 'ER0123', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d967cb96-3b8c-4b57-b83f-b6cc7b31d46d', 'ER0124', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ee872a5e-a6d7-40dc-a572-7f96deeea321', 'ER0125', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f2a2e0ef-5434-455c-8322-1ee061187cd6', 'ER0126', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('38d9618e-e138-4fe0-9d6d-1963939a16d0', 'ER0127', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cc18fa46-8ce3-4472-b20f-7fbe61f72a3e', 'ER0128', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b4d756f8-4d7c-4249-8320-abd23e353cb6', 'ER0129', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8a308a64-aea5-4d57-827f-dfa576140bf3', 'ER0130', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a461b3c1-177f-4cfd-b8cc-8703221c2259', 'ER0131', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8c1aecbb-aced-49da-a3f1-91fe5117c6de', 'ER0132', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('88dd1f41-c224-4e7f-9b65-d5acc4262dab', 'ER0133', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b17533fc-80df-4008-8d8e-6bcda46c02e1', 'ER0134', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b5a832e3-e6ab-4c6b-8e91-72be3bab307e', 'ER0135', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c151db62-50af-425e-9ab7-0b8972c24bb3', 'ER0136', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('910c3b2e-d66c-4533-98fd-814af79bd36b', 'ER0137', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a337a147-9520-4b4c-8bb1-01de893e0a05', 'ER0138', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7f5245dd-026d-451a-9102-ab7ecd30aa2c', 'ER0139', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2314b085-8e6f-4d5a-a59e-84a9c92c603b', 'ER0140', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fd96d263-86b2-43bc-97c6-de54a595ab87', 'ER0141', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('248ba5bf-6a62-4958-8877-3c44932510aa', 'ER0142', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a65d0ec3-a660-4eb5-8109-cb5940fca939', 'ER0143', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('def6c5b3-1d71-4519-bd43-b5ce477e9b31', 'ER0144', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('309cc736-1e8d-4d43-a241-c77a772acbc8', 'ER0145', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e32c1c41-3f8b-48e4-9b66-bb912af456c4', 'ER0146', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('51a39d5d-e07c-4b5b-95aa-07c61707973a', 'ER0147', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9d024616-3e25-46cf-ad39-b6d2ca5e7667', 'ER0148', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f95a9219-de6e-4186-9d53-6d7013581049', 'ER0149', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('340f1b07-d7d9-47c6-897c-aa32f7509ee2', 'ER0150', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('475e774c-1c1b-464e-acc5-ad08936eed59', 'ER0151', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3a0298cc-5299-4028-ae21-8fc0d9ba0def', 'ER0152', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8be287d9-3a80-4c83-9e07-984ea146238c', 'ER0153', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('07ec6bb9-a41f-43ac-9ca5-9d436e042138', 'ER0154', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5970e954-dade-4c7f-8437-29848382465b', 'ER0155', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ca009387-08b8-4a97-8654-32faf0475ef5', 'ER0156', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6690e811-c449-4857-b1ca-4bce7a3548e2', 'ER0157', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('80bbb133-9997-4edc-a4cd-9f2b0b0a07ee', 'ER0158', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('985844aa-9c51-4aad-a47b-a09234c12ef2', 'ER0159', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('23659776-def6-400f-a8e2-77a29bf73239', 'ER0160', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('40c2e6cd-5f26-4589-8aea-4202c40b15c1', 'ER0161', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9afd795d-b0a1-4645-9759-65a5e90e05d8', 'ER0162', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('93fe5fdb-266d-4679-9c40-92667b385a2d', 'ER0163', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('278b60d1-6561-4459-a403-34b431673cf0', 'ER0164', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('923e947a-1a17-4c3a-8014-53d139706913', 'ER0165', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('33de1811-8dfd-4dcb-ac94-b1c64529d7ea', 'ER0166', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8195958b-6a19-4560-ac06-e2abe1282f4d', 'ER0167', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e8481e2f-ff52-41db-af7d-b1cde9aad4e0', 'ER0168', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('610eef4d-d58f-4f69-abfb-5cad2c9e6d04', 'ER0169', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('95724265-7a53-4ecd-84bc-e2ae8055c495', 'ER0170', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ada828a9-83eb-46f0-99a2-0983db19ca90', 'ER0171', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6f2defbd-468d-4308-9bf0-25c1a6da0b48', 'ER0172', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e43ab168-18f0-4b19-8210-806a4230ce4c', 'ER0173', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0fce64c5-84d4-4ea3-a26d-62051c741c27', 'ER0174', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f1b83abb-1037-4a4f-a041-69e199c7900d', 'ER0175', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('abb65568-fe17-4c6f-b6f5-d144a7d45f9a', 'ER0176', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f0f3c747-3abe-48b3-8669-1ed643eb47d7', 'ER0177', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e18d852a-ced4-45d9-82f6-9d252fdffe1d', 'ER0178', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1e3d9c1a-b18e-4862-a7ed-8b673dcafa88', 'ER0179', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('54454191-ecba-454f-aa15-8534f6ea51c4', 'ER0180', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('52193246-3094-499a-baca-34b459c6867a', 'ER0181', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ff2cfd0d-bbdc-42ce-a794-988cd053f8f6', 'ER0182', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5cfc39cb-a35c-4b42-9e90-2b8739bf690e', 'ER0183', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1d27e565-7595-4701-9b7d-4acd51c243a4', 'ER0184', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8aa4b74a-9383-4c5e-adb3-6fe1db136314', 'ER0185', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d41987d0-66a0-4334-8101-761a7edbd9ab', 'ER0186', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1b8e0441-ebaa-4dd6-9701-4bd136114176', 'ER0187', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6371df15-04cd-4817-b57a-bb113704c475', 'ER0188', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cad85ec2-2316-468b-83ef-898d5a9aa403', 'ER0189', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6aaf21f4-18ac-48dd-ade8-dbd2f2f2325b', 'ER0190', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('55a50468-d591-4ddb-ab9d-9d502f1c5a19', 'ER0191', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('82628805-484c-43fd-b8a2-2efe1ad00cf4', 'ER0192', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('744f36d7-3507-414d-8374-f5c6a12cc609', 'ER0193', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b81d459c-8ab4-4b83-b47b-4c6a4337ab0b', 'ER0194', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1513a30f-cc55-46b2-be05-a7d87f3e2c44', 'ER0195', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3c503d76-15ae-4ae2-9e71-6b69f38ab082', 'ER0196', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('32d0830b-041f-4611-98ec-4180b039fe4e', 'ER0197', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('746f5f66-8ffb-4006-83eb-c5603375eb2d', 'ER0198', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('723a5f58-874b-401f-b1a5-9fb9575795d8', 'ER0199', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9d16b293-e86c-4a30-8228-647b5052cf72', 'ER0200', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8755cdd1-4ea2-4814-b468-6d1c8a1d8a8e', 'ER0201', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5438749e-2554-4931-b97f-90ae2c12d148', 'ER0202', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6fd9cf36-62e7-4581-86e6-b90227a1becd', 'ER0203', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('87e86711-64e1-4f85-bb75-a83e399d5c81', 'ER0204', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a97ac69f-41cf-48b8-9f8b-8198bb446bb1', 'ER0205', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f0a515b9-1fa4-4bd7-9bbe-b8684c93f095', 'ER0206', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6c09c3a9-c313-4bb5-86e4-856bd4634ee0', 'ER0207', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1cf55023-a30a-4406-ae6b-cd7a332ca2aa', 'ER0208', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a6a42d43-187d-45cb-8101-0e3feb1f7152', 'ER0209', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('63345e78-5040-44f0-87a8-800b5bff5bb9', 'ER0210', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0a571466-4138-4461-a2be-cb4924d6c8ac', 'ER0211', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9a8a587e-265d-4a48-a0d4-347ac08334a2', 'ER0212', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4cc2aef5-d99d-4066-ab44-e0c856731acd', 'ER0213', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e999cd2b-ebe5-45b9-b65f-fca6b084161a', 'ER0214', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c835a849-95cb-42dd-93bd-cc7a4d37cb7f', 'ER0215', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c0a794be-9afd-44de-a79c-935531514283', 'ER0216', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4f547c30-5333-44d1-8f12-cbc1242714d6', 'ER0217', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('31a051ff-024f-425d-a09a-d2c971f8f9d9', 'ER0218', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ac332250-3ecc-4bf9-aedf-27f7e9757618', 'ER0219', 'ba981b9b-972e-4301-a857-aafbb35612e0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e8bc8fec-d681-464e-98c8-1b942a1824e9', 'ER0272', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('86b8e939-55cc-4fdf-8ef6-4ef2d8ba6eb7', 'ER0273', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0ab894e4-92aa-4e4d-aec6-0fc7af24dda1', 'ER0277', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d2c57e15-2a37-43ef-afc9-6cfb28525042', 'ER0278', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c706702f-8701-4074-a199-95700962c124', 'ER0279', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8ff793a3-276c-4c2e-9117-57fc69f31c6f', 'ER0280', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b9444e2a-8f50-4981-8d46-e3a35d8c631b', 'ER0281', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4b051af8-9ef5-412d-874d-35923c9015c4', 'ER0283', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6dafaff2-f848-42d4-bded-2c2c9111e705', 'ER0284', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('39070353-0374-4147-8ed2-4ee2533e67d8', 'ER0286', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b59ce268-2eb9-465f-87e3-bc152c926838', 'ER0288', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('66aabe5b-f187-42de-ba5e-1bb4b74f9961', 'ER0290', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('65da8564-6072-4fc3-8bd1-9b931c67ff3b', 'ER0317', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a4389a81-89ff-4134-83e7-34b57acbac70', 'ER0318', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('38d1fb88-cc3c-4492-a947-019687516be2', 'ER0319', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d391172c-8b85-4e6c-87f3-0332b6fdeba1', 'ER0320', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cbeb9882-0b1d-4593-ad44-6816ceedc2a8', 'ER0321', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8123eeb6-418b-429f-9012-7e7c39714b7c', 'ER0323', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('16221585-3a0b-4ef9-9b89-d4e71a330b80', 'ER0324', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('665691e8-8462-49f3-b6e8-1504f2791ac9', 'ER0325', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c0662921-fd6b-4491-8846-2113190425f2', 'ER0326', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e29a542a-bd26-4731-8685-3c5d817e913c', 'ER0328', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d34d8447-67a1-4987-a9f2-4cc491d4ed0f', 'ER0329', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b0d4bb17-074a-4652-99ba-9db75ad837cf', 'ER0330', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('837acb7a-d6ac-49ba-8459-9ffef83156c5', 'ER0331', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('94c5abb8-8eb8-4bfd-9f4f-74fe432f4647', 'ER0332', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e9c37ca6-d031-451e-a0e5-fa1356836cd1', 'ER0334', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d81d8984-3817-4460-b4f9-1ab485e16fe7', 'ER0335', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('487de4ad-002b-4035-866d-2dee3f02aadb', 'ER0336', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('712714fd-8575-40ff-aaff-39dd576c945c', 'ER0338', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('aa534cb5-f9bd-45a2-8d06-2f428d191a73', 'ER0339', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('78da6b5d-ae44-4860-9bad-1f5cfe7eca05', 'ER0340', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('53a9df1a-959f-462d-88bd-6c9848f8cd02', 'ER0341', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0e0faa9d-2005-484c-95d6-ccaa3eb7d403', 'ER0342', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7752733b-c6f7-4924-a056-94135dc92f95', 'ER0343', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b396bf14-5faa-4eaa-94f5-fa5b2b2342da', 'ER0345', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('51f55228-bb29-4e8a-8537-f3528870b5a0', 'ER0346', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('34a26c46-11ef-41ee-af28-4dbf26161258', 'ER0347', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ac0e6790-937d-42b1-92ac-eae81b5431a1', 'ER0350', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b1f665f5-091f-4c73-b23a-de9e447abd8e', 'ER0351', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8b25754a-5e1c-421b-aa44-ca0e9746a371', 'ER0352', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a9d81346-6a5c-4977-a13b-775869af5c7f', 'ER0353', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bc2a9f22-8803-42eb-a5ff-2ed0c5582b67', 'ER0354', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('060253d2-961d-40a5-8e83-46051f533f90', 'ER0355', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3366ae38-a3f9-4b0f-a676-9122f4803ab5', 'ER0356', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9497b230-5583-4e12-893b-b37011da18fb', 'ER0357', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3e8d7ce1-235a-4258-bf53-7944c249fe28', 'ER0358', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4b014804-373f-44ee-b06a-29de5f885ecc', 'ER0359', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1c830dcf-b53f-411a-9cfd-64f1baf54c84', 'ER0360', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cde6b81a-b795-4c0f-aa54-2ad591ec1208', 'ER0361', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a0360b8e-a650-4ae7-ae1b-9621f838c2d8', 'ER0362', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2517d347-3a1c-4e12-9093-0b77143800af', 'ER0363', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5c306923-7bcf-4f6c-be5a-fdbc9078da68', 'ER0364', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b7eb0612-f02e-4fc6-ac72-c6ae3333f6ec', 'ER0365', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9578e319-9794-412f-8ece-ccf6b445de61', 'ER0366', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fe13dbbc-15b3-4c20-914f-f1df1bd72ad4', 'ER0367', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f6af7816-e6b1-42c8-a2e8-c418d70fd2fc', 'ER0368', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('87a785d0-0713-4796-8f1a-ae5c8f7c2a35', 'ER0369', '20cd13b8-6992-4f9d-86ba-da6cfbbf2527', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0feed691-4a62-40fc-b091-161ba3e668e3', 'ER0970', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e8d6879e-2cae-4444-bd3b-db95d20414db', 'ER0971', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('159e1d22-4d07-4fcd-ac92-d24a3ffd709a', 'ER0972', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('295698c1-f249-4499-bb12-ac103e689e50', 'ER0973', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d9a00691-0f4f-4fff-86f2-afa9c13c98c2', 'ER0974', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3f519548-1779-4bde-8af6-d62286db616d', 'ER0975', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a1c7f44e-d6bc-403f-b75d-b46177e13a4e', 'ER0976', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1f8f3a2b-76ed-4d72-a6cc-2742ea2b2498', 'ER0977', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8a989154-3b29-454a-b08f-62d7ab0d1e97', 'ER0978', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1ec340e3-57f8-463c-92f1-6de40944fc6f', 'ER0979', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('68328ea0-4164-45ea-8dce-fd3c3b2213ae', 'ER0980', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5720c652-431b-4399-9fdd-6fb55dfe1658', 'ER0981', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6e88adfc-75b5-45fc-b6dc-d3cee0122233', 'ER0982', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('30642847-f45b-4648-b7c6-4e43236e5af7', 'ER0983', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('45585c0e-9de1-4ba1-8e3d-6997bff8c715', 'ER0984', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('90da9164-d2cf-4622-8b09-343d8428d8d7', 'ER0985', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8a248a3a-c44d-4c1a-9b0b-e35b077a68b8', 'ER0986', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('95c37ce3-030b-4c08-b6ee-cc3dbde3c728', 'ER0987', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('590f361c-7538-44fa-85ba-0541920de462', 'ER0988', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7ced7ebb-3a38-48f1-bc99-444179b860d1', 'ER0989', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0a963d36-88e3-47c9-8da7-b9bc05d55d1c', 'ER0990', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e2f146b6-53c1-4437-a540-dd5cf4ee21d5', 'ER0991', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a0f61e5a-d883-422e-8a59-3558e1288e22', 'ER0992', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('19ee51d4-213b-45b0-bfd2-ec48e5e00f64', 'ER0993', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3b4fe7b8-5f30-4942-add4-5e58d852ce42', 'ER0994', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cf955d1b-ba00-4b54-a6ff-cacb9c9e3998', 'ER0995', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('383c798b-ef3f-491c-9be8-e06e7cf02ced', 'ER0996', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f90db5da-8236-488d-9f84-d3ad2d70d13c', 'ER0997', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2342fc04-768b-42c1-85d0-1d7a11179eae', 'ER0998', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('083b1a08-4724-4bc1-8f42-56d2d903d9b7', 'ER0999', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8c812b23-a593-4e5b-90f0-29ec9d3f1e19', 'ER1000', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6474629e-c5e3-41ac-83ec-0973f1878584', 'ER1001', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d7bad221-aec2-4110-9efe-565978e9f78b', 'ER1002', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fc6a80cf-6513-4b5c-8a32-43fefe3a6a5a', 'ER1003', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3c1cc41e-2302-4640-9e9e-b79b724d6ee5', 'ER1004', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b8329eca-41b7-4662-a6ef-41e5169ec6c4', 'ER1005', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4a0c027a-1072-4b2a-83da-ecb60ed48567', 'ER1006', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('be475678-d27c-480b-b889-e7c553ef0442', 'ER1007', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('95ba8ebd-731d-417b-982d-5dd783d7133b', 'ER1008', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9cbe79e6-a9a5-4039-a2e4-c5ef9473a1d4', 'ER1009', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('479dbd14-a02d-4ff4-8668-a09b3af575cf', 'ER1010', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('086e4cbf-622b-41da-bae7-8edd920a2881', 'ER1011', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4c43ecbe-b196-43ff-9ecb-0b978bcbdbe6', 'ER1012', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d178148e-d5ec-479f-aa53-03886d48ad50', 'ER1013', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('40d8a4a0-e00f-4cc3-b588-6161d2b1d539', 'ER1014', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('03be9a2c-89eb-40af-a74c-f8bb177c5f5c', 'ER1015', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1ec0e58c-bdeb-4d08-8fcc-2eed5816ee29', 'ER1016', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d408d9bc-2bcc-4190-8c37-1a3e83dd5dd2', 'ER1017', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a2fb2e41-0fad-4e49-9108-9ac0f288e24e', 'ER1018', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e37b5c10-ad2c-4b2d-aec7-6c856bb1f747', 'ER1019', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9ef7a85b-a56f-4907-b0c3-cf4309aca558', 'ER1020', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a7c46698-ce09-45b5-b5ec-f259cf2dca6e', 'ER1021', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('48b2b8cb-b569-4ca5-8ad5-eb83a3864144', 'ER1022', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bba757fb-5728-4708-95cb-adeeafaec710', 'ER1023', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('72ec5405-e0e7-4aed-a83a-c8e015db6c05', 'ER1024', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('814e959b-bc9a-44b7-8483-2a721fbcf944', 'ER1025', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f56cf74f-fb88-46d1-a59e-1a314fb78d33', 'ER1026', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f2cdfa8f-0af2-4552-b3cb-5a7bbb78ffe3', 'ER1027', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('08d97108-17cd-4247-8e9b-17060d8ba4a3', 'ER1028', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('32ae0beb-28ee-4d92-bff0-1f24a99c62f1', 'ER1029', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9ea08a04-cc2a-44ea-af9a-51331362af18', 'ER1030', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('85c4e40f-8071-4cfd-aaac-c679d8ee814b', 'ER1031', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4ebe9f3e-0cf7-4f14-89eb-85f1e9f470f8', 'ER1032', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4c5caf92-0311-49bd-a060-fb349576c23b', 'ER1033', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4e51d3e0-105d-47c9-b2e5-4a3aa84650ef', 'ER1034', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0ec15e58-10f9-4572-83d3-c788df83547e', 'ER1035', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9806782f-f1fa-4647-a790-1d9ce0cf8a63', 'ER1036', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('39371efc-0626-44df-b454-756c419e9f5d', 'ER1037', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9e882be3-d28a-4b12-81e7-5c1146b70ec3', 'ER1038', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('130f09e7-7920-4967-ac97-d0df1be9a1eb', 'ER1039', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f2f7bc2b-2c64-4593-91f1-077172020795', 'ER1040', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('66c749b2-7b09-4137-95a7-d41474a14de0', 'ER1041', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4865fd4c-a040-4b3b-b645-5d7ae487d5a8', 'ER1042', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cbffde1f-e1fb-42a3-a32f-fe79e70a1641', 'ER1043', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('366d14a9-8597-4d41-b40f-0e82dccda6a8', 'ER1044', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4d24cbf6-269e-4755-89b8-29a94755c10d', 'ER1045', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3b544a10-39a6-4ea6-a06a-e109b24d3bb1', 'ER1046', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('65ab09ec-94b4-4402-95a2-2f1d94ff7dd4', 'ER1047', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4847746f-1a7e-445f-8e8f-2445a856b578', 'ER1048', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1912a39f-ebac-4fb4-85c9-1e77058e85d6', 'ER1049', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f7bd3e9b-ae78-4597-81d6-0701364bc009', 'ER1050', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5101c290-de1d-4c7e-bd44-19e333aa986f', 'ER1051', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('76054978-14c8-4285-abf1-eabdc245f5de', 'ER1052', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d29369e1-c755-4fbc-8e01-c50292d0fd5e', 'ER1053', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('aedce45f-d0ae-4804-ae9c-765cb31b3729', 'ER1054', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('72a392a4-5565-43f7-9f31-2044a363cf8c', 'ER1055', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7237eb49-874b-406a-84e5-004a3c32cd1e', 'ER1056', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a08c9c0a-471f-4693-a87e-2d2c91655dbe', 'ER1057', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('150cd759-d37f-448b-8309-f6721b0f9e40', 'ER1058', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8dffaaa5-eb56-4ca0-8daa-debcd900c322', 'ER1059', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('386d05cf-47e0-4929-8fb8-8a02ee952d06', 'ER1060', '9fe9a541-6245-4d25-b17d-68aa5f74dcf5', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6c587df1-b353-4789-bacd-b2cdc4078246', 'ER1061', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('106f99a2-e09b-4b3f-98c6-526225df61ad', 'ER1062', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f36bf423-ddf5-4959-8ade-d1e842dd875c', 'ER1063', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d145bfa6-0064-4289-bca0-98e40ee7d740', 'ER1064', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('de928155-d804-4eb7-b49a-dc94fc54c0db', 'ER1065', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7b8cb7ac-d503-4c81-923a-f3a4ac662227', 'ER1066', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f859677d-ab86-4bf1-9245-07c88f53cd4b', 'ER1067', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4375456b-8894-47e6-b136-02f163831257', 'ER1068', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3eaff31a-37e5-4efc-a21b-2a3cc288b46e', 'ER1069', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cc105d80-3d71-401e-bbb3-de9983e74818', 'ER1070', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('30dadc09-5617-499a-848c-7ad2d6cfb1a4', 'ER1071', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2457be1f-81b9-45f7-9f45-23ca36af7c78', 'ER1072', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('83afc832-d20b-47ec-b51b-8999f1a92c5f', 'ER1073', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d4e1b1b6-e8a5-46ce-b447-04419bf6ba79', 'ER1074', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ef070b5c-ce80-4a10-aeda-4d680ad9fab0', 'ER1075', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7e1aa936-93be-43db-a059-99145ba96c69', 'ER1076', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('51b724f3-f0ea-4efc-8e53-106e8f67d5ac', 'ER1077', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f33b909a-c281-4f3e-a19c-e6f84fae1139', 'ER1078', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9be259e4-cf2f-4a50-bb5f-f505f72ad946', 'ER1079', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('820e9387-f3c9-4f87-b167-d8fd64e7979f', 'ER1080', '8ba35b55-6d19-46cb-bbbd-2fcfe852d84e', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2c972cf2-7b8a-4216-acbe-709b62f368f6', 'ER1081', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('99a89b8d-95c6-4725-96ea-f47fc4e8b40c', 'ER1082', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('201f6b9d-ef86-4b87-ac0d-3798b15390b1', 'ER1083', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7036a580-0138-47de-a000-fe80784c3a35', 'ER1084', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d379d2f1-3d65-4409-9b47-0a68e576ad97', 'ER1085', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7a663d25-db3e-4f69-ab94-6eb1122feb7d', 'ER1086', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('692ae9b0-fbab-4ccc-aeb8-902a18e32423', 'ER1087', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('88ecc5ec-b4bd-4220-b40e-5c25e7dbd50e', 'ER1088', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b760a536-efd5-4176-80a5-e2a6f92126d7', 'ER1089', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('81bb821a-e92e-4ee4-b0e5-adba3f58f614', 'ER1090', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7732c5b6-9321-4937-99b7-35431b5c1c34', 'ER1091', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('07bd556a-1c27-41aa-9722-c0be3ce9c3d4', 'ER1092', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7e863102-685a-4547-a7a4-016ff4a98062', 'ER1093', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('32b11f91-b747-4a28-ad30-a5ce0369756f', 'ER1094', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('140a3608-29b9-4d7b-89b2-9d08f28539a0', 'ER1095', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7fbabf73-f280-4c5c-89de-297a0c71f3b5', 'ER1096', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('60ebf823-cf72-4575-ab4d-e219b8cc80a6', 'ER1097', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6978580d-f063-4d62-a4a2-238932bd8b4c', 'ER1098', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('931e2ae4-da9c-49ae-ad94-86c28e805849', 'ER1099', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b272cf07-aedf-4e42-b1f0-84b33fdcd9cc', 'ER1100', '2bd636b7-3951-4164-a2c6-fe364a8110c0', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', NULL, NULL); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a00b16a1-0add-4e38-8770-67ffd0d5b314', 'ER0410', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('288c8d53-b005-4186-9f87-d75f19139a45', 'ER0419', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('58ac3cdf-ca51-484a-8aeb-a179409a0d93', 'ER0424', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4a49aed9-b583-414d-8209-ccdc691b1f12', 'ER0432', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b18a5ec1-f656-4a4a-a832-7f57899d9f3d', 'ER0440', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c5399d04-cce5-4353-a6e5-f4e7d26caf93', 'ER0447', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('418d9cc2-e3d3-404f-983d-712c98ddf8fa', 'ER0462', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1f73c17d-adf5-4b1d-ac1e-6ae952cad378', 'ER0474', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6569eff1-2138-45e1-abb4-81d898a3dc9e', 'ER0479', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e03ea1a1-44d6-4747-81ed-fab4cf79d92e', 'ER0519', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ad97b2f9-58dc-481e-a460-ffb30a0c2459', 'ER0539', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b7ee6958-3d5a-4d36-a467-7897bd0191dc', 'ER0541', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9ae9a44e-46b6-42e0-8d11-71a828aa3967', 'ER0544', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2d3c9b7e-f45f-40a0-8042-bcffea8a7e1d', 'ER0547', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('869082ba-0e67-4c45-a640-ef37710e51c1', 'ER0557', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c112a83b-4587-43ca-bcc9-5d5c15e32fb9', 'ER0635', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a05b4402-a684-4f33-bbf3-c355af91eaa0', 'ER0650', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4fe86268-f921-41f0-91c9-77b0fc775b72', 'ER0656', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('15592854-9bd6-4c4b-a7cb-5fecea482582', 'ER0660', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('432bc697-a5b9-4ea7-a56d-d1b5a72a81e6', 'ER0663', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bf7d3345-ec27-4817-8471-1cb277a591bf', 'ER0666', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cc19bbd2-276b-4a03-82ba-2861bad8b900', 'ER0674', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0d460fa1-6e3d-41a0-980e-e5aca2d49f72', 'ER0692', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c7762095-f936-4868-9abb-c7a22d1875e5', 'ER0694', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3ffc9641-ba02-4a3b-a1b0-0261c9c02dab', 'ER0695', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fda4805b-e7ea-4c55-8bf0-1b497522fe66', 'ER0712', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e806d025-8200-445c-a04c-ebadd110fb68', 'ER0724', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3dd1c767-0888-4e33-8de5-4fde93eaf215', 'ER0734', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0f98dff8-4da9-4e7e-917e-96bc513349bd', 'ER0744', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1969803e-c560-42a4-8a79-9630e606b486', 'ER0764', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('63d90c62-9c77-488a-8b38-0aba525dc597', 'ER0782', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('da04f0af-eae6-4c1f-86c3-54f1420abd73', 'ER0784', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('668b23c5-4ff2-4121-85a0-9cb9c96947f6', 'ER0786', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('783d0c68-9578-4ec3-a641-3363032981c6', 'ER0790', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('760586d2-591b-4c53-82cb-3e66372aeed8', 'ER0791', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f8eabf31-bee1-43fc-894a-526cff563abc', 'ER0816', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5c9f8c1f-4860-4fdc-8af0-1386503cc305', 'ER0820', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('dbf7b1ad-92ed-4998-9e45-8ad2fcc804db', 'ER0825', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('afdcd17b-d99a-4b9c-a60d-c1c00451056a', 'ER0826', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('76d50921-6e07-4f20-b7df-de29e63ef213', 'ER0835', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4eb2c46f-8a94-4a42-9ec3-a9ea329f97e1', 'ER0840', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b35794d7-5209-4380-a531-30b693427b4e', 'ER0850', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7b0b9582-8118-410c-81fb-b149e0e2989e', 'ER0858', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('faf7e51c-9875-4485-b674-5c97957c6bc4', 'ER0868', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fab8463a-db8f-4f18-8ecf-545aa87d9a82', 'ER0879', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5d6785d0-171b-4b37-a81e-37b72e5dada1', 'ER0885', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5a008a04-244c-4f9e-9a11-65a662ccc3ab', 'ER0901', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('eb9919f6-f02e-44cd-bd1f-297bec13006b', 'ER0905', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('447d46e7-9dfc-4e6c-b210-1de9a36c2e2e', 'ER0915', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e94da3bc-fe8a-4c04-bbcc-20501dad02dd', 'ER0926', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8001', '8002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f5d2e0d3-e3c5-4300-811d-e6f96305d60a', 'ER0254', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4dc4194f-7d05-4e07-bcef-307af0fc7ff0', 'ER0316', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('765b0b88-febb-4756-b155-4c7fd425fd53', 'ER0407', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2c214896-bafd-41dd-b6a5-5fc84d99aecf', 'ER0412', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8b3bea30-2cc3-46bf-b53d-e02d0c6ae65f', 'ER0422', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1a8b5c01-9501-4edc-940a-bcd3706fd149', 'ER0431', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d23b1444-c833-4ebe-acb1-823e04578407', 'ER0435', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('97622066-43bd-4106-8d05-03bfd9997fe5', 'ER0458', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2969bc56-7c35-4dcd-9044-1894b57976a4', 'ER0459', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5644fbf8-f2d2-4336-8981-6f94b82c3ea8', 'ER0470', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('994c7d5d-f499-4b44-9ccc-19b67b9027e9', 'ER0480', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d47a8d1d-a79d-48ea-8c17-3a5ff5316e13', 'ER0515', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c08deeeb-114c-43d4-bc3a-c3be61b16ffa', 'ER0520', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d9e1d1b0-2f9e-4736-b810-a4796254837a', 'ER0521', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b7b993f9-612b-4fec-a8b3-3bcbe1eb5837', 'ER0527', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1f1703fd-665f-429c-a19a-6bde1e5b38db', 'ER0555', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('816707ad-7609-406c-9d7f-39653b101c11', 'ER0559', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f40d7ea6-845e-4c26-8608-fa36f9416ab4', 'ER0567', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e57bbc23-ada2-486a-9efb-b8f88ca48dc3', 'ER0590', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('02f4e7ca-b092-47b4-a8db-8d826586a63f', 'ER0600', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a1367c49-5e53-4ff7-ae02-4f8ac2b9c1ce', 'ER0633', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cb7185f2-4986-46ce-ba36-8145ae13ea2e', 'ER0646', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f3d5451d-0d12-4a1b-8d15-c7f6e7c138d4', 'ER0648', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('74f0950a-cd54-4acc-8cab-8ba87850a1da', 'ER0654', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a35503ca-c3c6-4409-8777-39ac5cdb6b43', 'ER0676', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('94d4bf5d-f1c5-4fe1-8117-8cb43cc73d16', 'ER0697', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('806b59a1-6a6d-456b-8772-430a013827d6', 'ER0703', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('36bc1a03-5e33-4458-9099-30ba59c1d4cc', 'ER0723', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8aba3f55-01e8-4ed1-b014-e8caf469b069', 'ER0725', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d28263db-28ea-4213-bd33-92f5175dd3fa', 'ER0746', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2adf25c3-1201-402c-8abb-c41940ba7514', 'ER0747', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('37c13aab-bcd1-4157-a988-a1a7077be3b3', 'ER0752', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e2952143-9393-4ced-bf98-41e978cc0440', 'ER0760', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('12db1bd2-e648-40e2-a46c-7c1914322387', 'ER0768', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7cc630aa-f6b4-4920-b5a0-ca7c37921f2b', 'ER0800', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e2c34803-fd89-4e86-acc3-5eb91411453d', 'ER0821', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('407b4791-6324-4dee-a78a-b9f8a4e94770', 'ER0838', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('57fabbc9-2b37-4a50-9cca-53852bbbe968', 'ER0843', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bc88fbdc-091c-491f-8e34-7c84fd63edaa', 'ER0844', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2b19b391-86a4-4bee-a716-005f4024305f', 'ER0846', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cf7ce05e-b961-45e9-98b9-10455a2a2c6b', 'ER0852', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7ee1f666-e0b2-453e-acab-c1ebe3a40c8e', 'ER0863', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('90f42343-6adc-49d9-b4e1-37435fecb868', 'ER0880', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('79c1321a-be7c-4172-9e5e-7940f21074ba', 'ER0914', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('07b4eb3a-2689-4f2d-82d7-1bc70bd78fd5', 'ER0920', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ce293480-3b75-4108-af37-b7858edf5444', 'ER0923', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('72ffb226-f3d3-441e-b641-d1fc3f80aec8', 'ER0925', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b4c1d0e9-9efb-4a23-8f52-f25668882b59', 'ER0937', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f19ff037-0b1e-4251-9cf7-603bd9a2371a', 'ER0939', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8101', '8102'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6cae069f-d7df-41bb-bf6f-978be2c808bd', 'ER0231', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c752e889-1cf4-46ae-a26c-7ffbe9e5332f', 'ER0236', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d8e6a374-2048-487d-a486-3cb18a6ae3ba', 'ER0274', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0b9512ff-b624-4f00-a1f4-e122e02284b9', 'ER0314', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e2118b86-7075-4a02-baa2-6a8cad268128', 'ER0322', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('401cc1ed-5efe-478a-a08c-feae6daae963', 'ER0348', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3c5300fc-cbfa-4f63-ab29-9fd1eb62a7b4', 'ER0411', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('33145d41-d959-498a-b0c2-65bd52104b1f', 'ER0430', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('78dcd996-d03a-4490-a4b0-e2fca285acaf', 'ER0443', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('55420e4c-17b2-4eb4-af1d-f4cb10d95ab5', 'ER0445', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5663f527-a6b8-4056-a6dc-738188bfa5bb', 'ER0452', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a83ac6fa-dfa9-455c-9a17-9c448a8fc5e2', 'ER0456', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2da33db0-c125-4c7e-930b-071a34dca4d7', 'ER0464', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('def2913a-15f3-4e30-82ec-b32d96334025', 'ER0491', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2a2189df-a64d-4684-92de-c4d080c23493', 'ER0505', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b6ac56dc-60c8-4091-bc6a-40a218d7e89a', 'ER0514', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('77cad373-50a1-4df1-acc1-a74185853608', 'ER0524', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8c0e7c53-75eb-42fa-a1f4-0928b5b9ce25', 'ER0538', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('37529518-e9f5-4f76-8600-661e0d58ad0f', 'ER0542', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f6e0b649-7408-4592-a331-5cafc9a607c1', 'ER0564', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cfa63df3-ce43-4260-89bc-c96919d332eb', 'ER0568', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6149ff5d-3c69-43b5-adbf-052f0fa0cd2e', 'ER0571', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f0cd30ad-3d33-49fc-bb0d-413033bcedb3', 'ER0585', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f8f2336b-78fc-4131-87da-ccf88c1589a8', 'ER0588', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3bb5610b-caf0-4a45-817c-816adbaaa9c4', 'ER0596', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bd16e101-f663-4de2-9032-c93b5e7c6d4c', 'ER0606', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2fdf729b-1315-4f31-88b1-424184966b2c', 'ER0618', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('76489cef-51d6-4b07-ab3d-b551258e6a03', 'ER0624', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3201d1bf-f605-42e7-8cdf-650b8c7a929b', 'ER0625', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a242f221-ea4f-4766-b5cc-c341d3aea4c1', 'ER0642', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1ffcd901-40f0-485b-94d2-975eeb203c09', 'ER0662', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('76919693-d257-4379-bace-4d2df2f0df95', 'ER0684', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('af411f62-7475-483c-a313-4ef65e6cd343', 'ER0698', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('569ec48f-ca93-43e7-923f-55807a79a6a4', 'ER0733', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f506ca35-e4cf-4537-9a30-f42972cbd82a', 'ER0771', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('46195d01-7a88-42ac-a168-3b311c579ea7', 'ER0776', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9e7640a3-51ec-4e01-8fa1-d9956db413cb', 'ER0794', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9e5bce95-8c4b-4c6d-809d-692ec047dc22', 'ER0799', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('817d9853-e8eb-4fb7-a09b-7559e4a967a7', 'ER0812', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bfcb0fed-bfe1-4581-bc0d-b40a613165d1', 'ER0822', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('266be7aa-78b9-4cb5-89d8-ee0e74aae0e5', 'ER0833', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('dd436170-9321-4301-9454-6274c19bbf74', 'ER0839', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0961b8e3-6ab4-4f60-8e8f-4658f54f32a4', 'ER0845', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ecc660f8-7bac-4c61-b447-16ecc48368b2', 'ER0855', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f529a1a9-93be-4b81-8901-05a43c9e8dff', 'ER0888', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('76dbb4b1-c7b3-4493-a3a5-33a3cb7316bc', 'ER0906', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('219e1e02-fbb7-46a4-8304-30db4ecae9f5', 'ER0907', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a65b8d3c-aa5b-4a6f-8ea2-0610ffc1b092', 'ER0918', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9ead7076-f393-4435-a667-5361e742ac5b', 'ER0928', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5781291b-9511-4328-a6ed-c52565962dc5', 'ER0933', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8201', '8202'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4ff4bf36-0ee8-47f1-944a-5db4eb102583', 'ER0232', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('39723017-9af6-4b50-83e2-770939d81a96', 'ER0250', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ef55a00b-f682-4559-b18e-adadc1b8f6f5', 'ER0264', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ca139145-2f6b-44f8-85c5-760414bbac11', 'ER0292', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('103569e9-bebc-4e5b-b628-a068340fec68', 'ER0396', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d70044ff-0fba-4375-a416-57f424dc6633', 'ER0400', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('febc9b80-1358-4d44-8faa-da9ef45355c4', 'ER0420', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('025a3a3f-b7c9-4700-bd9c-e4a4b667baed', 'ER0421', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ca3a197a-c0d1-4790-bd8c-5745396f4f04', 'ER0425', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('deaab6d4-3ef5-4a58-9c89-a1c8b9f196da', 'ER0475', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8c73e07c-8bde-4313-97cd-e1579058e103', 'ER0484', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c29ef016-5cde-4d6d-9f6d-af6c69832d38', 'ER0485', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('aa4cc206-c6f7-40da-8093-aea833cd30b1', 'ER0513', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b31400f1-36cb-43f8-84d4-ceb4d210e6cf', 'ER0528', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('16103891-1897-4461-b952-be9a26606d99', 'ER0537', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e342e3df-c84f-45cd-9ff0-a49a5c951c68', 'ER0558', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('34435d82-1ae0-41f8-aad9-6c4c7fccd46c', 'ER0562', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7baf756e-e67c-42af-851e-0f281de7dec5', 'ER0566', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('be7f7bb0-dec3-4315-992f-14b7525f38dc', 'ER0575', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('20cb44ac-bb51-4beb-be6c-1ae23ea2de44', 'ER0577', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8680bb9e-56f0-465e-afb4-855ec32251ae', 'ER0578', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e904565c-9dd0-422f-84a8-af1b256f6ba9', 'ER0594', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('174c2146-818e-4347-90f5-4870f63c553e', 'ER0599', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4f4b1a91-33eb-4dc5-a761-20989979d5cd', 'ER0612', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5b099e8b-2c34-4571-a096-c7fd3421e313', 'ER0622', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('58b34085-3d72-4db0-9e30-6081aae3256d', 'ER0630', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ae0f395c-5ef0-417d-b1be-da53038f7d8a', 'ER0673', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('345a7bcb-89cc-430a-963a-c4fc4c90c6a4', 'ER0686', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3850dd5d-1766-4c31-a6f4-03e986979f53', 'ER0691', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0ac94618-a95d-4438-abce-b2132d3bc511', 'ER0728', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bda0d920-5ddd-4c5c-a6c7-4749911640e4', 'ER0761', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2afa9ea1-b3d5-4c07-972c-e10b9c4fa68d', 'ER0762', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('20ec4789-e5b6-42ac-9774-f4c2984ca87c', 'ER0766', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ac110c50-160c-44c1-8e5a-ed939c5548b6', 'ER0780', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('14f26cf7-7939-4e5c-9632-d84169a4a7cf', 'ER0801', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('87605dc1-afbb-45cf-99f5-84fcc2821bd1', 'ER0804', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d45222de-aaab-4c3f-b85a-7b35cc7e51cb', 'ER0806', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b4020db7-f7bd-4e1a-b84b-43b2f75f43ea', 'ER0818', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e7fe0225-fef2-463d-ad29-7b692b13ccaa', 'ER0830', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a0e99fe8-d873-4f4d-9d41-8c7f41a465e7', 'ER0831', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('afbcc0f6-a537-4d49-9f0f-1a484216fa60', 'ER0849', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('52f3109a-0d73-4d7a-915c-fa709056b4cc', 'ER0870', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a98f1d3d-e345-4007-b5e2-35eb18c9fb8a', 'ER0876', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('44c97804-82ab-4cc2-b987-eee8aa8ccb75', 'ER0881', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c857bc75-6371-49fe-8c89-5fbbb95299d4', 'ER0898', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0f06df36-d78b-46ea-9131-80390cca4a2a', 'ER0917', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('455e8879-89de-4cc4-b30c-28d479f04354', 'ER0935', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8301', '8302'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('32be1ba8-11bb-4654-ae8e-d955e226c2de', 'ER0391', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f4738619-8a39-49b8-9baa-7cf04572d5ef', 'ER0392', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fae50b70-4e84-41ee-942c-0b602ef48ffc', 'ER0393', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bebe88f8-b7a3-4448-962d-17909e68d351', 'ER0397', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5125de0d-c6f3-4b4f-b74b-12fe3fdbbe1a', 'ER0402', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7fa7961a-d377-489c-b468-27024efc2927', 'ER0415', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('26d39900-f91b-4995-a829-20331c896c90', 'ER0434', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e3360e6a-1f97-42b5-97cb-dea0ef28f3d2', 'ER0437', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8f5997d5-1408-4dc6-88b8-9e94bb05e123', 'ER0442', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e5c241a6-4310-40a1-aa17-9601b7272813', 'ER0451', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7675ae12-064a-4e16-9361-4176971630d7', 'ER0454', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1ae4ac01-9a83-43b3-944e-5a4ea9820975', 'ER0500', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('53b2037d-9007-4e90-b96f-0c35ba773fab', 'ER0502', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('84ca94e7-4fd9-4566-88ae-d6fef2bc458f', 'ER0522', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('64406b13-3ef4-4e09-b5dc-cf59fdc2b527', 'ER0530', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cfc4b28b-fb5b-43e8-bf72-c196c8439841', 'ER0540', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e550e034-7d42-46df-9823-e54337617934', 'ER0550', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4ea5bbaf-b624-47cc-b6be-9711b24536e2', 'ER0561', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b46c8fd6-69b3-4dc7-b986-fe7e9ae16fb5', 'ER0563', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('917218e4-64a0-4761-9850-49542dafd987', 'ER0565', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b1038243-4f97-48b1-8631-d1485d1cf967', 'ER0573', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('18ff2a9f-a51a-4b42-bee5-b69d3ba031a0', 'ER0602', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('11b229ea-7956-4cc1-b541-ddb4518d3029', 'ER0614', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f4ce0746-2f79-454c-9ac9-b5886a091cc9', 'ER0615', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2d98a48b-99df-41a1-9924-de67af42cc9d', 'ER0616', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3fa339e1-3685-4aef-8c13-e2922574d9a8', 'ER0636', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9886782c-fb85-4190-8977-359d60ccd2f5', 'ER0641', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('450ea166-fb63-44fb-b298-e9ebbdd278ba', 'ER0649', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5d885aa9-c3f9-405e-842e-5493327ebe14', 'ER0653', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('79b6e298-b56b-48ab-a721-14d6ba29681a', 'ER0658', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2c814c13-e4a6-4524-af6c-8e5dddfd5bcd', 'ER0664', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ba0ebee3-a626-4863-93f0-83a41b7c1b89', 'ER0678', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6fa3c5e1-631a-4b3d-9be1-d213cc3888a3', 'ER0682', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('aebce28f-37d2-4f62-a7f5-f360c65fe8cc', 'ER0704', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('db80dceb-cbdf-49bc-8e40-774604d4feb6', 'ER0715', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('57e8292a-c848-446e-88d2-320377bbd864', 'ER0720', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9023ce31-8705-4ddc-b6a2-b0be94904a07', 'ER0729', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1ed6b5dd-2cd6-4db9-a5fe-1240f1b11d7c', 'ER0730', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c1d9d47f-4044-463c-8749-16af7e838dae', 'ER0739', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4a45f635-9439-4d59-bfcb-9e467f23674c', 'ER0740', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b471677b-1010-4331-a7e0-652ae05ab00c', 'ER0755', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('64d28f20-d512-4dfe-9e25-01118372a84e', 'ER0758', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7ed0ddb7-2e46-4145-9b05-6cd56c6037f7', 'ER0772', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0fa34c0d-0070-4a0f-b624-2c0506201f9e', 'ER0787', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('386c4cb4-dc67-4a51-ba00-e933f6d9ad99', 'ER0789', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6c19cfdc-4ff0-4ab4-a68c-9d55bf98e1e4', 'ER0798', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('91475f3f-93dd-40bc-b0e5-8d85df7af33e', 'ER0819', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4e8e5886-6f4d-47cb-8214-aa4cd3a9c554', 'ER0884', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b50006bd-23f2-499c-8670-f73831a99907', 'ER0940', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8401', '8402'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3b45216f-b3d3-428b-9e2e-226805feb575', 'ER0243', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('66f1884b-8e62-4bf2-8bb5-6db5630b06ec', 'ER0276', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6021dde4-d44b-44e2-9426-ff02d19a7349', 'ER0289', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('75e88208-ea76-41a8-93be-e9fb21c2d824', 'ER0307', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('dd1ee306-97f8-45b6-8d7d-31e4015205c3', 'ER0395', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c3a74f62-ca5d-4e9e-86a3-6c4f2936647f', 'ER0398', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1f00300c-f30f-48b9-8d58-2c06e01f1eca', 'ER0405', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8f3a0f3f-e94a-48fd-817e-c6703209d55b', 'ER0408', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('592a9a4d-e01d-4982-a892-a2563e368200', 'ER0417', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4b310da2-5a96-411c-8139-16d9af29d1b3', 'ER0418', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5eb79092-a49a-4e2c-a8da-db29ca5c5b76', 'ER0433', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8d2030e0-cbab-4145-8997-eb8f69fe7028', 'ER0461', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8531d883-91ba-42d1-930e-417f1b7817be', 'ER0483', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('655bf75d-8bd0-400e-8ccb-9da35c9563b2', 'ER0486', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ada0c954-818d-464b-b645-14c10bed38b1', 'ER0492', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7fbcf57b-60a2-416d-882c-55b8f64b038b', 'ER0498', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6ce448ad-fe7e-4747-bae2-fa2fc8716aea', 'ER0503', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('54a54e8e-af22-4e23-8c00-b9b9cc1c3522', 'ER0508', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0857e35e-736f-4482-9eab-cd1193ed7a3c', 'ER0516', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bef3c26c-1b21-40c3-a93b-70604f8f2e7f', 'ER0517', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('37aa644d-13a6-47c1-8e10-a11375e855c6', 'ER0536', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('929bacb9-1971-4da7-a46e-95e25e2ee18b', 'ER0572', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b41d60fa-50d4-42af-b2d0-2eb7b438e6c0', 'ER0583', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f1a5a1e6-40a7-4344-b263-533382f0335e', 'ER0587', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cb3e714d-3c75-45d7-9c27-a654beadaf1e', 'ER0603', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('90dfda6f-a07e-43d7-b53c-6403c10e70c9', 'ER0637', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ef765ad9-9d68-48a6-a88d-aa0c512642d5', 'ER0640', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('42e14dc4-2c2f-40ff-9174-c6c08d89d7e5', 'ER0702', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3a7ce3bf-999a-4159-8129-c8f9a9c85a8a', 'ER0707', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7390fbfd-1b5e-4c86-b484-4886fdfaef98', 'ER0713', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('222a866e-b908-4911-96a8-1fd6a747e9ab', 'ER0714', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8d04f3e5-674f-497c-ae28-30b2760555c7', 'ER0735', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ca46a5d5-24ce-4cbe-b82d-20332898a151', 'ER0736', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('28d94088-1611-4f2b-904e-72e69c045966', 'ER0748', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fe7eebbd-0f0e-4ca7-b9b9-ae23dc561c1f', 'ER0756', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('57af0deb-6fc7-44e7-82d9-ce4f58ead398', 'ER0759', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f4208f89-2baa-4744-9b29-a8d3737e29f4', 'ER0781', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4cd06a7c-eefb-4abd-a89d-56d8969e8b56', 'ER0796', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fadb0c34-dc98-4f8e-a8bf-37eedf00e87b', 'ER0807', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a326bc9c-872c-46c3-b958-8607936d4f1c', 'ER0814', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('aafe2760-86d7-43c3-80c7-f2e878fd9060', 'ER0824', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('17d6c9b0-5a61-4b51-9c5b-92b595f8a1c3', 'ER0837', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('067d7c61-9700-4a29-adfc-122c2235e1e8', 'ER0847', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e8745d10-a448-4b94-9880-5078b6e19ea7', 'ER0853', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5bfc6567-b392-4ac2-9b09-34736efbc445', 'ER0857', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a2301a1a-15ad-4ac6-bcc5-7866fe7e7ae7', 'ER0859', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1629b705-12f9-4595-abf7-411e9419b545', 'ER0877', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4ce69add-afc2-4739-bff2-cdcf05c4f931', 'ER0902', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c53d8613-5eb9-4b2f-90ee-3604cd67c28c', 'ER0919', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('66a0ec93-0a11-4c50-b1d9-350d92483300', 'ER0924', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('276a4503-4922-4b2b-a539-204dd7d01872', 'ER0929', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8501', '8502'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5b5f4e09-639a-40f6-814f-e88832a8ba22', 'ER0234', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c2d25c3e-c9b3-4d44-bc7e-67f929765a01', 'ER0257', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e1e3766a-07fc-4cd9-84e8-44686a848c64', 'ER0262', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('df89693c-b27b-4846-9d27-490b87c343e6', 'ER0266', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7b606510-c3e5-4adc-9642-dc7cb90311b9', 'ER0268', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5218c202-9fa5-4f27-aabb-05b99c03e129', 'ER0293', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b21d8b50-a2bd-4385-b61c-c96700445a10', 'ER0294', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('45fbe1ad-c3b8-439e-9a2a-26bc3bdfd465', 'ER0300', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('24c48db9-ef4c-4f6d-8637-5fd9d772b019', 'ER0306', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5afa5ad2-d2e9-43b5-abe2-4de4c037bfd5', 'ER0455', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9bc9836b-20bb-4030-8f6f-71042710a5cc', 'ER0487', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5d1f7f47-9df2-4c85-a3a8-b05eaa5ff581', 'ER0488', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('916a503c-ed46-4a05-8da0-70b9966fe939', 'ER0494', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('44107f3b-34cd-42b3-9871-20ed9172cc34', 'ER0496', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('90da126b-3b7a-4856-b488-e80cde70e426', 'ER0526', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('485b767d-adf2-4693-a40a-88a0cba082af', 'ER0545', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b36ae160-3d96-471a-a00d-d519d48aa124', 'ER0551', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ca166db1-61af-4c80-ba81-93dd4da40e0b', 'ER0556', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('75ab7abc-2b77-4cc6-91a9-c72dbde3e0c4', 'ER0605', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4426c5ed-32b3-4315-93a6-c38f63052ed9', 'ER0621', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('144110f5-29e0-45b2-b16f-dec5efe60dca', 'ER0628', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('01e0afe2-27e9-4b72-94f2-c26075cb2468', 'ER0629', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('76695a22-10ca-4c5a-959d-dfc40d3c37f4', 'ER0644', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4add0597-ab48-44bf-9249-82bad9683547', 'ER0667', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d0a73e2a-09fa-43c8-84b3-35ae40093fc2', 'ER0670', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('dd6c11d2-4269-45d2-8925-6fa05fc5e0b6', 'ER0677', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1b6c3642-437d-425f-9c9b-ba174d230f2a', 'ER0679', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7f128c18-eebc-4bdb-b0d6-edec7369345b', 'ER0711', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a56d58a3-acd7-45d0-a792-e713c1b8c84b', 'ER0769', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('45aa8a5a-22f7-44a0-8d80-a1c6df6d1126', 'ER0774', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ac16bc34-0986-4782-90e7-c5331e552b08', 'ER0778', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9eee6868-a339-4180-a8e3-dc1939c4b979', 'ER0785', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ce1467d9-6310-4d1e-bb25-d20d74d83c3d', 'ER0795', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('00f786b4-9362-41fa-898e-7cc97856bf1f', 'ER0808', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a9dff12d-3851-4fca-86c0-5b3ccbd88410', 'ER0841', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fb4bf42b-a7b2-405b-8cf2-5acb9fca77f9', 'ER0864', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6a3a1c87-e237-43f2-9871-d8e06b81551c', 'ER0874', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('aba45f65-f9fc-4d59-802f-52918d6a2cbb', 'ER0897', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('22f05112-fd59-4bc5-b20d-f834365b7725', 'ER0910', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f64753b7-8329-49ab-8c83-72354382ad9e', 'ER0921', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('26d1a520-81e5-46c2-aa7b-106b3cb0a5ca', 'ER0922', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('34d5896e-036d-43db-aeb0-21f40ff1893a', 'ER0930', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8601', '8602'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('48b4ea2f-9344-4ac2-9e05-3d01020dbbd1', 'ER0224', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('db4768fc-a3c9-445e-b5f5-c4a3918aa251', 'ER0233', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d85d7d8b-ac82-4701-b803-d5c011c8cf81', 'ER0263', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a8558ac6-1ada-4e6c-86bc-06af088baafa', 'ER0282', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d2557abb-aaf8-461d-af63-0abadceebf69', 'ER0291', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('df532af7-c667-4fc9-8c29-82519e4dda33', 'ER0313', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f91ee436-06ab-4fce-901f-f21630a6548b', 'ER0315', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('63b506c4-29cb-4d01-b8d0-4f9c064c1fb2', 'ER0394', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('490bab67-e0f6-46cd-9c1c-84513602c107', 'ER0409', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c2d7f8cc-199d-4898-80f7-5d611d5743d1', 'ER0423', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('92ba1e7e-d73e-4661-9259-6172db65ae76', 'ER0429', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4c75e5e4-fc76-41f9-ba10-1c5842cc066d', 'ER0444', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bbf05411-4a8f-4899-a7b9-7ae5be878503', 'ER0449', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('84835798-8688-44c2-9087-098b580f2530', 'ER0466', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ef2708f0-aac6-42ac-bda3-1b0a5ef7853f', 'ER0467', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f2fb74db-8cf0-4524-9aeb-92069cef0fdc', 'ER0471', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1e3b292e-5ad8-47d6-998b-2ffc905319ec', 'ER0481', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('121c3724-289b-497c-9fda-153bb3a7a795', 'ER0499', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('db166fca-953d-496b-a77d-3713a10ec081', 'ER0518', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4fcaf494-3725-468c-85ec-6cdbaa4f643b', 'ER0525', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6fcf243b-2230-42b8-942c-546be0b6b114', 'ER0531', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7339304c-8400-4939-addc-c2c4aefa9c7d', 'ER0533', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bbed71c2-a657-486d-8f41-fb39d4eb5701', 'ER0548', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fbc0f04c-b8ed-4a76-b3cd-e548fc80f36a', 'ER0553', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c934ebf7-b05e-4e24-845f-af8d6631b02e', 'ER0554', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('864543b5-4b94-4e97-a939-15579f86b60a', 'ER0560', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1cca5ef8-2ded-4432-aef9-482cfe4bdfa6', 'ER0580', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('393f2e14-0b81-4451-9344-d5c52c49860b', 'ER0582', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1ebaa136-fa67-47eb-ae1f-c9e721f41f31', 'ER0593', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('eb8b77f6-d06b-478b-b3fb-5d90f9b13deb', 'ER0598', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8f9f6a2c-f941-449d-8196-14c9b6481195', 'ER0607', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e6cc959c-c08c-45a6-9faf-d0686606312b', 'ER0608', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('92c08123-66e6-4200-9fe2-790c1c9fb508', 'ER0613', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e9bba76e-2c70-4e4e-9c5a-a8d4570815fe', 'ER0619', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8840de27-7db1-4c0e-a6b7-7db26f1589e6', 'ER0634', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a777c006-4d1c-4ae2-ac9b-d99b4c4e4bb3', 'ER0645', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('548d55f1-e19c-4e76-a0c4-245c22d64e1c', 'ER0647', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7f81e1a1-7417-46b1-8e00-62710a63f1d6', 'ER0655', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0d5b35a8-2356-4601-b653-bf53c08bf4a6', 'ER0661', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b4ce9b6b-60ca-40eb-980c-00f895005ad7', 'ER0665', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('39e50601-6a88-4f2b-8f05-18e72b09720f', 'ER0671', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d6dee81d-b8a8-4725-bf42-a6b4363b6b78', 'ER0675', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6ff857ae-c692-42a9-9b2c-7e6224bd6151', 'ER0680', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5bc99e38-d1c4-4b2c-b96e-4af70617ebfd', 'ER0683', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('957094af-17b1-4640-a8be-27a8c5095df0', 'ER0689', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('99a6ab1d-e48b-4ced-8667-53027856678d', 'ER0693', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c0f394d8-bca7-440e-a9b3-e685c4fcf22f', 'ER0700', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('74e95deb-8659-4fb9-b2a7-53b0a1b552b3', 'ER0705', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a1f03597-33cc-43ef-bfc0-b78ae6f91827', 'ER0706', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fadeeb80-fb4b-4885-9cd1-f2b7f33bab6e', 'ER0718', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3ae69d77-428a-49de-bcc2-99ffbc981948', 'ER0726', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9c30f391-d476-455f-84e6-c12b699acc7c', 'ER0775', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2d9f04a2-3be9-4d3b-b5b6-1868789bb8ea', 'ER0788', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('53d44bfe-09c9-4eb7-86b3-6626813bd203', 'ER0797', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('eda8a7d9-4de9-4720-9620-9c0a065ecc83', 'ER0823', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2bfd5224-4f3f-46ae-b851-498b972c0612', 'ER0854', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8b59cc57-8097-4052-be27-6463663235eb', 'ER0861', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a4bb37cc-a039-48b6-b53a-7380311822a9', 'ER0878', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('666e10a9-1042-4a00-b50b-5ffd59c88b9f', 'ER0887', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6c098a51-3be1-41a9-854a-eca567836036', 'ER0890', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9dfd5f00-ce8f-46c8-a235-447943a0452b', 'ER0891', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('113fdac4-2b71-4467-bd9e-4d613d4bfd2b', 'ER0900', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bce95038-cbf3-478b-b797-180cbcedd3c7', 'ER0904', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('886242d5-d42b-4b85-aa34-b92857c9f0dc', 'ER0909', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('d6475596-dc89-48f6-b7e2-f4502af2c6f8', 'ER0911', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('33faf2b3-ccbf-487e-9837-1b5dcc99b6d2', 'ER0934', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8701', '8702'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('15b7ca8c-9922-4a9c-b9b1-6e637e7a1c39', 'ER0275', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('7545a1c0-7027-4c40-ba8c-748e96da1d5c', 'ER0333', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('deb2d3b4-d0ad-4070-b9e0-bfc5c0578e2f', 'ER0344', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9d4f86e5-e1f4-491f-877f-ccffcd14ab6a', 'ER0404', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3c814bed-c470-4b1d-8bb9-69a49df6aeaa', 'ER0436', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('117595c6-8e75-4670-be93-80ecfecd0379', 'ER0460', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5e4a46fe-4a31-48b1-8daa-f8eb5689148e', 'ER0463', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8b9d5d43-5e44-48b2-b1bd-5acff34a15f6', 'ER0465', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5f3820c7-ae84-437f-8950-47800517ff27', 'ER0469', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('481a6c4a-3db2-4fb6-978d-21316ce814c3', 'ER0473', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b85e9254-cadb-440c-8e4e-61802902a665', 'ER0476', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9c1bc306-db4d-4d39-8424-110d870e7dd5', 'ER0501', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ad940857-679b-4420-b129-4b04b5a07811', 'ER0504', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cc4b84b8-c4c4-4dad-ab7b-8d4fe0157ffa', 'ER0546', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8dafc20f-2c2d-40ed-aa3e-c98fafe838cd', 'ER0552', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c01e8109-57ba-48bb-8f23-a91774ec55e2', 'ER0569', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('808333ea-eb66-4d29-86d2-249dc371b100', 'ER0591', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3d0311b1-41a5-4645-bf8c-d306b8a799e6', 'ER0601', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('23c9f847-c950-48df-aec3-ee739cb9ad19', 'ER0610', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('68de6630-d048-4e9f-a75f-080675865052', 'ER0626', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('14fee469-347b-4a0d-9ddb-b64687647af9', 'ER0652', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('23c53815-feed-4e70-ac86-a12ef9a06557', 'ER0669', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e1033611-3fbc-4e58-ad80-27a7c1ceb450', 'ER0716', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('21b6c26c-f555-4a64-a171-0e46cdb5b923', 'ER0717', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('76fb0ea3-1b62-4063-9dfb-4be0c160551b', 'ER0749', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('64024720-d7d1-48d2-bf16-7ca3e44d6578', 'ER0751', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0378f377-221e-4b59-a70b-bafcdc69b1be', 'ER0777', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('68d544ea-7b3f-4de4-8e11-64652d81d21d', 'ER0805', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ea24e86a-1241-43ce-8d69-9121755306b9', 'ER0809', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6c116df0-435e-4098-9138-ffa821550a4f', 'ER0811', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8505fe09-a6bf-4b86-bbdf-9cafb57cff07', 'ER0827', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2a6e39dc-85b5-47dd-9a10-81f9f4e10531', 'ER0832', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('dd982300-c457-4017-8401-080529e12b12', 'ER0883', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('53897945-831d-4b05-99d2-02bc94bdf867', 'ER0886', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6989ad64-80d7-4a48-afa2-1b721ca8e63f', 'ER0889', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3da36b57-8719-4f34-ab0c-d90cf1cf244b', 'ER0894', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('44fa08b3-e6c9-4712-a526-8eef60ed0ff2', 'ER0895', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('63d64918-d12b-46fa-a1e2-34fb7134e534', 'ER0896', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5ec32d8c-7594-49a7-a19b-09401a9a9ba5', 'ER0903', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e0f372ab-9312-4429-8a4b-176c337e3e49', 'ER0936', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8801', '8802'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5795e9d8-f589-43aa-a50a-537ee9727cc3', 'ER0221', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0a2841db-8121-4263-9eed-2f51ce413057', 'ER0227', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bab539d3-e05a-4d06-b77f-736d1315fb4b', 'ER0271', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4cbffeae-1565-49d4-86f7-94e165843502', 'ER0287', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5839ffd3-b953-4ed6-8f07-3fada891bd6f', 'ER0310', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6c232d17-3e7c-4773-a9cf-efb7e100cfa5', 'ER0349', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5037e250-5f37-414e-95e6-ddecde35dc1e', 'ER0403', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9cfb155b-a81c-406a-a67c-38efeb6c70ce', 'ER0406', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4e8f2fdd-8873-4048-a296-a1bf79b3fdc5', 'ER0413', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('63a2ce55-b94a-41a5-8415-ce69ff933873', 'ER0414', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('597231aa-cd99-4d70-bf89-1e8dac4339d1', 'ER0416', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('71214120-7d0e-4f44-af70-88b2ccbaeede', 'ER0428', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ad5f34bf-9d9f-46ed-98c1-d25d4350a5f7', 'ER0441', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0745e642-aa0d-4a96-927a-9fa8cce6a236', 'ER0448', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a5a15904-b3fd-40c4-a8f1-a3997d4b8a13', 'ER0453', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c8c35711-bd14-415c-9ac0-02c0c4ca316d', 'ER0468', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('dee33af4-9055-4056-b290-d0a5aeee3028', 'ER0478', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('daded727-0378-4ff7-ae2b-48a4c2d5fcf3', 'ER0497', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3c4c57d8-7572-4066-ab3a-ec2cc5ae4dcb', 'ER0506', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('41804040-1ffe-46fd-a1dc-e8090ca632b9', 'ER0507', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('39057978-6d6e-4012-a8ac-80a627651319', 'ER0511', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('caf96e80-497c-4eba-9c8d-7b1c1786a768', 'ER0512', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0a025d5f-2a26-4dd6-8104-2f6bcbc4aeeb', 'ER0584', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1ee39efd-69a8-4611-aedf-ae04cfa104f8', 'ER0589', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c2d1438f-7e25-4cd5-9964-24ff0111a5d5', 'ER0597', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('56763b7d-c8e4-4a19-bb1c-4c36b1bb51c1', 'ER0627', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('95f9fe47-e32c-47ff-80da-530c7d8720d5', 'ER0638', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a616ddb2-541e-465c-9686-0da4045b44a5', 'ER0643', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('53578253-6f35-424e-b9e6-182584a515d4', 'ER0659', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c9e3c47e-ec6e-4b04-82b7-15b484d5536b', 'ER0743', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b26a6799-4bee-4477-bcc9-dce162f8fe1f', 'ER0763', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c377e023-995c-4740-a57d-32960a3f96ab', 'ER0765', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('db616bff-378a-4515-8409-f39f669d41d2', 'ER0767', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('082d9f7f-e18c-4372-8889-cd5368c286ed', 'ER0866', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c5a8976a-081e-4877-a381-54cc4be30534', 'ER0873', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('f697d11f-c097-428e-99a4-33cd090b1aec', 'ER0893', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('554e1780-3331-4f54-b012-5ed28ad9d067', 'ER0908', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('986a0cd5-29bb-49af-a152-1470dc24c56b', 'ER0913', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fef77879-433f-4fe1-ba57-e0ba42a50d2e', 'ER0916', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '8901', '8902'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('cbd30f71-825e-4d82-9e05-6e8f9e64fa0d', 'ER0225', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c278a611-1e08-45c7-a5b5-c0ca77da0585', 'ER0229', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1fee60de-d0c5-41de-9c14-f5ad210a1701', 'ER0251', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('53c5c988-f8e8-4b53-b883-6dad185fc23c', 'ER0259', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a0165c32-9d76-4c7b-bec0-32f00cab1d03', 'ER0270', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('908085eb-45d6-4131-8fc3-bdcafa585570', 'ER0285', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('119ae9e0-a633-4db2-8f2a-86ac5add3b48', 'ER0298', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('876e240d-ff7b-4bec-bcf7-28bbaa4d748d', 'ER0327', '7b5d91ee-d36c-4383-a27b-3b65179d568d', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c265c938-843c-43e9-b7c9-169ff60a6fab', 'ER0337', 'a3cd0de4-33f1-43e4-bf27-03a94b03e1e7', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('eb6fdc23-6dd2-462f-a940-b5c6aa9fb809', 'ER0399', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('56bf95fb-9b77-46b7-b25e-6ef87e22ba8e', 'ER0446', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e2108bea-12c6-4586-9f5a-cc1476eee9a9', 'ER0450', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('e52b3ba4-3bad-4fdb-b25d-62476ac70051', 'ER0477', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ebf47cb5-ab4b-450b-8f51-371e86bbdfde', 'ER0489', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('269ece03-5f28-4f84-b0e2-2cd1cf9a4be1', 'ER0490', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4499b6f8-c0d2-45cc-a258-d4dbabe987b0', 'ER0529', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('22f14acc-01d8-49b9-9d1f-59b9a80bfdcf', 'ER0570', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a71488ea-4d28-4c72-badd-5d6bc561f852', 'ER0574', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3dd05274-f775-4bcb-b6da-2a0f84e07a35', 'ER0576', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0f5f4b73-23f8-42da-8585-61c4525e57de', 'ER0592', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bf10f3ba-67a2-4fd6-85f4-0f2e5ee2da92', 'ER0617', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fe1692d2-c43b-4962-9ac0-b5fefdef6c1e', 'ER0620', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fa334b93-427a-4219-af90-8eb0438f3ccf', 'ER0623', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fbf24d86-974a-469c-b7bc-90828fb023c5', 'ER0631', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('0f79d8c5-86fd-48df-9c30-926d860ec1f9', 'ER0651', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('fea11fd0-baa6-4f8d-a461-61b9b8da026d', 'ER0668', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('4a38b1c4-21bd-4cfe-81f5-eda01c54d1bf', 'ER0672', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2ab1614b-cef4-4bce-a939-7e0435403aa4', 'ER0681', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('dfda7d1c-8bd8-46e3-87b1-535ff26e09ba', 'ER0685', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('2ad12db5-339a-4a38-bc8c-a942ec85883f', 'ER0699', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('1a3c1a1d-2d02-4bb0-8a2d-8e9c791d4651', 'ER0732', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('c7cb4640-2251-4c71-9a4d-fd124284fbad', 'ER0737', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('21f7c365-73c7-453e-a8e6-e7fbfa0a88a1', 'ER0738', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('bce08b82-4a75-4d43-bbdf-cc5cb2dee90f', 'ER0741', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ea8bfb8f-7a7f-4c69-b90d-d464f440e5db', 'ER0753', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('a256a58e-1044-448f-9b0d-bbb900243b41', 'ER0754', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b1acbd02-b4e4-4ba4-9633-cc5226264e3a', 'ER0770', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('5cd93f30-2d56-4ce5-91c4-b195fc337bd0', 'ER0802', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ea4b1c28-4e88-42af-b7d2-f5dfe645ffbf', 'ER0803', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('8d2b91cf-5f97-42d0-b34b-0927654f1a28', 'ER0810', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('6add4aab-b606-4503-a983-737c44207a13', 'ER0815', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('721bd886-2d45-4b36-9ed9-a9a8c3e4c82f', 'ER0836', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('3da30c23-fac3-4149-abe2-ce49cddba882', 'ER0842', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('256ab1ed-927b-411a-9ec5-afdfcaacfe61', 'ER0851', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('355f676d-0991-4516-baaa-175328a0f1ca', 'ER0865', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('ca1158e7-f886-495e-9700-be56305b4b39', 'ER0872', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('121aedad-1ab9-49e1-920b-526f2db3cf4c', 'ER0882', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('9ffc812d-a667-4894-9ea2-4a39e3c3f73e', 'ER0899', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('85c8140f-1308-4b5d-b9cb-411e5fefb040', 'ER0912', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, train_id, sequence_number, status, notes, created_at, updated_at, deleted_at, current_location_yard_id, train_set_wagon_id, current_train_schedule_id, current_yard_id, export_train_number, import_train_number) VALUES ('b76d84fd-839e-4bf1-b7f5-ddca82dbbd3a', 'ER0927', 'f7886975-ba6f-422d-8de4-e672c8482699', NULL, NULL, 'AVAILABLE', NULL, '2026-08-05 07:31:08.666221+00', '2026-08-05 07:31:08.870506+00', NULL, NULL, NULL, NULL, 'bf73dbd3-79aa-4ff8-a471-aed47e5ca29d', '9001', '9002'); +`, +}; + +const FOREIGN_KEYS_SQL = ` +ALTER TABLE ONLY freight.last_mile_container_allocations + ADD CONSTRAINT "FK_0bc28c4087bb0c6e435677a6fcb" FOREIGN KEY (last_mile_id) REFERENCES freight.last_mile(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.ff_clients + ADD CONSTRAINT "FK_1101fbe8e745d91b6bd0747b582" FOREIGN KEY (forwarder_company_id) REFERENCES freight.companies(id); + +ALTER TABLE ONLY freight.last_mile + ADD CONSTRAINT "FK_173a865c0889b8c5564993c70ff" FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.external_profiles + ADD CONSTRAINT "FK_26d2a16e263ab4fe0a4c8e91954" FOREIGN KEY (company_id) REFERENCES freight.companies(id); + +ALTER TABLE ONLY freight.company_profiles + ADD CONSTRAINT "FK_30228dd283a0f14486346e1db96" FOREIGN KEY (company_id) REFERENCES freight.companies(id); + +ALTER TABLE ONLY freight.first_mile_container_allocations + ADD CONSTRAINT "FK_31dfdcea6bf62ec98f2e5428996" FOREIGN KEY (first_mile_id) REFERENCES freight.first_mile(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.company_change_request + ADD CONSTRAINT "FK_366962cebba507dd8b0146f7f47" FOREIGN KEY (company_id) REFERENCES freight.companies(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.warehouses + ADD CONSTRAINT "FK_374ce3a1439d02d5ff97e479921" FOREIGN KEY (facility_id) REFERENCES freight.facilities(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.booking_container_allocations + ADD CONSTRAINT "FK_4fa907f6cc2cfe181385dcd57c5" FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.booking_requests + ADD CONSTRAINT "FK_590f6298c3c3ae09d982f96fb9d" FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.import_djibouti_operations + ADD CONSTRAINT "FK_6c3e3046300b152a13505bf8ffd" FOREIGN KEY (train_schedule_id) REFERENCES freight.train_schedules(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_6fa72e9c8fdfa587cffd9383470" FOREIGN KEY (destination_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.last_mile_container_allocations + ADD CONSTRAINT "FK_825ceccffec68ac49c85bd75de0" FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.interchange_document_items + ADD CONSTRAINT "FK_864508e9249279bd3e12dafdd21" FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.company_revisions + ADD CONSTRAINT "FK_8a17ca32f9e4624e1c55c51f7c7" FOREIGN KEY (company_id) REFERENCES freight.companies(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.first_mile_container_allocations + ADD CONSTRAINT "FK_8bba293eb2fd548433d28d97103" FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.booking_container + ADD CONSTRAINT "FK_91946ca51ff65658f487eae1162" FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_container_allocations + ADD CONSTRAINT "FK_99c89fe4e4c3e3670fdc25ea9a9" FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_requests + ADD CONSTRAINT "FK_9e49de0ed55d84d3f97627040cd" FOREIGN KEY (created_booking_id) REFERENCES freight.bookings(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.cargo_types + ADD CONSTRAINT "FK_CARGO_TYPES_PARENT_GROUP" FOREIGN KEY (parent_group_id) REFERENCES freight.cargo_types(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.first_mile + ADD CONSTRAINT "FK_a19b54ac3d426df8cc0e313be06" FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.first_mile + ADD CONSTRAINT "FK_a2ae03360a02c61fdc2e8361ea8" FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.ff_clients + ADD CONSTRAINT "FK_af4d6bac2844d39095b2056e978" FOREIGN KEY (client_company_id) REFERENCES freight.companies(id); + +ALTER TABLE ONLY freight.booking_cargo_modifier + ADD CONSTRAINT "FK_booking_cargo_modifier_booking_id" FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_cargo_modifier + ADD CONSTRAINT "FK_booking_cargo_modifier_rate_id" FOREIGN KEY (rate_id) REFERENCES freight.rates(id); + +ALTER TABLE ONLY freight.booking_cargo_modifier + ADD CONSTRAINT "FK_booking_cargo_modifier_rate_snapshot_id" FOREIGN KEY (rate_snapshot_id) REFERENCES freight.booking_rate_snapshot(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.booking_container + ADD CONSTRAINT "FK_booking_container_container_type_id" FOREIGN KEY (container_type_id) REFERENCES freight.container_types(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.booking_container + ADD CONSTRAINT "FK_booking_container_weight_limit_rule_id" FOREIGN KEY (weight_limit_rule_id) REFERENCES freight.weight_limit_rules(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.booking_rate_snapshot + ADD CONSTRAINT "FK_booking_rate_snapshot_booking_id" FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_rate_snapshot + ADD CONSTRAINT "FK_booking_rate_snapshot_rate_id" FOREIGN KEY (rate_id) REFERENCES freight.rates(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_bookings_cargo_type_id" FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_bookings_company_id" FOREIGN KEY (company_id) REFERENCES freight.companies(id); + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_bookings_company_profile_id" FOREIGN KEY (company_profile_id) REFERENCES freight.company_profiles(id); + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_bookings_consolidation_partner_id" FOREIGN KEY (consolidation_partner_id) REFERENCES freight.bookings(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_bookings_customer_id" FOREIGN KEY (customer_id) REFERENCES freight.customers(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_bookings_service_type_id" FOREIGN KEY (service_type_id) REFERENCES freight.service_types(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_bookings_shipping_line_id" FOREIGN KEY (shipping_line_id) REFERENCES freight.shipping_lines(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_bookings_train_id" FOREIGN KEY (train_id) REFERENCES freight.trains(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.last_mile + ADD CONSTRAINT "FK_c2d9331fda382ae3e92e6bf913a" FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_document_review + ADD CONSTRAINT "FK_c669c986810af3b82cf033a8d79" FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.interchange_document_items + ADD CONSTRAINT "FK_c9fba2c4a1631542a37ad5089d7" FOREIGN KEY (interchange_document_id) REFERENCES freight.interchange_documents(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT "FK_cabe80a0c7b9e0144854a4f353d" FOREIGN KEY (origin_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.cargoes + ADD CONSTRAINT "FK_cargoes_cargo_type_id" FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id); + +ALTER TABLE ONLY freight.cargoes + ADD CONSTRAINT "FK_cargoes_container_id" FOREIGN KEY (container_id) REFERENCES freight.containers(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.cargo_type_wagon_types + ADD CONSTRAINT "FK_cgwt_cargo_type" FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.cargo_type_wagon_types + ADD CONSTRAINT "FK_cgwt_wagon_type" FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.containers + ADD CONSTRAINT "FK_containers_container_type_id" FOREIGN KEY (container_type_id) REFERENCES freight.container_types(id); + +ALTER TABLE ONLY freight.containers + ADD CONSTRAINT "FK_containers_wagon_id" FOREIGN KEY (wagon_id) REFERENCES freight.wagons(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.container_type_wagon_types + ADD CONSTRAINT "FK_ctwt_container_type" FOREIGN KEY (container_type_id) REFERENCES freight.container_types(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.container_type_wagon_types + ADD CONSTRAINT "FK_ctwt_wagon_type" FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.dropdown_options + ADD CONSTRAINT "FK_dropdown_options_setting" FOREIGN KEY (setting_id) REFERENCES freight.dropdown_settings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.file_upload_fields + ADD CONSTRAINT "FK_file_upload_fields_setting" FOREIGN KEY (setting_id) REFERENCES freight.file_upload_settings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.locomotives + ADD CONSTRAINT "FK_locomotive_current_yard" FOREIGN KEY (current_yard_id) REFERENCES freight.yards(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.rates + ADD CONSTRAINT "FK_rates_cargo_type_id" FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.rates + ADD CONSTRAINT "FK_rates_destination_yard_id" FOREIGN KEY (destination_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.rates + ADD CONSTRAINT "FK_rates_origin_yard_id" FOREIGN KEY (origin_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.train_locomotives + ADD CONSTRAINT "FK_train_locomotives_locomotive" FOREIGN KEY (locomotive_id) REFERENCES freight.locomotives(id); + +ALTER TABLE ONLY freight.train_locomotives + ADD CONSTRAINT "FK_train_locomotives_train" FOREIGN KEY (train_id) REFERENCES freight.trains(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.train_set_locomotives + ADD CONSTRAINT "FK_train_set_locomotives_locomotive" FOREIGN KEY (locomotive_id) REFERENCES freight.locomotives(id); + +ALTER TABLE ONLY freight.train_set_locomotives + ADD CONSTRAINT "FK_train_set_locomotives_train_set" FOREIGN KEY (train_set_id) REFERENCES freight.train_sets(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.train_sets + ADD CONSTRAINT "FK_train_sets_train" FOREIGN KEY (train_id) REFERENCES freight.trains(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.trains + ADD CONSTRAINT "FK_trains_current_yard" FOREIGN KEY (current_yard_id) REFERENCES freight.yards(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagons + ADD CONSTRAINT "FK_wagon_current_yard" FOREIGN KEY (current_yard_id) REFERENCES freight.yards(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagons + ADD CONSTRAINT "FK_wagons_current_location_yard_id" FOREIGN KEY (current_location_yard_id) REFERENCES freight.yards(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagons + ADD CONSTRAINT "FK_wagons_train_id" FOREIGN KEY (train_id) REFERENCES freight.trains(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagons + ADD CONSTRAINT "FK_wagons_wagon_type_id" FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types(id); + +ALTER TABLE ONLY freight.warranties + ADD CONSTRAINT "FK_warranties_vehicle_id" FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.weight_limit_rules + ADD CONSTRAINT "FK_weight_limit_rules_container_type" FOREIGN KEY (container_type_id) REFERENCES freight.container_types(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.work_orders + ADD CONSTRAINT "FK_work_orders_vehicle_id" FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_batch_offers + ADD CONSTRAINT booking_batch_offers_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_batch_offers + ADD CONSTRAINT booking_batch_offers_train_schedule_id_fkey FOREIGN KEY (train_schedule_id) REFERENCES freight.train_schedules(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_container_units + ADD CONSTRAINT booking_container_units_booking_container_id_fkey FOREIGN KEY (booking_container_id) REFERENCES freight.booking_container(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_contract_signatures + ADD CONSTRAINT booking_contract_signatures_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_contract_signatures + ADD CONSTRAINT booking_contract_signatures_signature_file_id_fkey FOREIGN KEY (signature_file_id) REFERENCES freight.files(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.booking_document_review + ADD CONSTRAINT booking_document_review_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id); + +ALTER TABLE ONLY freight.booking_handovers + ADD CONSTRAINT booking_handovers_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.booking_handovers + ADD CONSTRAINT booking_handovers_edr_assignment_id_fkey FOREIGN KEY (edr_assignment_id) REFERENCES freight.last_mile_vehicle_assignments(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.booking_handovers + ADD CONSTRAINT booking_handovers_truck_assignment_id_fkey FOREIGN KEY (truck_assignment_id) REFERENCES freight.customer_truck_assignments(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.booking_review_note + ADD CONSTRAINT booking_review_note_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT bookings_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id); + +ALTER TABLE ONLY freight.bookings + ADD CONSTRAINT bookings_contract_route_id_fkey FOREIGN KEY (contract_route_id) REFERENCES freight.contract_routes(id); + +ALTER TABLE ONLY freight.clearance_incidents + ADD CONSTRAINT clearance_incidents_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.clearance_milestones + ADD CONSTRAINT clearance_milestones_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.clearance_milestones + ADD CONSTRAINT clearance_milestones_clearance_cycle_id_fkey FOREIGN KEY (clearance_cycle_id) REFERENCES freight.contract_clearance_cycles(id); + +ALTER TABLE ONLY freight.clearance_milestones + ADD CONSTRAINT clearance_milestones_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.compliance_records + ADD CONSTRAINT compliance_records_vehicle_id_fkey FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id); + +ALTER TABLE ONLY freight.contract_approval_steps + ADD CONSTRAINT contract_approval_steps_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.contract_cargo_scope + ADD CONSTRAINT contract_cargo_scope_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.contract_clearance_cycles + ADD CONSTRAINT contract_clearance_cycles_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.contract_document_review + ADD CONSTRAINT contract_document_review_clearance_cycle_id_fkey FOREIGN KEY (clearance_cycle_id) REFERENCES freight.contract_clearance_cycles(id); + +ALTER TABLE ONLY freight.contract_document_review + ADD CONSTRAINT contract_document_review_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.contract_document_revisions + ADD CONSTRAINT contract_document_revisions_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.contract_rate_snapshots + ADD CONSTRAINT contract_rate_snapshots_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.contract_review_notes + ADD CONSTRAINT contract_review_notes_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.contract_routes + ADD CONSTRAINT contract_routes_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.contract_signatures + ADD CONSTRAINT contract_signatures_contract_id_fkey FOREIGN KEY (contract_id) REFERENCES freight.contracts(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.contracts + ADD CONSTRAINT contracts_renewal_of_id_fkey FOREIGN KEY (renewal_of_id) REFERENCES freight.contracts(id); + +ALTER TABLE ONLY freight.customer_truck_assignments + ADD CONSTRAINT customer_truck_assignments_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.customer_truck_containers + ADD CONSTRAINT customer_truck_containers_assignment_id_fkey FOREIGN KEY (assignment_id) REFERENCES freight.customer_truck_assignments(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.customer_truck_containers + ADD CONSTRAINT customer_truck_containers_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.facility_handling_events + ADD CONSTRAINT facility_handling_events_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id); + +ALTER TABLE ONLY freight.facility_handling_events + ADD CONSTRAINT facility_handling_events_inventory_id_fkey FOREIGN KEY (inventory_id) REFERENCES freight.warehouse_inventory(id); + +ALTER TABLE ONLY freight.facility_handling_events + ADD CONSTRAINT facility_handling_events_train_schedule_id_fkey FOREIGN KEY (train_schedule_id) REFERENCES freight.train_schedules(id); + +ALTER TABLE ONLY freight.facility_handling_events + ADD CONSTRAINT facility_handling_events_yard_id_fkey FOREIGN KEY (yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.first_mile_vehicle_assignments + ADD CONSTRAINT first_mile_vehicle_assignments_first_mile_id_fkey FOREIGN KEY (first_mile_id) REFERENCES freight.first_mile(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.first_mile_vehicle_assignments + ADD CONSTRAINT first_mile_vehicle_assignments_vehicle_id_fkey FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id); + +ALTER TABLE ONLY freight.cargoes + ADD CONSTRAINT fk_cargoes_booking FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.cargoes + ADD CONSTRAINT fk_cargoes_wagon_allocation FOREIGN KEY (wagon_booking_allocation_id) REFERENCES freight.wagon_booking_allocations(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.containers + ADD CONSTRAINT fk_containers_booking FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.containers + ADD CONSTRAINT fk_containers_booking_container FOREIGN KEY (booking_container_id) REFERENCES freight.booking_container(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.containers + ADD CONSTRAINT fk_containers_wagon_allocation FOREIGN KEY (wagon_booking_allocation_id) REFERENCES freight.wagon_booking_allocations(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.fuel_consumption + ADD CONSTRAINT fk_fuel_consumption_vehicle FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.fuel_purchases + ADD CONSTRAINT fk_fuel_purchases_vehicle FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.invoice_lines + ADD CONSTRAINT fk_invoice_lines_invoice FOREIGN KEY (invoice_id) REFERENCES freight.invoices(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.invoices + ADD CONSTRAINT fk_invoices_company FOREIGN KEY (company_id) REFERENCES freight.companies(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.invoices + ADD CONSTRAINT fk_invoices_company_profile FOREIGN KEY (company_profile_id) REFERENCES freight.company_profiles(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.invoices + ADD CONSTRAINT fk_invoices_payment FOREIGN KEY (payment_id) REFERENCES freight.payments(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.maintenance_costs + ADD CONSTRAINT fk_maintenance_schedule FOREIGN KEY (maintenance_schedule_id) REFERENCES freight.maintenance_schedules(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.payment_refunds + ADD CONSTRAINT fk_payment_refunds_payment FOREIGN KEY (payment_id) REFERENCES freight.payments(id) ON DELETE RESTRICT; + +ALTER TABLE ONLY freight.train_schedule_bookings + ADD CONSTRAINT fk_train_schedule_bookings_booking FOREIGN KEY (booking_id) REFERENCES freight.bookings(id); + +ALTER TABLE ONLY freight.train_schedule_bookings + ADD CONSTRAINT fk_train_schedule_bookings_schedule FOREIGN KEY (train_schedule_id) REFERENCES freight.train_schedules(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.train_schedules + ADD CONSTRAINT fk_train_schedules_destination FOREIGN KEY (destination_station_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.train_schedules + ADD CONSTRAINT fk_train_schedules_origin FOREIGN KEY (origin_station_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.train_schedules + ADD CONSTRAINT fk_train_schedules_route FOREIGN KEY (route_id) REFERENCES freight.routes(id); + +ALTER TABLE ONLY freight.train_schedules + ADD CONSTRAINT fk_train_schedules_train_set FOREIGN KEY (train_set_id) REFERENCES freight.train_sets(id); + +ALTER TABLE ONLY freight.train_set_wagons + ADD CONSTRAINT fk_train_set_wagons_physical_wagon FOREIGN KEY (physical_wagon_id) REFERENCES freight.wagons(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.train_set_wagons + ADD CONSTRAINT fk_train_set_wagons_train_set FOREIGN KEY (train_set_id) REFERENCES freight.train_sets(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.train_set_wagons + ADD CONSTRAINT fk_train_set_wagons_wagon_type FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types(id); + +ALTER TABLE ONLY freight.train_sets + ADD CONSTRAINT fk_train_sets_locomotive FOREIGN KEY (locomotive_id) REFERENCES freight.locomotives(id); + +ALTER TABLE ONLY freight.vehicles + ADD CONSTRAINT fk_vehicles_truck_type FOREIGN KEY (truck_type_id) REFERENCES freight.truck_types(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagon_allocation_bulk_loads + ADD CONSTRAINT fk_wabl_allocation FOREIGN KEY (wagon_booking_allocation_id) REFERENCES freight.wagon_booking_allocations(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.wagon_allocation_bulk_loads + ADD CONSTRAINT fk_wabl_booking FOREIGN KEY (booking_id) REFERENCES freight.bookings(id); + +ALTER TABLE ONLY freight.wagon_allocation_bulk_loads + ADD CONSTRAINT fk_wabl_cargo_type FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagon_allocation_container_items + ADD CONSTRAINT fk_waci_allocation FOREIGN KEY (wagon_booking_allocation_id) REFERENCES freight.wagon_booking_allocations(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.wagon_allocation_container_items + ADD CONSTRAINT fk_waci_booking_container FOREIGN KEY (booking_container_id) REFERENCES freight.booking_container(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagon_allocation_container_items + ADD CONSTRAINT fk_waci_container FOREIGN KEY (container_id) REFERENCES freight.containers(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagon_allocation_container_items + ADD CONSTRAINT fk_waci_container_type FOREIGN KEY (container_type_id) REFERENCES freight.container_types(id); + +ALTER TABLE ONLY freight.wagon_booking_allocations + ADD CONSTRAINT fk_wagon_booking_allocations_booking FOREIGN KEY (booking_id) REFERENCES freight.bookings(id); + +ALTER TABLE ONLY freight.wagon_booking_allocations + ADD CONSTRAINT fk_wagon_booking_allocations_wagon FOREIGN KEY (train_set_wagon_id) REFERENCES freight.train_set_wagons(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.wagons + ADD CONSTRAINT fk_wagons_current_train_schedule FOREIGN KEY (current_train_schedule_id) REFERENCES freight.train_schedules(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagons + ADD CONSTRAINT fk_wagons_train_set_wagon FOREIGN KEY (train_set_wagon_id) REFERENCES freight.train_set_wagons(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagon_movements + ADD CONSTRAINT fk_wm_transfer_request FOREIGN KEY (transfer_request_id) REFERENCES freight.wagon_transfer_requests(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagon_transfer_requests + ADD CONSTRAINT fk_wtr_from_yard FOREIGN KEY (from_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.wagon_transfer_requests + ADD CONSTRAINT fk_wtr_to_yard FOREIGN KEY (to_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.wagon_transfer_requests + ADD CONSTRAINT fk_wtr_wagon_type FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types(id); + +ALTER TABLE ONLY freight.gps_devices + ADD CONSTRAINT gps_devices_vehicle_id_fkey FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id); + +ALTER TABLE ONLY freight.last_mile_vehicle_assignments + ADD CONSTRAINT last_mile_vehicle_assignments_last_mile_id_fkey FOREIGN KEY (last_mile_id) REFERENCES freight.last_mile(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.last_mile_vehicle_assignments + ADD CONSTRAINT last_mile_vehicle_assignments_vehicle_id_fkey FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id); + +ALTER TABLE ONLY freight.last_mile_vehicle_containers + ADD CONSTRAINT last_mile_vehicle_containers_assignment_id_fkey FOREIGN KEY (assignment_id) REFERENCES freight.last_mile_vehicle_assignments(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.maintenance_intervals + ADD CONSTRAINT maintenance_intervals_vehicle_id_fkey FOREIGN KEY (vehicle_id) REFERENCES freight.vehicles(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.priority_rule_change_requests + ADD CONSTRAINT priority_rule_change_requests_priority_config_id_fkey FOREIGN KEY (priority_config_id) REFERENCES freight.priority_configs(id); + +ALTER TABLE ONLY freight.rate_change_requests + ADD CONSTRAINT rate_change_requests_rate_id_fkey FOREIGN KEY (rate_id) REFERENCES freight.rates(id); + +ALTER TABLE ONLY freight.route_milestones + ADD CONSTRAINT route_milestones_route_id_fkey FOREIGN KEY (route_id) REFERENCES freight.routes(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.route_milestones + ADD CONSTRAINT route_milestones_yard_id_fkey FOREIGN KEY (yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.routes + ADD CONSTRAINT routes_destination_yard_id_fkey FOREIGN KEY (destination_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.routes + ADD CONSTRAINT routes_origin_yard_id_fkey FOREIGN KEY (origin_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.train_checkpoint_events + ADD CONSTRAINT train_checkpoint_events_train_schedule_id_fkey FOREIGN KEY (train_schedule_id) REFERENCES freight.train_schedules(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.train_set_wagons + ADD CONSTRAINT train_set_wagons_alight_yard_id_fkey FOREIGN KEY (alight_yard_id) REFERENCES freight.yards(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.train_set_wagons + ADD CONSTRAINT train_set_wagons_board_yard_id_fkey FOREIGN KEY (board_yard_id) REFERENCES freight.yards(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagon_movements + ADD CONSTRAINT wagon_movements_booking_id_fkey FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagon_movements + ADD CONSTRAINT wagon_movements_from_yard_id_fkey FOREIGN KEY (from_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.wagon_movements + ADD CONSTRAINT wagon_movements_to_yard_id_fkey FOREIGN KEY (to_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.wagon_movements + ADD CONSTRAINT wagon_movements_train_schedule_id_fkey FOREIGN KEY (train_schedule_id) REFERENCES freight.train_schedules(id) ON DELETE SET NULL; + +ALTER TABLE ONLY freight.wagon_movements + ADD CONSTRAINT wagon_movements_wagon_id_fkey FOREIGN KEY (wagon_id) REFERENCES freight.wagons(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.warehouse_inventory_movement + ADD CONSTRAINT warehouse_inventory_movement_inventory_id_fkey FOREIGN KEY (inventory_id) REFERENCES freight.warehouse_inventory(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.warehouse_inventory + ADD CONSTRAINT warehouse_inventory_warehouse_id_fkey FOREIGN KEY (warehouse_id) REFERENCES freight.warehouses(id); + +ALTER TABLE ONLY freight.warehouse_inventory + ADD CONSTRAINT warehouse_inventory_yard_id_fkey FOREIGN KEY (yard_id) REFERENCES freight.warehouse_yards(id); + +ALTER TABLE ONLY freight.warehouse_inventory + ADD CONSTRAINT warehouse_inventory_zone_id_fkey FOREIGN KEY (zone_id) REFERENCES freight.warehouse_zones(id); + +ALTER TABLE ONLY freight.warehouse_loadings + ADD CONSTRAINT warehouse_loadings_warehouse_inventory_id_fkey FOREIGN KEY (warehouse_inventory_id) REFERENCES freight.warehouse_inventory(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.warehouse_yard_cargo_types + ADD CONSTRAINT warehouse_yard_cargo_types_cargo_type_id_fkey FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.warehouse_yard_cargo_types + ADD CONSTRAINT warehouse_yard_cargo_types_yard_id_fkey FOREIGN KEY (yard_id) REFERENCES freight.warehouse_yards(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.warehouse_yards + ADD CONSTRAINT warehouse_yards_warehouse_id_fkey FOREIGN KEY (warehouse_id) REFERENCES freight.warehouses(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.warehouse_zones + ADD CONSTRAINT warehouse_zones_yard_id_fkey FOREIGN KEY (yard_id) REFERENCES freight.warehouse_yards(id) ON DELETE CASCADE; + +ALTER TABLE ONLY freight.yard_distances + ADD CONSTRAINT yard_distances_from_yard_id_fkey FOREIGN KEY (from_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.yard_distances + ADD CONSTRAINT yard_distances_to_yard_id_fkey FOREIGN KEY (to_yard_id) REFERENCES freight.yards(id); + +ALTER TABLE ONLY freight.yard_facilities + ADD CONSTRAINT yard_facilities_yard_id_fkey FOREIGN KEY (yard_id) REFERENCES freight.yards(id) ON DELETE CASCADE; +`; + +export class FreightBaseline3250000000000 implements MigrationInterface { + name = "FreightBaseline3250000000000"; + + public async up(queryRunner: QueryRunner): Promise { + // Databases migrated before the squash already have every object below. + if (await queryRunner.hasTable("freight.bookings")) return; + + await queryRunner.query(STRUCTURE_SQL); + for (const sql of Object.values(FREIGHT_SEED_SQL)) { + await queryRunner.query(sql); + } + await queryRunner.query(FOREIGN_KEYS_SQL); + } + + public async down(): Promise { + // Reverting the baseline means dropping the entire freight schema, which is + // never what a `migration:revert` intends. Drop the database instead. + throw new Error( + "FreightBaseline3250000000000 is a squashed baseline and cannot be reverted", + ); + } +} diff --git a/apps/edr-freight-api/src/migrations/3260000000000-InvoicePaymentProcessingStatus.ts b/apps/edr-freight-api/src/migrations/3260000000000-InvoicePaymentProcessingStatus.ts new file mode 100644 index 000000000..76fee7d22 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/3260000000000-InvoicePaymentProcessingStatus.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Adds PAYMENT_PROCESSING to the invoice status enum: the customer completed + * provider checkout (success redirect) and settlement is awaiting the + * provider webhook. + */ +export class InvoicePaymentProcessingStatus3260000000000 + implements MigrationInterface +{ + name = "InvoicePaymentProcessingStatus3260000000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'PAYMENT_PROCESSING' AFTER 'PENDING'`, + ); + } + + public async down(): Promise { + // Postgres cannot drop an enum value; PAYMENT_PROCESSING stays. Harmless. + } +} diff --git a/apps/edr-freight-api/src/migrations/3270000000000-YardGpsLocation.ts b/apps/edr-freight-api/src/migrations/3270000000000-YardGpsLocation.ts new file mode 100644 index 000000000..18e71ae4c --- /dev/null +++ b/apps/edr-freight-api/src/migrations/3270000000000-YardGpsLocation.ts @@ -0,0 +1,53 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Creates freight.yard_locations (GPS per yard, decimal degrees WGS84) and + * seeds the five facility yards: Sebeta, GMP/Indode (KALITY), Mojo, Adama, + * Dire Dawa. Also normalizes has_facility — only those five load/unload cargo. + * + * Coordinates are approximate — adjust rows directly if surveyed values arrive. + */ +export class YardGpsLocation3270000000000 implements MigrationInterface { + name = 'YardGpsLocation3270000000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS freight.yard_locations ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + yard_id uuid NOT NULL UNIQUE REFERENCES freight.yards(id) ON DELETE CASCADE, + latitude double precision NOT NULL, + longitude double precision NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz + ) + `); + + // LEGACY_DEST is the historical code for Sebeta in some environments. + await queryRunner.query(` + INSERT INTO freight.yard_locations (yard_id, latitude, longitude) + SELECT y.id, v.lat, v.lng + FROM (VALUES + ('SEBETA', 8.9096, 38.6360), + ('LEGACY_DEST', 8.9096, 38.6360), + ('KALITY', 8.7386, 38.7913), + ('MOJO', 8.5794, 39.1200), + ('ADAMA', 8.5622, 39.2440), + ('DIRE_DAWA', 9.6009, 41.8103) + ) AS v(code, lat, lng) + JOIN freight.yards y ON y.code = v.code AND y.deleted_at IS NULL + ON CONFLICT (yard_id) DO NOTHING + `); + + await queryRunner.query(` + UPDATE freight.yards + SET has_facility = (code IN ('SEBETA', 'LEGACY_DEST', 'KALITY', 'MOJO', 'ADAMA', 'DIRE_DAWA')) + WHERE deleted_at IS NULL + AND has_facility <> (code IN ('SEBETA', 'LEGACY_DEST', 'KALITY', 'MOJO', 'ADAMA', 'DIRE_DAWA')) + `); + } + + public async down(): Promise { + // Additive table + data normalization; no rollback. + } +} diff --git a/apps/edr-freight-api/src/migrations/3280000000000-LastMileRateBands.ts b/apps/edr-freight-api/src/migrations/3280000000000-LastMileRateBands.ts new file mode 100644 index 000000000..973ff00a4 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/3280000000000-LastMileRateBands.ts @@ -0,0 +1,68 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Last-mile rate bands: adds min_km/max_km to freight.rates so container + * last-mile rates can be one row per (container size × distance band), + * and extends UQ_rates_pattern with the band start so sibling bands don't + * collide. Existing rows all have NULL min_km (COALESCE → -1), so the + * uniqueness semantics for every current rate are unchanged. + */ +export class LastMileRateBands3280000000000 implements MigrationInterface { + name = 'LastMileRateBands3280000000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.rates ADD COLUMN IF NOT EXISTS min_km numeric(10,2) + `); + await queryRunner.query(` + ALTER TABLE freight.rates ADD COLUMN IF NOT EXISTS max_km numeric(10,2) + `); + + await queryRunner.query(` + DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'CK_rates_km_band' + AND conrelid = 'freight.rates'::regclass + ) THEN + ALTER TABLE freight.rates ADD CONSTRAINT "CK_rates_km_band" + CHECK (max_km IS NULL OR (min_km IS NOT NULL AND max_km > min_km)); + END IF; + END $$ + `); + + await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern"`); + await queryRunner.query(` + CREATE UNIQUE INDEX IF NOT EXISTS "UQ_rates_pattern" ON freight.rates USING btree ( + rate_type, + COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid), + COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid), + COALESCE(trade_direction, ''::character varying), + COALESCE(origin_yard_id, '00000000-0000-0000-0000-000000000000'::uuid), + COALESCE(destination_yard_id, '00000000-0000-0000-0000-000000000000'::uuid), + rate_unit, + COALESCE(min_km, '-1'::numeric) + ) WHERE ((deleted_at IS NULL) AND ((status)::text <> 'SUPERSEDED'::text)) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern"`); + await queryRunner.query(` + CREATE UNIQUE INDEX IF NOT EXISTS "UQ_rates_pattern" ON freight.rates USING btree ( + rate_type, + COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid), + COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid), + COALESCE(trade_direction, ''::character varying), + COALESCE(origin_yard_id, '00000000-0000-0000-0000-000000000000'::uuid), + COALESCE(destination_yard_id, '00000000-0000-0000-0000-000000000000'::uuid), + rate_unit + ) WHERE ((deleted_at IS NULL) AND ((status)::text <> 'SUPERSEDED'::text)) + `); + await queryRunner.query(` + ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_km_band" + `); + await queryRunner.query(`ALTER TABLE freight.rates DROP COLUMN IF EXISTS max_km`); + await queryRunner.query(`ALTER TABLE freight.rates DROP COLUMN IF EXISTS min_km`); + } +} diff --git a/apps/edr-freight-api/src/migrations/3290000000000-LastMileRequestContract.ts b/apps/edr-freight-api/src/migrations/3290000000000-LastMileRequestContract.ts new file mode 100644 index 000000000..d4971f903 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/3290000000000-LastMileRequestContract.ts @@ -0,0 +1,36 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Last-mile contract fields on freight.last_mile_requests: the customer-chosen + * delivery date, the chief-approved advance amount (held until the invoice is + * generated at signing time), the rate snapshot rendered into the contract, + * and the customer signature bookkeeping. The signed PDF and signature image + * live in freight.files (resource 'last_mile_requests'). + */ +export class LastMileRequestContract3290000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.last_mile_requests + ADD COLUMN IF NOT EXISTS requested_delivery_date date, + ADD COLUMN IF NOT EXISTS approved_advance_amount numeric(14,2), + ADD COLUMN IF NOT EXISTS contract_summary jsonb, + ADD COLUMN IF NOT EXISTS contract_generated_at timestamptz, + ADD COLUMN IF NOT EXISTS customer_signed_at timestamptz, + ADD COLUMN IF NOT EXISTS signer_display_name varchar(160), + ADD COLUMN IF NOT EXISTS consent_text text + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.last_mile_requests + DROP COLUMN IF EXISTS requested_delivery_date, + DROP COLUMN IF EXISTS approved_advance_amount, + DROP COLUMN IF EXISTS contract_summary, + DROP COLUMN IF EXISTS contract_generated_at, + DROP COLUMN IF EXISTS customer_signed_at, + DROP COLUMN IF EXISTS signer_display_name, + DROP COLUMN IF EXISTS consent_text + `); + } +} diff --git a/apps/edr-freight-api/src/modules/audit/audit.controller.ts b/apps/edr-freight-api/src/modules/audit/audit.controller.ts new file mode 100644 index 000000000..a7c8782b2 --- /dev/null +++ b/apps/edr-freight-api/src/modules/audit/audit.controller.ts @@ -0,0 +1,32 @@ +import { Controller, Get, Query } from "@nestjs/common"; +import { ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger"; + +import { BookingStaff } from "../../common/booking-guards"; +import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry"; +import { AuditService } from "./audit.service"; + +@ApiTags("audit") +@Controller("audit") +@BookingStaff(FREIGHT_PERMS.audit.view) +export class AuditController { + constructor(private readonly auditService: AuditService) {} + + @Get("logs") + @ApiOperation({ summary: "List freight-api audit log commands" }) + @ApiQuery({ name: "skip", type: Number, required: false }) + @ApiQuery({ name: "take", type: Number, required: false }) + list(@Query("skip") skip?: string, @Query("take") take?: string) { + // Same fallback chain @tria-plc/auditlog's client interceptor uses to + // stamp AuditLog.application (mezgeb/client/client-audit.interceptor.js) + // — reading it here instead of a hardcoded literal means this can't + // silently drift out of sync with whatever APPLICATION_NAME/APP_NAME + // actually is at runtime. + const application = + process.env.APPLICATION_NAME ?? process.env.APP_NAME ?? "DEFAULT"; + return this.auditService.list( + application, + skip !== undefined ? parseInt(skip, 10) : undefined, + take !== undefined ? parseInt(take, 10) : undefined, + ); + } +} diff --git a/apps/edr-freight-api/src/modules/audit/audit.module.ts b/apps/edr-freight-api/src/modules/audit/audit.module.ts new file mode 100644 index 000000000..635973fc6 --- /dev/null +++ b/apps/edr-freight-api/src/modules/audit/audit.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { AuditLogCommand } from "@tria-plc/auditlog"; + +import { AuditController } from "./audit.controller"; +import { AuditService } from "./audit.service"; + +@Module({ + imports: [TypeOrmModule.forFeature([AuditLogCommand])], + controllers: [AuditController], + providers: [AuditService], +}) +export class AuditModule {} diff --git a/apps/edr-freight-api/src/modules/audit/audit.service.ts b/apps/edr-freight-api/src/modules/audit/audit.service.ts new file mode 100644 index 000000000..04ea4beed --- /dev/null +++ b/apps/edr-freight-api/src/modules/audit/audit.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { AuditLogCommand } from "@tria-plc/auditlog"; + +import { CLIENT_APP_HEADER } from "../auth/login-audience.middleware"; + +export interface AuditLogListResult { + count: number; + items: AuditLogCommand[]; +} + +/** + * Own read path onto @tria-plc/auditlog's tables, gated by AuditController's + * @BookingStaff — the package's own AuditLogCommandController (mounted at + * /api/audit-log-commands) ships with no guards at all, so it can't be used + * directly for a permission-gated UI. Query mirrors the package's + * AuditLogCommandService.buildAuditLogQuery/getAllAuditLogs exactly. + */ +@Injectable() +export class AuditService { + constructor( + @InjectRepository(AuditLogCommand) + private readonly auditLogCommandRepository: Repository, + ) {} + + async list( + application: string, + skip = 0, + take = 10, + ): Promise { + const [items, count] = await this.auditLogCommandRepository + .createQueryBuilder("audit_log_commands") + .leftJoinAndSelect("audit_log_commands.auditLog", "auditLog") + .andWhere( + "(audit_log_commands.auditLogId IS NULL OR auditLog.application = :application)", + { application }, + ) + .andWhere( + "(audit_log_commands.auditLogId IS NULL OR auditLog.status = :status)", + { status: "Commit" }, + ) + // Backoffice-only view: portal (customer-facing) writes carry the same + // request-header set by every axios call from that app — see + // login-audience.middleware.ts. Rows with no linked auditLog (child/ + // event commands with no request context) stay visible; they aren't + // attributable to any frontend, so they're not portal noise either. + .andWhere( + "(audit_log_commands.auditLogId IS NULL OR auditLog.requestHeader ->> :clientAppHeader = :clientApp)", + { clientAppHeader: CLIENT_APP_HEADER, clientApp: "backoffice" }, + ) + .select([ + "audit_log_commands.id", + "audit_log_commands.createdAt", + "audit_log_commands.deletedAt", + "audit_log_commands.entityName", + "audit_log_commands.queryMethod", + "audit_log_commands.changes", + "audit_log_commands.payload", + "auditLog.id", + "auditLog.user", + ]) + .addOrderBy("audit_log_commands.createdAt", "DESC") + .skip(skip) + .take(take) + .getManyAndCount(); + + return { count, items }; + } +} diff --git a/apps/edr-freight-api/src/modules/auth/dto/list-users-query.dto.ts b/apps/edr-freight-api/src/modules/auth/dto/list-users-query.dto.ts new file mode 100644 index 000000000..2325568e9 --- /dev/null +++ b/apps/edr-freight-api/src/modules/auth/dto/list-users-query.dto.ts @@ -0,0 +1,38 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { EUserStatus, EUserType } from '@tria-plc/api-common/utils/enums/user.enum'; +import { Transform, TransformFnParams } from 'class-transformer'; +import { IsBoolean, IsEnum, IsIn, IsOptional } from 'class-validator'; + +import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto'; + +/** Query-string booleans arrive as strings; implicit conversion is off app-wide. */ +const toOptionalBoolean = ({ value }: TransformFnParams): boolean | undefined => + value === undefined || value === null || value === '' + ? undefined + : value === true || value === 'true'; + +export class ListUsersQueryDto extends PaginationQueryDto { + @ApiPropertyOptional({ enum: EUserType }) + @IsOptional() + @IsEnum(EUserType) + userType?: EUserType; + + @ApiPropertyOptional({ enum: EUserStatus }) + @IsOptional() + @IsEnum(EUserStatus) + userStatus?: EUserStatus; + + @ApiPropertyOptional({ description: 'Filter by active flag.' }) + @IsOptional() + @Transform(toOptionalBoolean) + @IsBoolean() + isActive?: boolean; + + @ApiPropertyOptional({ + enum: ['username', 'email', 'createdAt'], + default: 'username', + }) + @IsOptional() + @IsIn(['username', 'email', 'createdAt']) + sortBy?: string; +} diff --git a/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts b/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts index 10dbd0b37..ff8f803b9 100644 --- a/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts +++ b/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts @@ -19,6 +19,8 @@ import { ForgotPasswordController } from './forgot-password.controller'; import { ForgotPasswordService } from './forgot-password.service'; import { FreightMeController } from './freight-me.controller'; import { FreightMeService } from './freight-me.service'; +import { ListUsersController } from './list-users.controller'; +import { ListUsersService } from './list-users.service'; @Module({ imports: [ @@ -39,8 +41,10 @@ import { FreightMeService } from './freight-me.service'; CheckAvailabilityController, ForgotPasswordController, CustomerResetController, + ListUsersController, ], providers: [ + ListUsersService, FreightMeService, AccountService, CheckAvailabilityService, diff --git a/apps/edr-freight-api/src/modules/auth/list-users.controller.ts b/apps/edr-freight-api/src/modules/auth/list-users.controller.ts new file mode 100644 index 000000000..e7fbfd771 --- /dev/null +++ b/apps/edr-freight-api/src/modules/auth/list-users.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { ListUsersQueryDto } from './dto/list-users-query.dto'; +import { ListUsersService } from './list-users.service'; +import { StaffReference } from '../../common/booking-guards'; + +@ApiTags('auth') +@Controller('staff/users') +@ApiBearerAuth() +export class ListUsersController { + constructor(private readonly service: ListUsersService) {} + + @Get() + @StaffReference() + @ApiOperation({ + summary: 'List IAM users (paginated) for backoffice pickers', + }) + findAll(@Query() query: ListUsersQueryDto) { + return this.service.findAll(query); + } +} diff --git a/apps/edr-freight-api/src/modules/auth/list-users.service.ts b/apps/edr-freight-api/src/modules/auth/list-users.service.ts new file mode 100644 index 000000000..cf7e22be2 --- /dev/null +++ b/apps/edr-freight-api/src/modules/auth/list-users.service.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { PaginatedResponse } from '@edr/types'; +import { User } from '@tria-plc/iamapi-common/entities/iam/user/user.entity'; +import { Repository } from 'typeorm'; + +import { ListUsersQueryDto } from './dto/list-users-query.dto'; +import { paginateQuery } from '../../common/utils/pagination.util'; + +/** + * Read-only listing of `iam.users` for backoffice pickers. + * + * Exists because `@tria-plc/iamapi-common@1.0.0`'s `GET /users/filter` pairs a + * `@QueryParams()` pagination DTO with a plain `@Query()` DTO that does not + * declare `skip`/`take`/`orderBy`; the global whitelist pipe then 400s on the + * very params the route's own paginator reads. Drop this once IAM ships a fix. + */ +@Injectable() +export class ListUsersService { + constructor( + @InjectRepository(User) private readonly users: Repository, + ) {} + + findAll(query: ListUsersQueryDto): Promise> { + const sortBy = query.sortBy ?? 'username'; + const qb = this.users + .createQueryBuilder('user') + // Explicit select: never widen this to `user` — the entity's lazy + // relations include credentials and sessions. + .select([ + 'user.id', + 'user.name', + 'user.username', + 'user.email', + 'user.phoneNumber', + 'user.userType', + 'user.status', + 'user.isActive', + 'user.createdAt', + ]) + .orderBy(`user.${sortBy}`, query.sortOrder ?? 'ASC'); + + if (query.userType) { + qb.andWhere('user.userType = :userType', { userType: query.userType }); + } + if (query.userStatus) { + qb.andWhere('user.status = :userStatus', { userStatus: query.userStatus }); + } + if (query.isActive !== undefined) { + qb.andWhere('user.isActive = :isActive', { isActive: query.isActive }); + } + if (query.search) { + // `name` is localized jsonb ({ en, am, … }), not a string — match its + // values rather than casting the whole object to text. + qb.andWhere( + `(user.username ILIKE :search + OR user.email ILIKE :search + OR user.phone_number ILIKE :search + OR EXISTS ( + SELECT 1 FROM jsonb_each_text(user.name) AS n(k, v) + WHERE n.v ILIKE :search + ))`, + { search: `%${query.search}%` }, + ); + } + + return paginateQuery(qb, query); + } +} diff --git a/apps/edr-freight-api/src/modules/billing/billing.service.spec.ts b/apps/edr-freight-api/src/modules/billing/billing.service.spec.ts index dbf2bd4fd..1356f8cfa 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.service.spec.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.service.spec.ts @@ -203,6 +203,129 @@ describe("BillingService.markInvoiceAsPaid", () => { }); }); +/** + * A gateway success can land after the pay window AND its drain tail (relay + * backlog, payment-api restart, a CBE bill paid at a counter). The money is + * captured either way, so the settle lookup must accept an EXPIRED invoice — + * matching only OPEN_STATUSES used to drop it silently, leaving a debited + * customer with an EXPIRED invoice, an EXPIRED booking and no alert. + */ +describe("BillingService.settleByPaymentId", () => { + function serviceFor(invoice: Record | null) { + const mg = { + findOne: jest.fn().mockResolvedValue(invoice), + update: jest.fn().mockResolvedValue(undefined), + }; + const events = makeEvents(); + // The lookup is by paymentId ALONE — status is judged on the resolved row, + // so a stale capture can never skip past a newer invoice to an older one. + const findOne = jest.fn(({ where }: { where: Record }) => { + expect(where).toEqual({ paymentId: "pay-1" }); + return Promise.resolve(invoice); + }); + const dataSource = { + getRepository: () => ({ findOne }), + transaction: (cb: (mg: unknown) => unknown) => cb(mg), + manager: mg, + }; + const service = new BillingService( + dataSource as never, + {} as never, + {} as never, + events as never, + {} as never, // payment + {} as never, // companies + {} as never, // invoiceDocuments + ); + return { service, mg, events }; + } + + it("settles an EXPIRED invoice — the money was already captured", async () => { + const { service, mg, events } = serviceFor({ + id: "inv-1", + status: Freight.InvoiceStatus.Expired, + source: "booking", + sourceId: "booking-1", + totalAmount: 1500, + paidAt: null, + }); + + const settled = await service.settleByPaymentId("pay-1", "txn-1"); + + expect(settled?.status).toBe(Freight.InvoiceStatus.Paid); + expect(mg.update).toHaveBeenCalledWith( + expect.anything(), + { id: "inv-1" }, + expect.objectContaining({ + status: Freight.InvoiceStatus.Paid, + paidAmount: 1500, + balanceAmount: 0, + }), + ); + // The domain reacts to this — it is what revives the expired booking. + expect(events.emitAsync).toHaveBeenCalledWith( + "booking.invoice.paid", + expect.objectContaining({ invoiceId: "inv-1" }), + ); + }); + + it("still settles an open (PENDING) invoice", async () => { + const { service, mg } = serviceFor({ + id: "inv-1", + status: Freight.InvoiceStatus.Pending, + source: "booking", + sourceId: "booking-1", + totalAmount: 1500, + paidAt: null, + }); + + await service.settleByPaymentId("pay-1"); + + expect(mg.update).toHaveBeenCalled(); + }); + + /** + * `upsertIntent` keeps ONE local payments row per booking reference, so every + * invoice the booking was ever charged on carries the same `paymentId`. A + * capture from a lapsed first attempt must not reach back past the invoice the + * customer actually paid and settle the older EXPIRED one — that would mark two + * invoices paid off a single payment. + */ + it("no-ops when the booking's newest invoice is already PAID", async () => { + const { service, mg, events } = serviceFor({ + id: "inv-2", + status: Freight.InvoiceStatus.Paid, + source: "booking", + sourceId: "booking-1", + totalAmount: 1500, + }); + + expect(await service.settleByPaymentId("pay-1")).toBeNull(); + expect(mg.update).not.toHaveBeenCalled(); + expect(events.emitAsync).not.toHaveBeenCalled(); + }); + + it.each([ + ["CANCELLED", Freight.InvoiceStatus.Cancelled], + ["REFUNDED", Freight.InvoiceStatus.Refunded], + ])("does not settle a %s invoice — that is a refund case", async ( + _label, + status, + ) => { + const { service, mg, events } = serviceFor({ + id: "inv-1", + status, + source: "booking", + sourceId: "booking-1", + totalAmount: 1500, + }); + + expect(await service.settleByPaymentId("pay-1")).toBeNull(); + expect(mg.update).not.toHaveBeenCalled(); + expect(events.emitAsync).not.toHaveBeenCalled(); + }); +}); + describe("BillingService.recordPayment", () => { function serviceFor(invoice: Record | null) { const mg = { diff --git a/apps/edr-freight-api/src/modules/billing/billing.service.ts b/apps/edr-freight-api/src/modules/billing/billing.service.ts index 501bc8ee8..ea4413638 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.service.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.service.ts @@ -10,6 +10,7 @@ import { import { EventEmitter2 } from "@nestjs/event-emitter"; import { DataSource, EntityManager, In } from "typeorm"; +import { Booking } from "../bookings/entities/booking.entity"; import { CompaniesService } from "../companies/companies.service"; import { applyBookingRefDirectionScope } from "../user-trade-access/trade-scope.util"; import { PaymentService } from "../payment/payment.service"; @@ -52,6 +53,8 @@ const DEFAULT_DUE_DAYS = 14; const OPEN_STATUSES: Freight.InvoiceStatus[] = [ Freight.InvoiceStatus.Issued, Freight.InvoiceStatus.Pending, + // Success-redirect ack; still unsettled, so it must stay payable/settleable. + Freight.InvoiceStatus.PaymentProcessing, Freight.InvoiceStatus.PartiallyPaid, Freight.InvoiceStatus.Overdue, ]; @@ -924,6 +927,26 @@ export class BillingService { }); if (!invoice) return null; + // Reconcile-before-expire, caller-proof: an invoice with a payment intent may + // have settled at the gateway without the webhook landing yet. `paid` — leave + // it open, the (re-emitted) payment.succeeded settles it. `unverifiable` — + // never expire on unknown; the caller's next sweep retries. Invoices with no + // intent (`paymentId` null) were never payable at a gateway and expire directly. + if (invoice.paymentId) { + const { paid, unverifiable } = await this.reconcilePayable( + invoice.sourceId, + ); + if (paid || unverifiable) { + this.logger.warn( + `expirePayable skipped for invoice ${invoice.invoiceNumber} (${invoice.id}) — ` + + (paid + ? "gateway reconcile found a settled payment" + : "settlement unverifiable at the gateway"), + ); + return null; + } + } + return this.transition( invoice.id, Freight.InvoiceStatus.Expired, @@ -1028,6 +1051,57 @@ export class BillingService { ); } + /** + * Success-redirect ack (see PaymentService.acknowledgeSuccessRedirect): move + * the invoice linked to a gateway intent to PAYMENT_PROCESSING. Only from + * ISSUED/PENDING — never overwrites a settlement (PAID/PARTIALLY_PAID) and + * is idempotent. Balance untouched: this is a display state, not a + * settlement; settleByPaymentId still performs the real transition. + */ + async markInvoicePaymentProcessing(paymentId: string): Promise { + const repo = this.dataSource.getRepository(Invoice); + const invoices = await repo.findBy({ + paymentId, + status: In([ + Freight.InvoiceStatus.Issued, + Freight.InvoiceStatus.Pending, + ]), + }); + for (const invoice of invoices) { + await repo.update( + { id: invoice.id, status: invoice.status }, + { status: Freight.InvoiceStatus.PaymentProcessing }, + ); + this.emitInvoiceEvent("payment-processing", { + ...invoice, + status: Freight.InvoiceStatus.PaymentProcessing, + } as Invoice); + } + } + + /** + * Counterpart of {@link markInvoicePaymentProcessing} for a failed intent: + * PAYMENT_PROCESSING → PENDING so the invoice reads payable again for a + * retry. No-op from any other status. + */ + async revertInvoicePaymentProcessing(paymentId: string): Promise { + const repo = this.dataSource.getRepository(Invoice); + const invoices = await repo.findBy({ + paymentId, + status: Freight.InvoiceStatus.PaymentProcessing, + }); + for (const invoice of invoices) { + await repo.update( + { id: invoice.id, status: Freight.InvoiceStatus.PaymentProcessing }, + { status: Freight.InvoiceStatus.Pending }, + ); + this.emitInvoiceEvent("payment-processing-reverted", { + ...invoice, + status: Freight.InvoiceStatus.Pending, + } as Invoice); + } + } + // ── Payment initiation & settlement (the gateway boundary) ─────────────────── /** @@ -1136,6 +1210,17 @@ export class BillingService { .getRepository(Invoice) .update({ id: invoice.id }, { paymentId: result.intentId }); + // CBE_BILL: the bill reference IS the booking's PNR — the number the customer pays against + // at any CBE channel. Persist it on the booking so it survives the initiate response and + // shows on the booking/contract everywhere. The payment service reissues the same reference + // while the bill stays open, so re-initiating overwrites with an identical value. + const billReference = result.response.clientAction?.billReference; + if (billReference && invoice.source === Freight.InvoiceSource.Booking) { + await this.dataSource + .getRepository(Booking) + .update({ id: invoice.sourceId }, { pnrCode: billReference }); + } + // Settlement is driven by the payment API (webhook/outbox → payment.succeeded); // billing must not simulate it. Kept for local demos only. // An OTP intent (CAC Bank) is NOT paid yet — the payer still has to enter the @@ -1173,6 +1258,25 @@ export class BillingService { * `paymentId`, marks it paid, and emits `${source}.invoice.paid` for the domain * to advance on. Idempotent — no-op when no open invoice is linked (already * settled, or settled inline by {@link payInvoice}). + * + * EXPIRED is settleable HERE and only here: this is the gateway path, so the + * money is already captured and we are recording a fait accompli. A success can + * land after the pay window plus its drain tail (relay backlog, payment-api + * restart, a CBE bill paid at a counter) — matching only `OPEN_STATUSES` used to + * drop it silently, leaving a debited customer with an EXPIRED invoice and no + * alert. The manual/offline path ({@link recordPayment}) keeps its EXPIRED guard: + * a teller must not accept cash against a lapsed invoice. + * + * The status is checked on the RESOLVED invoice, never inside the lookup. + * `paymentId` is freight's local intent projection, and `upsertIntent` keeps ONE + * row per booking reference across every pay attempt — so a booking that was + * re-invoiced after a lapsed attempt has SEVERAL invoices carrying the same + * `paymentId`. Filtering by status inside the query would let a late capture from + * attempt 1 skip past the already-PAID attempt-2 invoice and settle the older + * EXPIRED one, marking two invoices paid off a single capture. Resolving the + * newest invoice first and then asking whether IT is settleable makes the answer + * "this booking's money is already recorded" instead. CANCELLED/REFUNDED are + * refund cases, not settlements, and are logged rather than settled. */ async settleByPaymentId( paymentId: string, @@ -1180,11 +1284,31 @@ export class BillingService { paidAt?: Date, ): Promise { const invoice = await this.dataSource.getRepository(Invoice).findOne({ - where: { paymentId, status: In(OPEN_STATUSES) }, - order: { issuedAt: "DESC" }, + where: { paymentId }, + // NULLS LAST: a DRAFT invoice has no issuedAt and Postgres sorts NULLs + // first on DESC, which would hand back an unissued invoice. + order: { issuedAt: { direction: "DESC", nulls: "LAST" } }, }); if (!invoice) return null; + const settleable: Freight.InvoiceStatus[] = [ + ...OPEN_STATUSES, + Freight.InvoiceStatus.Expired, + ]; + if (!settleable.includes(invoice.status)) { + // Already PAID is the ordinary idempotent no-op (redelivery, or settled + // inline by payInvoice). Anything else means money was captured with + // nowhere to land — that needs a person, so say so loudly. + if (invoice.status !== Freight.InvoiceStatus.Paid) { + this.logger.error( + `Payment ${paymentId} succeeded but invoice ${invoice.invoiceNumber} ` + + `(${invoice.id}) is ${invoice.status} — nothing settled. The capture ` + + `needs a refund or a manual settlement.`, + ); + } + return null; + } + return this.markInvoiceAsPaid(invoice.id, paymentId, undefined, { providerTxnId, paidAt, diff --git a/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts index 82bb1144e..cd803d719 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-invoice.service.ts @@ -102,6 +102,10 @@ export class BookingInvoiceService { ); switch (payload.type) { case "PREPAID": + // Before advancing: if this invoice belonged to a partial offer that + // lapsed before the settlement landed, revive it, or the booking boards + // whole having paid only the offered part. + await this.bookingBatch.reviveOfferForInvoice(payload.invoiceId); await this.advanceBookingOnPayment(payload.sourceId); break; default: @@ -111,6 +115,34 @@ export class BookingInvoiceService { } } + /** + * Success-redirect ack: the customer finished provider checkout, webhook not + * in yet. Mirror the invoice's PAYMENT_PROCESSING on the booking so the + * portal stops offering "Pay now". Display state only — settlement + * (`booking.invoice.paid`) still drives PAID. Status-guarded, so it never + * touches a booking that already advanced or was terminated. + */ + @OnEvent("booking.invoice.payment-processing") + async onBookingInvoicePaymentProcessing( + payload: InvoiceEventPayload, + ): Promise { + await this.dataSource.getRepository(Booking).update( + { id: payload.sourceId, status: "SELECTED_FOR_BATCH" }, + { status: "PAYMENT_VERIFICATION_IN_PROGRESS" }, + ); + } + + /** Payment failed after a redirect ack — the booking reads payable again. */ + @OnEvent("booking.invoice.payment-processing-reverted") + async onBookingInvoicePaymentProcessingReverted( + payload: InvoiceEventPayload, + ): Promise { + await this.dataSource.getRepository(Booking).update( + { id: payload.sourceId, status: "PAYMENT_VERIFICATION_IN_PROGRESS" }, + { status: "SELECTED_FOR_BATCH" }, + ); + } + updateStatus( invoiceId: string, status: Freight.InvoiceStatus, @@ -164,15 +196,24 @@ export class BookingInvoiceService { // may have moved on or been terminated between invoicing and settlement. // Only advance one that is still awaiting payment: no-op when already PAID, // and refuse to advance a booking in a terminal/advanced status - // (CANCELLED/REJECTED/EXPIRED or already past the payment gate) so we never - // rewrite its status or re-run allocation. + // (CANCELLED/REJECTED or already past the payment gate) so we never rewrite + // its status or re-run allocation. + // + // EXPIRED is NOT in that list: settlement is async, so a payment can land + // after the pay window and its drain tail (relay backlog, payment-api + // restart, a CBE bill paid at a counter). The money was captured, so it gets + // exactly the same treatment as an in-window payment — the booking becomes + // PAID and ensurePaidBookingAllocated re-places it via + // replaceStrandedPaidBooking (a same-day train with room, or a manual-assign + // log). Leaving EXPIRED here debited the customer for nothing. CANCELLED and + // REJECTED stay: a person terminated those, so a payment against them is a + // refund case, not a boarding. if (booking.paymentStatus === "PAID" || booking.status === "PAID") { return; } const TERMINAL_OR_ADVANCED_STATUSES: string[] = [ "CANCELLED", "REJECTED", - "EXPIRED", "IN_TRANSIT", "ARRIVED", "COMPLETED", diff --git a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts index 4996f45a9..75a179b5c 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts @@ -87,6 +87,33 @@ describe('BookingPricingService — domestic corridor', () => { expect(result.lineItems[0].amount).toBe(Math.round(35 * 120 * MOCK_CBE_RATE)); }); + it('keeps the exchange rate decimals — ETB amounts round to cents, not whole birr', async () => { + exchangeService.getRate.mockResolvedValue(162.2132); + const booking = { + id: 'b-1-frac', + freightType: 'BULK', + tradeDirection: 'DOMESTIC', + paymentCurrency: 'ETB', + cargoTotalWeightVgm: 120, + originYardId: MOJO, + destinationYardId: DIRE, + bookingContainers: [], + } as unknown as Booking; + + const result = await ( + service as unknown as { + computeBaseRailLinesWithRates: ( + b: Booking, + input: { containers: [] }, + ) => Promise<{ lineItems: Array<{ amount: number }> }>; + } + ).computeBaseRailLinesWithRates(booking, { containers: [] }); + + // 35 × 120 × 162.2132 = 681,295.44 — the .44 must survive (whole-birr + // rounding here billed with the integer part of the rate, in effect). + expect(result.lineItems[0].amount).toBe(681295.44); + }); + it('prices domestic bulk in USD using INTERCITY_BULK USD rate directly', async () => { const booking = { id: 'b-1-usd', diff --git a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts index b44c1c5e0..df81fd5f6 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts @@ -6,6 +6,7 @@ import { RatesService } from '../rule-engine/services/rates.service'; import { Rate } from '../rule-engine/entities/rate.entity'; import { isBulkQuantityUnit } from '../rule-engine/entities/rate-unit.util'; import { ContractRateSnapshot } from '../contracts/entities/contract-rate-snapshot.entity'; +import { round2 } from '../billing/invoice-settlement.util'; import { ExchangeService } from '@edr/api-common'; import { AppliedCargoModifier, @@ -16,7 +17,7 @@ import { containersPerWagonForSize, wagonsPerUnitForSize, } from '../rule-engine/container-type.util'; -import { bulkItemWagonsForAllowedTypes } from '../train-scheduling/train-capacity.util'; +import { bulkWagonsForAllowedTypes } from '../train-scheduling/train-capacity.util'; import { BookingsRepository } from './bookings.repository'; import { wagonRemainder } from './consolidation.service'; import { GeneratePriceResponseDto, PriceLineItemDto } from './dto/generate-price-response.dto'; @@ -205,14 +206,14 @@ export class BookingPricingService { const unitAmount = frozen ? Number(frozen.unitPrice) : isEtbBooking - ? Math.round(unitUsd * usdToEtb) + ? round2(unitUsd * usdToEtb) : unitUsd; const convertedAmount = frozen ? isEtbBooking - ? Math.round(unitAmount * quantity) + ? round2(unitAmount * quantity) : unitAmount * quantity : isEtbBooking - ? Math.round(usdAmount * usdToEtb) + ? round2(usdAmount * usdToEtb) : usdAmount; const item: PriceLineItemDto = { @@ -583,8 +584,8 @@ export class BookingPricingService { } else { const unitUsd = Number(rate!.rateValue); const usdAmount = this.amountForRate(rate!, container.quantity, lineWagons); - amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount; - unitAmount = isEtbBooking ? Math.round(unitUsd * usdToEtb) : unitUsd; + amount = isEtbBooking ? round2(usdAmount * usdToEtb) : usdAmount; + unitAmount = isEtbBooking ? round2(unitUsd * usdToEtb) : unitUsd; } if (rate) usedRatesMap.set(rate.id, rate); lines.push({ @@ -652,8 +653,8 @@ export class BookingPricingService { ); } else { const usdAmount = this.amountForRate(fallback, quantity, wagonCount); - amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount; - unitAmount = isEtbBooking ? Math.round(unitUsd * usdToEtb) : unitUsd; + amount = isEtbBooking ? round2(usdAmount * usdToEtb) : usdAmount; + unitAmount = isEtbBooking ? round2(unitUsd * usdToEtb) : unitUsd; } lines.push({ code: rateType, @@ -726,8 +727,16 @@ export class BookingPricingService { for (const leg of legs) { if (!leg.active) continue; + // New-style last-mile rates (PER_TON_KM bulk / distance-banded PER_KM) + // price the operational leg via last-mile-charge.util, not the booking + // quote — this legacy lookup must never pick one up. const rate = liveRates.find( - (r) => r.rateType === leg.rateType && r.currency === 'USD' && r.status === 'LIVE', + (r) => + r.rateType === leg.rateType && + r.currency === 'USD' && + r.status === 'LIVE' && + r.rateUnit !== 'PER_TON_KM' && + r.minKm == null, ); if (!rate) continue; @@ -763,12 +772,12 @@ export class BookingPricingService { if (frozen) { unitAmount = Number(frozen.unitPrice); amount = isEtbBooking - ? Math.round(unitAmount * quantity) + ? round2(unitAmount * quantity) : unitAmount * quantity; } else { const usdAmount = value * quantity; - amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount; - unitAmount = isEtbBooking ? Math.round(value * usdToEtb) : value; + amount = isEtbBooking ? round2(usdAmount * usdToEtb) : usdAmount; + unitAmount = isEtbBooking ? round2(value * usdToEtb) : value; } // Skip legs that resolve to nothing (zero rate, or zero km / count / tons). if (!(amount > 0)) continue; @@ -971,7 +980,7 @@ export class BookingPricingService { if (!(usdToEtb > 0)) return null; const converted = snap.currency === 'USD' && bookingCurrency === 'ETB' - ? Math.round(unitPrice * usdToEtb) + ? round2(unitPrice * usdToEtb) : snap.currency === 'ETB' && bookingCurrency === 'USD' ? unitPrice / usdToEtb : null; @@ -1027,7 +1036,7 @@ export class BookingPricingService { const currency = booking.paymentCurrency; const isEtb = currency === 'ETB'; const usdToEtb = isEtb ? await this.exchangeService.getRate('USD', 'ETB') : 1; - const convert = (usd: number): number => (isEtb ? Math.round(usd * usdToEtb) : usd); + const convert = (usd: number): number => (isEtb ? round2(usd * usdToEtb) : usd); const onLeg = liveRates.filter( (r) => @@ -1190,8 +1199,10 @@ export class BookingPricingService { // Break-bulk (PER_ITEM): `tons` above is the item count; size by // indivisible items instead of pretending the count is tonnage. Best // count across allowed wagon types, each capped by its items-fit. - const byItems = bulkItemWagonsForAllowedTypes(booking, cargo, capacity); - if (byItems > 0) return byItems; + // PER_TON cargo may cap tons per wagon below the rating (sugar 50T on a + // 70T wagon) — 200T then prices 4 wagons, not 3. + const byWagons = bulkWagonsForAllowedTypes(booking, cargo, capacity); + if (byWagons > 0) return byWagons; return Math.max(1, Math.ceil(tons / capacity)); } catch { return null; diff --git a/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts index 149875e6e..181c47b2d 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts @@ -927,13 +927,6 @@ export class BookingTransitionService { "OPERATION_CHANGES_REQUESTED", ]); - // A company sitting on another unpaid hold commits nothing new — this is - // the moment export capacity locks, so the lock applies here too. - // Government bookings allocate without paying and are exempt. - if (!booking.isGovernment) { - await this.bookingsService.assertNoUnpaidHold(booking.companyId); - } - // A bare initiated instance (clearance-first flow) carries no cargo or // price — it must go through the contract completion endpoint, which // persists cargo, prices, invoices and only then lands here itself. diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts index 2063bb01d..84354d860 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts @@ -1,8 +1,8 @@ import { Module, forwardRef } from "@nestjs/common"; import { UserTradeAccessModule } from "../user-trade-access/user-trade-access.module"; -import { ConfigService } from "@nestjs/config"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { ExchangeModule, ExchangeOptions } from "@edr/api-common"; + +import { registerExchangeModule } from "../exchange-settings/exchange-module-options"; // import { CustomersModule } from '../customers/customers.module'; import { CompaniesModule } from '../companies/companies.module'; @@ -86,11 +86,7 @@ import { VehiclesModule } from "../vehicles/vehicles.module"; RuleEngineModule, FileUploadSettingsModule, SignaturesModule, - ExchangeModule.forRootAsync({ - inject: [ConfigService], - useFactory: (config: ConfigService): ExchangeOptions => - config.get("app.cbeExchange") ?? {}, - }), + registerExchangeModule(), ], controllers: [BookingsController], providers: [ diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts index 185a6aaa7..1c5cc7481 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts @@ -1396,16 +1396,6 @@ export class BookingsRepository extends BaseRepository { .getMany(); } - /** Open unpaid holds (wagons reserved, pay window running) for a company. */ - countUnpaidHoldsForCompany(companyId: string): Promise { - return this.repository.count({ - where: { - companyId, - status: In(['SELECTED_FOR_BATCH', 'AWAITING_PAYMENT']), - }, - }); - } - /** Bookings currently reserved (SELECTED_FOR_BATCH) against a schedule. */ findReservedForSchedule(scheduleId: string): Promise { return this.repository @@ -1415,7 +1405,9 @@ export class BookingsRepository extends BaseRepository { .leftJoinAndSelect('bookingContainer.containerType', 'containerType') .leftJoinAndSelect('booking.cargoType', 'cargoType') .where('booking.train_schedule_id = :scheduleId', { scheduleId }) - .andWhere(`booking.status IN ('SELECTED_FOR_BATCH', 'AWAITING_PAYMENT')`) + .andWhere( + `booking.status IN ('SELECTED_FOR_BATCH', 'AWAITING_PAYMENT', 'PAYMENT_VERIFICATION_IN_PROGRESS')`, + ) .getMany(); } diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index 67ac73a58..12c56ac13 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -28,6 +28,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { DataSource, In } from 'typeorm'; import { deriveTradeDirection } from '../../common/derive-trade-direction.util'; +import { assertExportReceivedWithGrn } from '../../common/export-received-gate'; import { Yard } from '../rule-engine/entities/yard.entity'; import { ServiceType } from '../rule-engine/entities/service-type.entity'; import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity'; @@ -239,6 +240,10 @@ export class BookingsService { */ async carriageAcceptanceSheet(bookingId: string): Promise<{ filename: string; buffer: Buffer }> { const booking = await this.findById(bookingId); + // The sheet attests that EDR has taken custody. For export that happens at + // cargo receipt (GRN), so the GRN is required even when wagons are already + // allocated — an allocation is a plan, not possession. + await assertExportReceivedWithGrn(this.dataSource, booking); let wagons: CarriageAcceptanceWagonRow[] = await this.dataSource.query( `SELECT tsw.sequence_no AS "sequenceNo", COALESCE(wt.code, wt.name) AS "wagonType", @@ -291,7 +296,10 @@ export class BookingsService { LEFT JOIN freight.containers c ON c.id = inv.container_id AND c.deleted_at IS NULL WHERE inv.booking_id = $1 AND inv.deleted_at IS NULL - AND COALESCE(NULLIF(TRIM(inv.grn_number), ''), '') <> '' + AND COALESCE( + NULLIF(TRIM(inv.grn_number), ''), + substring(inv.notes FROM 'GRN Number: ([^\\n\\r]+)') + ) IS NOT NULL ORDER BY inv.created_at`, [bookingId], ) @@ -909,24 +917,6 @@ export class BookingsService { return result.booking; } - /** - * A company with an open unpaid hold (SELECTED_FOR_BATCH — wagons reserved, - * pay window running) may not take more capacity until it pays or the hold - * dies: otherwise one customer can lock a train's wagons over and over - * without ever paying. EXPIRED / CANCELLED holds free the lock. - */ - async assertNoUnpaidHold(companyId?: string | null): Promise { - if (!companyId) return; - const holds = - await this.bookingsRepository.countUnpaidHoldsForCompany(companyId); - if (holds > 0) { - throw new ConflictException( - 'You already have a booking waiting for payment. Pay it or cancel it ' + - 'before making a new booking.', - ); - } - } - /** Create a new freight booking. */ async create( dto: CreateBookingDto, @@ -989,10 +979,6 @@ export class BookingsService { companyId = company.id; } - // Government bookings allocate without paying, so the unpaid-hold lock - // only applies to commercial companies. - if (!isGovernment) await this.assertNoUnpaidHold(companyId); - if (dto.trainScheduleId) { // Staff manual pin: the schedule must be OPEN and on the same route. const schedule = await this.dataSource diff --git a/apps/edr-freight-api/src/modules/companies/companies.controller.ts b/apps/edr-freight-api/src/modules/companies/companies.controller.ts index 7665d82eb..1c9d75487 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -408,6 +408,30 @@ export class CompaniesController { return this.companiesService.completeIdentityVerification(user.id, dto); } + @Post("identity/gm/same-as-owner") + @ApiOperation({ + summary: + "Declare the General Manager is the company's owner, copying the owner's verified identity across. " + + "Refused until the owner is Fayda-verified — there would be nothing proven to copy.", + }) + async setGmSameAsOwner( + @CurrentUser() user: CurrentIamUser, + ): Promise { + return this.companiesService.setGmSameAsOwner(user.id); + } + + @Delete("identity/gm") + @ApiOperation({ + summary: + "Clear the General Manager's identity — the \"same as owner\" declaration or a verification, and the details either wrote. " + + "Leaves the GM open to be verified in their own right, or typed where Fayda is optional.", + }) + async clearGmIdentity( + @CurrentUser() user: CurrentIamUser, + ): Promise { + return this.companiesService.clearGmIdentity(user.id); + } + @Delete("identity/fayda/poa") @ApiOperation({ summary: diff --git a/apps/edr-freight-api/src/modules/companies/companies.fayda-identity.spec.ts b/apps/edr-freight-api/src/modules/companies/companies.fayda-identity.spec.ts index 511c3aa25..51dd1d61f 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.fayda-identity.spec.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.fayda-identity.spec.ts @@ -21,9 +21,11 @@ import { POA_DELEGATION_FILE_KEY } from "../file-upload-settings/poa-delegation. * is named, both nationalities must verify them, and their details come from * the verified payload rather than the form. * - * The owner is NOT the general manager — GM is a separate, plain typed role - * the portal offers a "same as owner" copy for, but it is never itself - * Fayda-verified or gated on. + * The owner is NOT the general manager. The GM is proved the same way, by one + * of two routes — verifying in their own right, or being declared the owner, + * which reuses that verification rather than making one human prove themselves + * twice. It stays out of the trading gate either way: the GM names who to talk + * to, not what the company may do. */ interface Ctx { @@ -278,15 +280,104 @@ describe("Fayda identity verification binds a person to the company", () => { expect(ctx.attributes.poaFaydaSub).toBe("new-sub"); }); - it("refuses to rename a verified person by hand", async () => { - const { service } = makeService({ + it("stages nothing for a verified field an approved company resubmits", async () => { + // Approving it could not move the live row — the verified value is written + // back over it — so it must never reach a reviewer as a pending change. + const { service, deps } = makeService({ + status: CompanyStatus.Active, + attributes: { ...OWNER_VERIFIED, ownerEmail: "abebe@example.com" }, + }); + + await expect( + service.updateProfile("user-1", { + companyEmail: "someone-else@example.com", + } as never), + ).resolves.toBeDefined(); + expect(deps.changeRequestRepo.create).not.toHaveBeenCalled(); + expect(deps.changeRequestRepo.update).not.toHaveBeenCalled(); + expect(deps.companiesRepo.update).not.toHaveBeenCalled(); + }); + + // The verified value wins, and it wins by overwriting rather than by + // rejecting: nobody types these fields, so a submission that disagrees is a + // stale form echoing itself back, not an edit. Failing it would block a save + // the customer never made — and leave them no way through, since re-verifying + // returns the same value they are being 400'd for. + it("overwrites a hand-renamed verified person with the verified name", async () => { + const { service, ctx } = makeService({ attributes: { ...OWNER_VERIFIED, ...POA_VERIFIED }, files: [paper()], }); await expect( service.updateProfile("user-1", { poaName: "Someone Else" } as never), - ).rejects.toBeInstanceOf(BadRequestException); + ).resolves.toBeDefined(); + expect(ctx.attributes.poaName).toBe(POA_VERIFIED.poaName); + }); + + // Fayda's email and phone claims are optional — a verification can prove the + // person and return neither. Holding the company mirrors to "the owner is + // verified" rather than to "the verification supplied this value" would + // clobber the fallbacks the portal is built to send (account email, eTrade's + // registered phone) with nothing at all. OWNER_VERIFIED is exactly that + // shape: a sub, no contact details. + it("keeps company contact details a Fayda verification never supplied", async () => { + const { service, deps } = makeService({ + attributes: { ...OWNER_VERIFIED }, + }); + + await expect( + service.updateProfile("user-1", { + companyEmail: "account@example.com", + companyPhone: "+251911777777", + } as never), + ).resolves.toBeDefined(); + const [, patch] = deps.companiesRepo.update.mock.calls.at(-1)!; + expect(patch.email).toBe("account@example.com"); + expect(patch.phone).toBe("+251911777777"); + }); + + it("overwrites company contact details the verification did supply", async () => { + const { service, deps } = makeService({ + attributes: { + ...OWNER_VERIFIED, + ownerEmail: "abebe@example.com", + ownerPhone: "+251911000000", + }, + }); + + await expect( + service.updateProfile("user-1", { + companyEmail: "someone-else@example.com", + companyPhone: "+251911999999", + } as never), + ).resolves.toBeDefined(); + const [, patch] = deps.companiesRepo.update.mock.calls.at(-1)!; + expect(patch.email).toBe("abebe@example.com"); + expect(patch.phone).toBe("+251911000000"); + }); + + // "Same as owner" copies `ownerEmail ?? null` onto the GM while setting + // `gmFaydaSub`. Locking that null made generalManagerEmail required by + // onboarding, hidden by the portal's link card and unwritable at once. + it("lets the GM's details be typed when the copied owner identity carried none", async () => { + const { service } = makeService({ + attributes: { + ...OWNER_VERIFIED, + gmSameAsOwner: true, + gmFaydaSub: "owner-sub", + generalManagerName: "Abebe Bikila", + generalManagerEmail: null, + generalManagerPhone: null, + }, + }); + + await expect( + service.updateProfile("user-1", { + generalManagerEmail: "gm@example.com", + generalManagerPhone: "+251911888888", + } as never), + ).resolves.toBeDefined(); }); it("never locks or gates the general manager — it is not the verified subject", async () => { @@ -418,10 +509,12 @@ describe("Ethiopian companies verify with Fayda; foreign companies verify identi ).resolves.toBeDefined(); }); - it("still requires a Fayda-verified PoA from a foreign company", async () => { - // The owner's credential is nationality-specific; the representative's is - // not. A PoA acts for the company inside Ethiopia whoever owns it, so a - // typed foreign name is not a representative the platform can accept. + it("accepts a typed PoA from a foreign company, whose representative may hold no Fayda ID", async () => { + // Fayda is an Ethiopian national ID, so only an Ethiopian company's + // representative can be held to it. A foreign company is offered the + // verification and uses it where its representative holds one, but a typed + // name stays sufficient — holding it to Fayda would leave a foreign + // company whose representative has no Fayda ID unable to trade at all. const { service } = makeService({ profileTypes: [ProfileType.importer], nationality: CompanyNationality.Foreign, @@ -434,6 +527,48 @@ describe("Ethiopian companies verify with Fayda; foreign companies verify identi files: [paper()], }); + await expect( + service.createCompanyProfileForUser( + "user-1", + ProfileType.freightForwarder, + ), + ).resolves.toBeDefined(); + }); + + it("still refuses a foreign company that named no PoA at all", async () => { + // The typed fallback is a different credential, not a waiver: a freight + // forwarder acts on other companies' behalf and needs a representative + // whatever its nationality. + const { service } = makeService({ + profileTypes: [ProfileType.importer], + nationality: CompanyNationality.Foreign, + attributes: { ownerPassportNumber: "P1234567" }, + files: [paper()], + }); + + await expect( + service.createCompanyProfileForUser( + "user-1", + ProfileType.freightForwarder, + ), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it("holds an Ethiopian company to a Fayda-verified PoA, typed details notwithstanding", async () => { + // The relaxation above is scoped to foreign companies only — an Ethiopian + // representative holds a Fayda ID, so typing a name must not substitute. + const { service } = makeService({ + profileTypes: [ProfileType.importer], + nationality: CompanyNationality.Ethiopian, + attributes: { + ...OWNER_VERIFIED, + poaName: "Abebe Bekele", + poaEmail: "abebe@example.com", + poaPhone: "+251911000000", + }, + files: [paper()], + }); + await expect( service.createCompanyProfileForUser( "user-1", @@ -463,4 +598,119 @@ describe("Ethiopian companies verify with Fayda; foreign companies verify identi ), ).rejects.toBeInstanceOf(BadRequestException); }); + + // ------------------------------------------------------------------------- + // General manager + // ------------------------------------------------------------------------- + + it("reuses the owner's verified identity when the GM is declared the same person", async () => { + // The GM is very often the owner. Copying the proven identity is the whole + // point — asking one human to complete two verifications proves nothing + // extra, and typing the details instead would forge a verified badge. + const { service, ctx } = makeService({ + attributes: { + ...OWNER_VERIFIED, + ownerEmail: "abebe@example.com", + ownerPhone: "+251911222333", + }, + }); + + const state = await service.setGmSameAsOwner("user-1"); + + expect(state.gm.verified).toBe(true); + expect(state.gmSameAsOwner).toBe(true); + expect(state.gm.name).toBe("Abebe Bikila"); + expect(ctx.attributes.gmFaydaSub).toBe("owner-sub"); + // The notifiers mail the flat column, so a linked GM has to land there too. + expect(ctx.attributes.generalManagerEmail).toBe("abebe@example.com"); + }); + + it("refuses to declare the GM is the owner while the owner is unverified", async () => { + // Without a verification there is no proven identity to copy — only typed + // text, which would arrive wearing a badge it had not earned. + const { service } = makeService({ attributes: {} }); + + await expect(service.setGmSameAsOwner("user-1")).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it("lets the GM verify as the same human as the owner", async () => { + // The owner/PoA collision check exists because self-delegation is not + // delegation. It must not fire here: the GM being the owner is a supported + // answer, so verifying with the owner's own Fayda sub has to succeed. + const { service, ctx } = makeService({ + attributes: { ...OWNER_VERIFIED }, + verification: { + purpose: "VERIFY", + verified: true, + sub: "owner-sub", + fullName: "Abebe Bikila", + email: "abebe@example.com", + phoneNumber: "+251911222333", + }, + }); + + const state = await service.completeIdentityVerification("user-1", { + subject: "gm", + code: "c", + state: "s", + }); + + expect(state.gm.verified).toBe(true); + expect(ctx.attributes.gmFaydaSub).toBe("owner-sub"); + expect(ctx.attributes.generalManagerName).toBe("Abebe Bikila"); + }); + + it("still refuses a PoA who is the owner", async () => { + // The GM exemption above must not have widened into the PoA. + const { service } = makeService({ + attributes: { ...OWNER_VERIFIED }, + verification: { + purpose: "VERIFY", + verified: true, + sub: "owner-sub", + fullName: "Abebe Bikila", + }, + }); + + await expect( + service.completeIdentityVerification("user-1", { + subject: "poa", + code: "c", + state: "s", + }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it("reports a pre-existing typed GM as unverified rather than blank", async () => { + // Companies onboarded before the GM was verifiable have typed details and + // no gm* attributes. Those details are still what the notifiers mail, so + // they must survive — flagged unverified so the portal offers the upgrade. + const { service, company } = makeService({ + attributes: { + ...OWNER_VERIFIED, + generalManagerName: "Legacy Manager", + generalManagerEmail: "legacy@example.com", + }, + }); + + const state = service.getCompanyIdentityState(company() as never); + + expect(state.gm.verified).toBe(false); + expect(state.gm.name).toBe("Legacy Manager"); + expect(state.gm.email).toBe("legacy@example.com"); + }); + + it("does not let an unproven GM block the company from trading", async () => { + // The GM names who to talk to, not what the company may do. Capturing it + // through Fayda changed how it is collected, not whether it gates. + const { service } = makeService({ + attributes: { ...OWNER_VERIFIED }, + }); + + await expect( + service.createCompanyProfileForUser("user-1", ProfileType.importer), + ).resolves.toBeDefined(); + }); }); diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index e0af07e0b..b5c953cac 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -33,6 +33,7 @@ import { buildCompanyIdentityState, CompanyIdentityStateDto, CompleteIdentityVerificationDto, + IDENTITY_SUBJECTS, IdentitySubject, } from "./dto/complete-identity-verification.dto"; import { ETradeService } from "./services/etrade.service"; @@ -122,31 +123,47 @@ const REQUIRED_POA_FIELDS: { key: string; label: string }[] = [ /** * `attributes` key prefix per verifiable person. The owner is NOT the general - * manager — GM is a plain typed role (the portal offers a "same as owner" copy - * once the owner is verified), while the owner is who this verification - * actually proves. They're very often the same human; that's what the copy is - * for. + * manager: the owner is who the verification proves the company through, the + * GM is personnel it names. They're very often the same human, which is what + * the portal's "same as owner" copy is for. */ -const IDENTITY_PREFIX: Record = { +const IDENTITY_PREFIX: Record = { owner: "owner", poa: "poa", + gm: "gm", }; const IDENTITY_LABEL: Record = { owner: "owner", poa: "Power of Attorney", + gm: "General Manager", }; +/** + * Typed GM columns a GM verification also writes. Three notifier services mail + * `company.generalManagerEmail` directly, so leaving these behind would mean a + * verified GM whose address the system never actually uses. + */ +const GM_TYPED_FIELDS = [ + "generalManagerName", + "generalManagerEmail", + "generalManagerPhone", +] as const; + /** * Identity fields a Fayda verification owns outright, per person. Once verified * these can no longer be typed — the government IdP is the source, so an edit * that disagrees with it is either a mistake or an attempt to launder the - * guarantee away. The GM fields are deliberately absent: GM is never itself - * Fayda-verified, so it stays freely editable regardless of the owner's state. + * guarantee away. + * + * The GM's entries are its typed columns: a verified GM is locked the same way + * the others are, while an unverified one (a foreign company's, or a record + * that predates this) stays freely editable. */ const IDENTITY_OWNED_FIELDS: Record = { owner: ["ownerName", "ownerEmail", "ownerPhone", "ownerAddress"], poa: ["poaName", "poaEmail", "poaPhone", "poaAddress"], + gm: [...GM_TYPED_FIELDS], }; /** @@ -722,6 +739,39 @@ export class CompaniesService { return out; } + /** + * `UpdateProfileDto` keys this company's completed verifications own — the + * ones `mapProfileDtoToCompanyUpdates` overwrites with the verified value + * whatever a request submits for them. + * + * A key only lands here once there is a verified value to hold it to: Fayda's + * email and phone claims are optional, and a verification that returned + * neither owns nothing to overwrite with. + * + * The map is the enforcement; this is the list used to keep those keys out of + * a change request in the first place. If the two ever drift the map still + * wins — the cost is a staged field that approving turns out not to move. + */ + private faydaOwnedKeys(company: Company): string[] { + const attrs = company.attributes ?? {}; + const held = (key: string) => { + const v = attrs[key]; + return v !== null && v !== undefined && v !== ""; + }; + + const keys: string[] = []; + if (attrs.ownerFaydaSub) { + // The Company-column mirrors of the owner's verified contact details. + if (held("ownerEmail")) keys.push("companyEmail"); + if (held("ownerPhone")) keys.push("companyPhone"); + } + for (const subject of IDENTITY_SUBJECTS) { + if (!attrs[`${IDENTITY_PREFIX[subject]}FaydaSub`]) continue; + keys.push(...IDENTITY_OWNED_FIELDS[subject].filter(held)); + } + return keys; + } + /** * Translate an UpdateProfileDto (or a staged change-request snapshot) into a * `Company` patch: scalar columns plus a merged `attributes` blob (contact/GM/ @@ -812,49 +862,46 @@ export class CompaniesService { // lets the customer type them once verified) — lock them the same way // ownerEmail/ownerPhone themselves are locked below, once there is a // verified owner to lock them to. + // + // Keyed on the verified VALUE, not on `ownerFaydaSub`: Fayda's email and + // phone claims are optional, so a verification can prove the person while + // supplying neither (see completeIdentityVerification's conditional + // spreads). The portal falls back to the account email / eTrade's + // registered phone in exactly that case and submits it on every save of + // the company step — locking against an absent value would 400 that + // forever, and re-verifying could never clear it because Fayda still has + // nothing to return. if (attrUpdates.ownerFaydaSub) { - if ( - dto.companyEmail !== undefined && - dto.companyEmail !== attrUpdates.ownerEmail - ) { - throw new BadRequestException( - "companyEmail is set by the owner's Fayda verification and cannot be edited. Re-verify to change it.", - ); - } - if ( - dto.companyPhone !== undefined && - normalizeE164(dto.companyPhone) !== - normalizeE164(String(attrUpdates.ownerPhone ?? "")) - ) { - throw new BadRequestException( - "companyPhone is set by the owner's Fayda verification and cannot be edited. Re-verify to change it.", - ); - } + if (attrUpdates.ownerEmail && dto.companyEmail !== undefined) + companyUpdates.email = attrUpdates.ownerEmail; + if (attrUpdates.ownerPhone && dto.companyPhone !== undefined) + companyUpdates.phone = normalizeE164(String(attrUpdates.ownerPhone)); } // Renaming a Fayda-verified person by hand would launder the guarantee - // away, so the fields the verification owns are refused once it exists. - for (const subject of ["owner", "poa"] as IdentitySubject[]) { + // away, so the verification keeps these fields: a submission that disagrees + // is overwritten with the verified value rather than rejected — the same + // doctrine `applyEtradeSourcedFields` uses for eTrade's fields, and for the + // same reason. The customer never types these (the portal derives them, and + // a stale form or a re-render can echo back something else entirely), so a + // 400 punishes a save they never made while an overwrite lands the truth. + for (const subject of IDENTITY_SUBJECTS) { if (!attrUpdates[`${IDENTITY_PREFIX[subject]}FaydaSub`]) continue; for (const field of IDENTITY_OWNED_FIELDS[subject]) { - const incoming = (dto as Record)[field]; - if (incoming === undefined) continue; - // The verification itself is allowed to write them; anything else is - // compared against what is already stored, not against the value this - // same call just copied into the patch. Phones are compared normalized: - // a form that re-renders +251911000000 as 0911000000 is echoing the - // stored value back, not trying to change it. + if ((dto as Record)[field] === undefined) continue; + // The verification itself is what writes them; it must not be undone by + // the value this same call just copied into the patch. if (dto.faydaIdentity && field in dto.faydaIdentity) continue; const stored = company.attributes?.[field]; - const same = field.endsWith("Phone") - ? normalizeE164(String(incoming)) === - normalizeE164(String(stored ?? "")) - : incoming === stored; - if (!same) { - throw new BadRequestException( - `${field} is set by the Fayda verification of this company's ${IDENTITY_LABEL[subject]} and cannot be edited. Re-verify to change it.`, - ); - } + // A verification that supplied nothing for this field left no guarantee + // to protect, so it stays typeable. Matters most for the GM — + // `setGmSameAsOwner` copies `ownerEmail ?? null` onto + // `generalManagerEmail` while setting `gmFaydaSub`, and + // REQUIRED_COMPANY_INFO still demands that email, so holding a null + // here makes it required, hidden by the portal's "same as owner" card, + // and unwritable all at once. + if (stored === null || stored === undefined || stored === "") continue; + attrUpdates[field] = stored; } } @@ -960,6 +1007,11 @@ export class CompaniesService { // for review with the live row left intact. await this.assertTinAvailable(company, dto.tin); const fields = this.pickDefined(dto); + // Drop what the verifications own before anything is staged. Approving one + // of these could not change the live row — mapProfileDtoToCompanyUpdates + // writes the verified value back over it — so showing it to a reviewer + // asks them to rule on a change that does not exist. + for (const key of this.faydaOwnedKeys(company)) delete fields[key]; const selfService: Record = {}; const staged: Record = {}; for (const [key, value] of Object.entries(fields)) { @@ -2693,12 +2745,17 @@ export class CompaniesService { // The owner delegating power of attorney to themselves is not a // delegation — it would let one identity satisfy both halves of the check. - const other: IdentitySubject = dto.subject === "poa" ? "owner" : "poa"; - const otherSub = company.attributes?.[`${IDENTITY_PREFIX[other]}FaydaSub`]; - if (otherSub && otherSub === result.sub) { - throw new BadRequestException( - `This identity is already registered as the company's ${IDENTITY_LABEL[other]}. The Power of Attorney must be a different person from the owner.`, - ); + // Only owner/PoA collide this way: the GM is very often the owner, and + // saying so is a supported answer rather than a conflict, so it is left out + // of this check entirely. + if (dto.subject === "owner" || dto.subject === "poa") { + const other: IdentitySubject = dto.subject === "poa" ? "owner" : "poa"; + const otherSub = company.attributes?.[`${IDENTITY_PREFIX[other]}FaydaSub`]; + if (otherSub && otherSub === result.sub) { + throw new BadRequestException( + `This identity is already registered as the company's ${IDENTITY_LABEL[other]}. The Power of Attorney must be a different person from the owner.`, + ); + } } const now = new Date().toISOString(); @@ -2714,13 +2771,26 @@ export class CompaniesService { ...(result.address ? { [`${prefix}Address`]: result.address } : {}), }; + // A GM verification also lands on the typed columns the rest of the system + // already reads (the booking, train-scheduling and contract notifiers all + // mail `generalManagerEmail`), and clears any earlier "same as owner" + // declaration — verifying in their own right is the GM answering for + // themselves. + if (dto.subject === "gm") { + identity.gmSameAsOwner = false; + if (result.fullName) identity.generalManagerName = result.fullName; + if (result.email) identity.generalManagerEmail = result.email; + if (result.phoneNumber) + identity.generalManagerPhone = normalizeE164(result.phoneNumber); + } + // An approved company's *owner* is its identity proof, so re-verifying one // is staged for backoffice review rather than quietly rewriting a live - // record. The PoA is personnel — the company names its own representative, - // and the delegation letter backing them is what the reviewer sees — so a - // PoA verification lands live, matching the typed PoA fields in - // `SELF_SERVICE_ATTRIBUTES`. - if (company.status === CompanyStatus.Active && dto.subject !== "poa") { + // record. The PoA and GM are personnel — the company names its own + // representative and manager, and the delegation letter backing the PoA is + // what the reviewer sees — so those land live, matching their typed + // counterparts in `SELF_SERVICE_ATTRIBUTES`. + if (company.status === CompanyStatus.Active && dto.subject === "owner") { await this.stageIdentityChange(company, userId, identity); return this.getCompanyIdentityState(company); } @@ -2734,6 +2804,85 @@ export class CompaniesService { return this.getCompanyIdentityState(updated); } + /** + * Declare that the General Manager is the company's owner. + * + * The GM is very often the owner, and making that human verify twice buys + * nothing — the owner's verification already proves them. So this copies the + * owner's verified identity across rather than starting a second flow, and + * records `gmSameAsOwner` so the portal can show it as a declaration rather + * than as a verification the GM passed in their own right. + * + * Refused until the owner is actually verified: without that there is no + * proven identity to copy, only typed text that would arrive wearing a + * verified badge. + */ + async setGmSameAsOwner(userId: string): Promise { + const { company } = await this.getCompanyInfoByUserId(userId); + const attrs = company.attributes ?? {}; + const ownerSub = attrs.ownerFaydaSub as string | undefined; + if (!ownerSub) { + throw new BadRequestException( + "Verify the company owner with Fayda first — there is no proven identity to reuse yet.", + ); + } + + const copied: Record = { + gmSameAsOwner: true, + gmFaydaSub: ownerSub, + gmFaydaVerifiedAt: attrs.ownerFaydaVerifiedAt ?? new Date().toISOString(), + gmName: attrs.ownerName ?? null, + gmEmail: attrs.ownerEmail ?? null, + gmPhone: attrs.ownerPhone ?? null, + gmAddress: attrs.ownerAddress ?? null, + gmBirthdate: attrs.ownerBirthdate ?? null, + gmGender: attrs.ownerGender ?? null, + // Kept in step for the notifiers, same as a GM verification does. + generalManagerName: attrs.ownerName ?? null, + generalManagerEmail: attrs.ownerEmail ?? null, + generalManagerPhone: attrs.ownerPhone ?? null, + }; + + const updated = await this.companiesRepo.update(company.id, { + attributes: { ...attrs, ...copied }, + }); + if (!updated) + throw new NotFoundException(`Company ${company.id} not found`); + updated.companyProfiles = company.companyProfiles; + return this.getCompanyIdentityState(updated); + } + + /** + * Undo the "same as owner" declaration, clearing the copied identity so the + * GM can be verified in their own right (or typed, where Fayda is optional). + */ + async clearGmIdentity(userId: string): Promise { + const { company } = await this.getCompanyInfoByUserId(userId); + const attrs = { ...(company.attributes ?? {}) }; + for (const key of [ + "gmSameAsOwner", + "gmFaydaSub", + "gmFaydaVerifiedAt", + "gmName", + "gmEmail", + "gmPhone", + "gmAddress", + "gmBirthdate", + "gmGender", + ...GM_TYPED_FIELDS, + ]) { + attrs[key] = null; + } + + const updated = await this.companiesRepo.update(company.id, { + attributes: attrs, + }); + if (!updated) + throw new NotFoundException(`Company ${company.id} not found`); + updated.companyProfiles = company.companyProfiles; + return this.getCompanyIdentityState(updated); + } + /** * Drop the Power of Attorney entirely — the verified identity, the details it * wrote and the delegation paper together. @@ -2864,14 +3013,28 @@ export class CompaniesService { ); } - // The representative is not. A PoA acts for the company inside Ethiopia - // whoever owns it, so they are always an Ethiopian holding a Fayda ID — - // a foreign company nominates one rather than typing a name. const poaNamed = POA_ATTRIBUTES.some((k) => (company.attributes?.[k] as string | undefined)?.trim(), ); if (!opts.requirePoa && !poaNamed) return; + // Fayda is an Ethiopian national ID, so only an Ethiopian company's + // representative can be held to it. A foreign company is offered the + // verification and nominates a Fayda-holding representative where it can, + // but a typed name has to remain sufficient — otherwise a foreign company + // whose representative holds no Fayda ID could never trade at all. Mirrors + // `poaProven` in buildCompanyIdentityState; the two must agree. + if (state.passportRequired) { + if (!state.poa.verified && !state.poa.name?.trim()) { + throw new BadRequestException( + opts.requirePoa + ? "Name your Power of Attorney — a freight forwarder cannot operate without one." + : "Complete the Power of Attorney you named, or remove the representative.", + ); + } + return; + } + if (!state.poa.verified) { throw new BadRequestException( opts.requirePoa diff --git a/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts index a9988cd28..d41e61e8e 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts @@ -5,13 +5,15 @@ import { Company, CompanyNationality } from "../entities/company.entity"; import { ProfileType } from "../entities/company-profile.entity"; /** - * The two people a company is verified through — its owner and its Power of - * Attorney. "Owner" is not the same as the General Manager: a company's GM is - * a plain typed role (with a "same as owner" copy the portal offers), while - * the owner is the person this verification proves. They're very often the - * same human, which is exactly what the copy is for. + * The three people a company is verified through — its owner, its Power of + * Attorney and its General Manager. The owner is the person the company's + * existence is proven by; the other two are personnel it names. + * + * The GM is very often the owner, which is what the portal's "same as owner" + * copy is for: that path reuses the owner's verified identity outright rather + * than asking the same human to verify twice. */ -export const IDENTITY_SUBJECTS = ["owner", "poa"] as const; +export const IDENTITY_SUBJECTS = ["owner", "poa", "gm"] as const; export type IdentitySubject = (typeof IDENTITY_SUBJECTS)[number]; export class CompleteIdentityVerificationDto { @@ -73,6 +75,19 @@ export class CompanyIdentityStateDto { @ApiProperty({ type: IdentityVerificationStateDto }) poa!: IdentityVerificationStateDto; + @ApiProperty({ + type: IdentityVerificationStateDto, + description: + "General manager. `verified` is true both when the GM verified with Fayda in their own right and when the company declared the GM is the owner — in the latter case the owner's Fayda sub backs it.", + }) + gm!: IdentityVerificationStateDto; + + @ApiProperty({ + description: + "True when the GM's identity is the owner's, declared through the portal's \"same as owner\" copy rather than a separate verification.", + }) + gmSameAsOwner!: boolean; + @ApiProperty({ description: "False while a mandatory requirement (Fayda for Ethiopian, passport for foreign) is still outstanding.", @@ -81,11 +96,28 @@ export class CompanyIdentityStateDto { } /** `attributes` key prefix per person. */ -const PREFIX: Record = { +const PREFIX: Record = { owner: "owner", poa: "poa", + gm: "gm", }; +/** + * Typed GM fields, kept in step with the Fayda-written ones. + * + * The GM predates this verification: its details are plain company columns + * that three notifier services mail (booking-lifecycle, train-scheduling and + * contract notifiers all read `company.generalManagerEmail`). A verification + * therefore writes BOTH — the `gm*` attributes carry the proof, these carry + * the value everything else already reads — and an unverified company keeps + * showing whatever was typed before this existed. + */ +const GM_TYPED_KEYS = { + name: "generalManagerName", + email: "generalManagerEmail", + phone: "generalManagerPhone", +} as const; + /** company.attributes keys that together mean "a PoA was entered". */ const POA_KEYS = [ "poaName", @@ -101,7 +133,7 @@ function stateFor( ): IdentityVerificationStateDto { const p = PREFIX[subject]; const read = (key: string) => (attrs[key] as string | undefined) ?? null; - return { + const state: IdentityVerificationStateDto = { verified: Boolean(read(`${p}FaydaSub`)), name: read(`${p}Name`), phone: read(`${p}Phone`), @@ -111,6 +143,18 @@ function stateFor( birthdate: read(`${p}Birthdate`), gender: read(`${p}Gender`), }; + if (subject !== "gm") return state; + + // Companies onboarded before the GM was verifiable have typed details and no + // `gm*` attributes at all. Report those rather than a blank card — they are + // still what the notifiers mail — leaving `verified` false so the portal + // offers the upgrade instead of pretending the identity is proven. + return { + ...state, + name: state.name ?? read(GM_TYPED_KEYS.name), + email: state.email ?? read(GM_TYPED_KEYS.email), + phone: state.phone ?? read(GM_TYPED_KEYS.phone), + }; } /** @@ -144,14 +188,34 @@ export function buildCompanyIdentityState( (p) => p.type === ProfileType.freightForwarder, ) || POA_KEYS.some((k) => (attrs[k] as string | undefined)?.trim()); - // Only the *owner's* credential is nationality-specific. A Power of Attorney - // acts for the company inside Ethiopia whoever owns it, so the PoA is always - // proven with Fayda — a foreign company nominates a representative who holds - // one rather than typing a name nothing backs. + const gm = stateFor(attrs, "gm"); + const gmSameAsOwner = Boolean(attrs.gmSameAsOwner); + const ownerProven = faydaRequired ? owner.verified : !passportRequired || Boolean(owner.passportNumber); - const complete = ownerProven && (!poaDue || poa.verified); - return { faydaRequired, passportRequired, owner, poa, complete }; + // Fayda is an Ethiopian national ID, so only an Ethiopian company's + // personnel can be held to it. A foreign company may nominate a + // representative who holds one — and is offered the verification — but a + // typed name has to remain sufficient, or a foreign company whose PoA has no + // Fayda ID could never finish onboarding. + const poaProven = faydaRequired + ? poa.verified + : poa.verified || Boolean(poa.name?.trim()); + + // The GM is deliberately absent from this verdict: it names who to talk to, + // not what the company may do, and it has never gated trading. Capturing it + // through Fayda changes how it is collected, not whether it is required. + const complete = ownerProven && (!poaDue || poaProven); + + return { + faydaRequired, + passportRequired, + owner, + poa, + gm, + gmSameAsOwner, + complete, + }; } diff --git a/apps/edr-freight-api/src/modules/contract-templates/contract-template-code.spec.ts b/apps/edr-freight-api/src/modules/contract-templates/contract-template-code.spec.ts new file mode 100644 index 000000000..803814967 --- /dev/null +++ b/apps/edr-freight-api/src/modules/contract-templates/contract-template-code.spec.ts @@ -0,0 +1,66 @@ +import { + CONTRACT_TEMPLATE_CODES, + contractTemplateCodeFor, +} from './entities/contract-template.entity'; +import { CONTRACT_TEMPLATE_DEFAULTS } from '../../seed/data/contract-template-defaults'; + +describe('contractTemplateCodeFor', () => { + it('splits import and export by the customs flag', () => { + expect(contractTemplateCodeFor('IMPORT', 'BULK', true)).toBe('IMPORT_BULK_CUSTOMS'); + expect(contractTemplateCodeFor('IMPORT', 'BULK', false)).toBe('IMPORT_BULK_NO_CUSTOMS'); + expect(contractTemplateCodeFor('EXPORT', 'CONTAINER', true)).toBe( + 'EXPORT_CONTAINER_CUSTOMS', + ); + expect(contractTemplateCodeFor('EXPORT', 'CONTAINER', false)).toBe( + 'EXPORT_CONTAINER_NO_CUSTOMS', + ); + }); + + it('never gives intercity a customs variant — it crosses no border', () => { + for (const flag of [true, false, null, undefined]) { + expect(contractTemplateCodeFor('DOMESTIC', 'BULK', flag)).toBe('INTERCITY_BULK'); + expect(contractTemplateCodeFor('DOMESTIC', 'CONTAINER', flag)).toBe( + 'INTERCITY_CONTAINER', + ); + } + }); + + it('treats a missing customs flag as no customs on cross-border contracts', () => { + expect(contractTemplateCodeFor('IMPORT', 'CONTAINER', null)).toBe( + 'IMPORT_CONTAINER_NO_CUSTOMS', + ); + expect(contractTemplateCodeFor('IMPORT', 'CONTAINER', undefined)).toBe( + 'IMPORT_CONTAINER_NO_CUSTOMS', + ); + }); + + it('only ever resolves to a code that exists', () => { + const directions = ['IMPORT', 'EXPORT', 'DOMESTIC', null]; + const freights = ['BULK', 'CONTAINER', 'BREAK_BULK', null]; + for (const d of directions) { + for (const f of freights) { + for (const c of [true, false]) { + expect(CONTRACT_TEMPLATE_CODES).toContain(contractTemplateCodeFor(d, f, c)); + } + } + } + }); +}); + +describe('CONTRACT_TEMPLATE_DEFAULTS', () => { + it('seeds exactly the ten declared codes, once each', () => { + const seeded = CONTRACT_TEMPLATE_DEFAULTS.map((t) => t.code).sort(); + expect(seeded).toHaveLength(10); + expect(seeded).toEqual([...CONTRACT_TEMPLATE_CODES].sort()); + }); + + it('gives every _CUSTOMS template the customs articles and no other one', () => { + for (const seed of CONTRACT_TEMPLATE_DEFAULTS) { + const hasCustomsArticle = seed.articles.some((a) => a.id === 'customs-clearing'); + // Note "_NO_CUSTOMS" also ends with "_CUSTOMS" — exclude it explicitly. + const isCustomsVariant = + seed.code.endsWith('_CUSTOMS') && !seed.code.endsWith('_NO_CUSTOMS'); + expect(hasCustomsArticle).toBe(isCustomsVariant); + } + }); +}); diff --git a/apps/edr-freight-api/src/modules/contract-templates/contract-templates.service.spec.ts b/apps/edr-freight-api/src/modules/contract-templates/contract-templates.service.spec.ts index 0478db102..11a838223 100644 --- a/apps/edr-freight-api/src/modules/contract-templates/contract-templates.service.spec.ts +++ b/apps/edr-freight-api/src/modules/contract-templates/contract-templates.service.spec.ts @@ -25,11 +25,19 @@ function seededTemplate(code: string): ContractTemplate { } describe("contractTemplateCodeFor", () => { - it("maps every direction/freight pair to one of the six codes", () => { - expect(contractTemplateCodeFor("IMPORT", "BULK")).toBe("IMPORT_BULK"); - expect(contractTemplateCodeFor("EXPORT", "CONTAINER")).toBe("EXPORT_CONTAINER"); - expect(contractTemplateCodeFor("DOMESTIC", "CONTAINER")).toBe("INTERCITY_CONTAINER"); - expect(contractTemplateCodeFor("DOMESTIC", "BULK")).toBe("INTERCITY_BULK"); + it("maps every direction/freight/customs triple to one of the ten codes", () => { + expect(contractTemplateCodeFor("IMPORT", "BULK", true)).toBe("IMPORT_BULK_CUSTOMS"); + expect(contractTemplateCodeFor("IMPORT", "BULK", false)).toBe( + "IMPORT_BULK_NO_CUSTOMS", + ); + expect(contractTemplateCodeFor("EXPORT", "CONTAINER", true)).toBe( + "EXPORT_CONTAINER_CUSTOMS", + ); + // Intercity is domestic — no border, so no customs variant either way. + expect(contractTemplateCodeFor("DOMESTIC", "CONTAINER", true)).toBe( + "INTERCITY_CONTAINER", + ); + expect(contractTemplateCodeFor("DOMESTIC", "BULK", false)).toBe("INTERCITY_BULK"); expect(contractTemplateCodeFor(null, null)).toBe("INTERCITY_CONTAINER"); }); }); @@ -60,7 +68,7 @@ describe("ContractTemplatesService.preview", () => { ); it("interpolates {{contractYear}} inside seeded article bodies", async () => { - const { html } = await service.preview("IMPORT_BULK"); + const { html } = await service.preview("IMPORT_BULK_CUSTOMS"); expect(html).toContain(`August 31, ${new Date().getFullYear()}`); }); }); diff --git a/apps/edr-freight-api/src/modules/contract-templates/contract-templates.service.ts b/apps/edr-freight-api/src/modules/contract-templates/contract-templates.service.ts index d2755fece..ea41b57a2 100644 --- a/apps/edr-freight-api/src/modules/contract-templates/contract-templates.service.ts +++ b/apps/edr-freight-api/src/modules/contract-templates/contract-templates.service.ts @@ -24,13 +24,22 @@ import { contractTemplateCodeFor, } from "./entities/contract-template.entity"; -/** Registry keys used to derive labels for the mock preview per template code. */ +/** + * Registry keys used to derive labels for the mock preview per template code. + * The registry's FORWARDING scope carries the customs/clearing clause pack, so + * the `_CUSTOMS` codes preview against it and `_NO_CUSTOMS` against + * TRANSPORT_ONLY. + */ const PREVIEW_TEMPLATE_KEYS: Record = { - IMPORT_BULK: "IMP_BULK_USD_FORWARDING", - EXPORT_BULK: "EXP_BULK_USD_TRANSPORT_ONLY", + IMPORT_BULK_CUSTOMS: "IMP_BULK_USD_FORWARDING", + IMPORT_BULK_NO_CUSTOMS: "IMP_BULK_USD_TRANSPORT_ONLY", + EXPORT_BULK_CUSTOMS: "EXP_BULK_USD_FORWARDING", + EXPORT_BULK_NO_CUSTOMS: "EXP_BULK_USD_TRANSPORT_ONLY", INTERCITY_BULK: "DOM_BULK_USD_TRANSPORT_ONLY", - IMPORT_CONTAINER: "IMP_CON_USD_TRANSPORT_ONLY", - EXPORT_CONTAINER: "EXP_CON_USD_FORWARDING", + IMPORT_CONTAINER_CUSTOMS: "IMP_CON_USD_FORWARDING", + IMPORT_CONTAINER_NO_CUSTOMS: "IMP_CON_USD_TRANSPORT_ONLY", + EXPORT_CONTAINER_CUSTOMS: "EXP_CON_USD_FORWARDING", + EXPORT_CONTAINER_NO_CUSTOMS: "EXP_CON_USD_TRANSPORT_ONLY", INTERCITY_CONTAINER: "DOM_CON_USD_TRANSPORT_ONLY", }; @@ -59,14 +68,19 @@ export class ContractTemplatesService { /** * The active template used when generating a contract document for the given - * direction/freight pair; null when missing or deactivated (the renderer then - * falls back to the built-in generic layout). + * direction/freight/customs triple; null when missing or deactivated (the + * renderer then falls back to the built-in generic layout). */ async findActiveForContract( tradeDirection?: string | null, freightType?: string | null, + customsClearingEnabled?: boolean | null, ): Promise { - const code = contractTemplateCodeFor(tradeDirection, freightType); + const code = contractTemplateCodeFor( + tradeDirection, + freightType, + customsClearingEnabled, + ); const template = await this.repository.findByCode(code); return template?.isActive ? template : null; } @@ -194,6 +208,9 @@ export class ContractTemplatesService { year: "numeric", }), contractYear: now.getFullYear(), + // Representative validity window for the admin preview only. + contractStartDate: `1 January ${now.getFullYear()}`, + contractEndDate: `31 December ${now.getFullYear()}`, client: { companyName: "Abyssinia Trading PLC", companyAddress: "Bole Sub-city, Woreda 03, H.No 1234, Addis Ababa", @@ -225,6 +242,11 @@ export class ContractTemplatesService { scheduledDate: "—", contractType: "GENERAL", cargoDescription: isBulk ? "Steel billets — 2,800 MT" : "40ft containers — FMCG cargo", + cargoTypeName: isBulk ? "Steel billets" : "Coffee", + containerType: isBulk ? "—" : "40ft", + cargoSummary: isBulk + ? "Steel billets × 2,800" + : "Coffee (40ft) × 12; Sesame (20ft) × 6", totalWeightVgm: "—", equipmentReturn: isBulk ? "—" : "With empty return", hazardousLabel: "No", diff --git a/apps/edr-freight-api/src/modules/contract-templates/entities/contract-template.entity.ts b/apps/edr-freight-api/src/modules/contract-templates/entities/contract-template.entity.ts index 73729fbb6..c322c20a4 100644 --- a/apps/edr-freight-api/src/modules/contract-templates/entities/contract-template.entity.ts +++ b/apps/edr-freight-api/src/modules/contract-templates/entities/contract-template.entity.ts @@ -2,17 +2,30 @@ import { BaseEntity } from "@edr/api-common"; import { Column, Entity, Index } from "typeorm"; /** - * The six canonical contract document templates, one per - * (trade direction × freight type) combination. Contracts store DOMESTIC for - * intercity movements; the template layer labels those INTERCITY to match the - * commercial vocabulary used on the printed documents. + * The ten canonical contract document templates. Import and export split by + * customs clearing (× freight type = 8); intercity does not, because it is a + * purely domestic Ethiopian movement that crosses no border and therefore has + * no customs leg at all (× freight type = 2). + * + * Contracts store DOMESTIC for intercity movements; the template layer labels + * those INTERCITY to match the commercial vocabulary used on the printed + * documents. + * + * The `_CUSTOMS` variant is issued when the contract has customs clearing + * enabled (the Service Provider clears in Djibouti/Ethiopia on the Client's + * behalf); `_NO_CUSTOMS` is the transport-only paper, where the Client handles + * its own declarations. */ export const CONTRACT_TEMPLATE_CODES = [ - "IMPORT_BULK", - "EXPORT_BULK", + "IMPORT_BULK_CUSTOMS", + "IMPORT_BULK_NO_CUSTOMS", + "EXPORT_BULK_CUSTOMS", + "EXPORT_BULK_NO_CUSTOMS", "INTERCITY_BULK", - "IMPORT_CONTAINER", - "EXPORT_CONTAINER", + "IMPORT_CONTAINER_CUSTOMS", + "IMPORT_CONTAINER_NO_CUSTOMS", + "EXPORT_CONTAINER_CUSTOMS", + "EXPORT_CONTAINER_NO_CUSTOMS", "INTERCITY_CONTAINER", ] as const; @@ -33,10 +46,19 @@ export interface ContractTemplateArticle { order: number; } -/** Map a contract's stored direction/freight pair onto a template code. */ +/** + * Map a contract's stored direction/freight/customs triple onto a template + * code. `customsClearingEnabled` is treated as false when absent so an older + * contract row with a null flag still resolves to a real template rather than + * falling through to the generic layout. + * + * Intercity is domestic and has no customs leg, so it resolves to a single + * unsuffixed code regardless of the flag. + */ export function contractTemplateCodeFor( tradeDirection?: string | null, freightType?: string | null, + customsClearingEnabled?: boolean | null, ): ContractTemplateCode { const direction = tradeDirection === "IMPORT" @@ -46,7 +68,11 @@ export function contractTemplateCodeFor( : "INTERCITY"; const freight = (freightType ?? "").toUpperCase().includes("BULK") ? "BULK" : "CONTAINER"; - return `${direction}_${freight}` as ContractTemplateCode; + if (direction === "INTERCITY") { + return `INTERCITY_${freight}` as ContractTemplateCode; + } + const customs = customsClearingEnabled ? "CUSTOMS" : "NO_CUSTOMS"; + return `${direction}_${freight}_${customs}` as ContractTemplateCode; } @Entity({ schema: "freight", name: "contract_templates" }) diff --git a/apps/edr-freight-api/src/modules/contracts/contract-booking.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-booking.service.ts index 8da28e400..7e0a23e3f 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-booking.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-booking.service.ts @@ -109,23 +109,6 @@ export class ContractBookingService { private readonly bookingTransitionService: BookingTransitionService, ) {} - /** - * Mirrors BookingsService.assertNoUnpaidHold for the contract booking paths: - * a company sitting on an unpaid hold (SELECTED_FOR_BATCH) books nothing new - * until it pays or the hold dies. - */ - private async assertNoUnpaidHold(companyId?: string | null): Promise { - if (!companyId) return; - const holds = - await this.bookingsRepository.countUnpaidHoldsForCompany(companyId); - if (holds > 0) { - throw new ConflictException( - 'You already have a booking waiting for payment. Pay it or cancel it ' + - 'before making a new booking.', - ); - } - } - async createUnderContract( contractId: string, dto: CreateBookingUnderContractDto, @@ -186,8 +169,6 @@ export class ContractBookingService { // remainder; the customer cannot start any other booking on the contract. // If the remainder splits again the same rule repeats until the cap is // exhausted and the contract completes. - await this.assertNoUnpaidHold(contract.companyId); - if (contract.contractKind === 'ONE_TIME') { if (await this.hasSplitBooking(contractId)) { await this.assertExactRemainder(contract, dto); @@ -475,7 +456,6 @@ export class ContractBookingService { ); } } - await this.assertNoUnpaidHold(contract.companyId); const route = await this.resolveRoute(contract, dto.contractRouteId); diff --git a/apps/edr-freight-api/src/modules/contracts/contract-pricing.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-pricing.service.ts index d54b0b544..924c1a72d 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-pricing.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-pricing.service.ts @@ -2,6 +2,7 @@ import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { RatesService } from '../rule-engine/services/rates.service'; import { ContainerTypesService } from '../rule-engine/services/container-types.service'; +import { round2 } from '../billing/invoice-settlement.util'; import { ExchangeService } from '@edr/api-common'; import { ContractsRepository } from './contracts.repository'; import { Contract } from './entities/contract.entity'; @@ -79,7 +80,7 @@ export class ContractPricingService { const currency = contract.paymentCurrency; const isEtb = currency === 'ETB'; const usdToEtb = isEtb ? await this.exchangeService.getRate('USD', 'ETB') : 1; - const convert = (usd: number): number => (isEtb ? Math.round(usd * usdToEtb) : usd); + const convert = (usd: number): number => (isEtb ? round2(usd * usdToEtb) : usd); const lineItems: ContractUnitRateLineItem[] = []; const baseType = this.baseRateType(contract); @@ -176,8 +177,14 @@ export class ContractPricingService { } } if (contract.lastMileDeliveryAddress) { + // New-style last-mile rates (PER_TON_KM / distance-banded PER_KM) are + // priced operationally per job, not as a single contract unit price. const lm = liveRates.find( - (r) => r.rateType === 'LAST_MILE' && r.currency === 'USD', + (r) => + r.rateType === 'LAST_MILE' && + r.currency === 'USD' && + r.rateUnit !== 'PER_TON_KM' && + r.minKm == null, ); if (lm && Number(lm.rateValue) > 0) { lineItems.push({ diff --git a/apps/edr-freight-api/src/modules/contracts/contract-transition.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-transition.service.ts index 1a4de8245..0a073c9f8 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-transition.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-transition.service.ts @@ -423,6 +423,7 @@ export class ContractTransitionService { const active = await this.contractTemplates.findActiveForContract( contract.tradeDirection, contract.freightType, + contract.customsClearingEnabled, ); if (!active) return null; return { diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.module.ts b/apps/edr-freight-api/src/modules/contracts/contracts.module.ts index 0e60462a4..2a07b02e5 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.module.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.module.ts @@ -1,9 +1,8 @@ import { Module, forwardRef } from '@nestjs/common'; import { UserTradeAccessModule } from '../user-trade-access/user-trade-access.module'; -import { ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { ExchangeModule, ExchangeOptions } from '@edr/api-common'; +import { registerExchangeModule } from '../exchange-settings/exchange-module-options'; import { BillingModule } from '../billing/billing.module'; import { CompaniesModule } from '../companies/companies.module'; import { FilesModule } from '../files/files.module'; @@ -102,11 +101,7 @@ import { ContractDocumentViewModelBuilder } from '../../contracts/contract-docum // by ContractBookingService.createUnderContract. forwardRef because // TrainSchedulingModule already imports ContractsModule. forwardRef(() => TrainSchedulingModule), - ExchangeModule.forRootAsync({ - inject: [ConfigService], - useFactory: (config: ConfigService): ExchangeOptions => - config.get('app.cbeExchange') ?? {}, - }), + registerExchangeModule(), ], controllers: [ContractsController, GlExchangeController], providers: [ diff --git a/apps/edr-freight-api/src/modules/exchange-settings/dto/update-exchange-setting.dto.ts b/apps/edr-freight-api/src/modules/exchange-settings/dto/update-exchange-setting.dto.ts new file mode 100644 index 000000000..98e87007c --- /dev/null +++ b/apps/edr-freight-api/src/modules/exchange-settings/dto/update-exchange-setting.dto.ts @@ -0,0 +1,13 @@ +import { IsNumber, Max, Min } from "class-validator"; + +/** + * Operator-set USD→ETB fallback. Bounded well outside any plausible published + * rate but far short of a fat-fingered magnitude error — this value multiplies + * real invoice amounts whenever CBE is unreachable. + */ +export class UpdateExchangeSettingDto { + @IsNumber({ maxDecimalPlaces: 6 }) + @Min(1) + @Max(10_000) + fallbackRate!: number; +} diff --git a/apps/edr-freight-api/src/modules/exchange-settings/entities/exchange-setting.entity.ts b/apps/edr-freight-api/src/modules/exchange-settings/entities/exchange-setting.entity.ts new file mode 100644 index 000000000..1e1f4ad66 --- /dev/null +++ b/apps/edr-freight-api/src/modules/exchange-settings/entities/exchange-setting.entity.ts @@ -0,0 +1,47 @@ +import { BaseEntity } from "@edr/api-common"; +import { Column, Entity } from "typeorm"; + +/** + * Whether the stored fallback rate was written by the automatic sync (after a + * successful CBE fetch) or typed in by an operator in the backoffice. + */ +export type ExchangeFallbackSource = "AUTO" | "MANUAL"; + +/** + * Single-row table holding the USD→ETB fallback used when the CBE endpoint is + * unreachable. The live CBE rate always wins; this is only consulted on + * failure, and is overwritten by every successful fetch so it tracks the last + * known good rate. + */ +@Entity({ schema: "freight", name: "exchange_settings" }) +export class ExchangeSetting extends BaseEntity { + /** USD→ETB rate served while the CBE endpoint is failing. */ + @Column({ + name: "fallback_rate", + type: "numeric", + precision: 18, + scale: 6, + transformer: { + to: (value: number) => value, + from: (value: string | null) => (value === null ? null : Number(value)), + }, + }) + fallbackRate!: number; + + /** `AUTO` when written by the sync, `MANUAL` when set in the backoffice. */ + @Column({ + name: "fallback_source", + type: "varchar", + length: 16, + default: "AUTO", + }) + fallbackSource!: ExchangeFallbackSource; + + /** When the fallback last changed — i.e. the last successful CBE fetch. */ + @Column({ name: "last_synced_at", type: "timestamptz", nullable: true }) + lastSyncedAt?: Date | null; + + /** IAM user id of the last operator to set the rate manually. */ + @Column({ name: "updated_by_id", type: "uuid", nullable: true }) + updatedById?: string | null; +} diff --git a/apps/edr-freight-api/src/modules/exchange-settings/exchange-module-options.ts b/apps/edr-freight-api/src/modules/exchange-settings/exchange-module-options.ts new file mode 100644 index 000000000..fb126f969 --- /dev/null +++ b/apps/edr-freight-api/src/modules/exchange-settings/exchange-module-options.ts @@ -0,0 +1,27 @@ +import { ExchangeModule, ExchangeOptions } from "@edr/api-common"; +import { ConfigService } from "@nestjs/config"; +import { DynamicModule } from "@nestjs/common"; + +import { ExchangeSettingsService } from "./exchange-settings.service"; + +/** + * The app's single `ExchangeModule` registration shape: CBE endpoint config + * from `app.cbeExchange`, with the DB-backed fallback wired in. + * + * `ExchangeModule` is registered per-feature-module (bookings, contracts, + * warehouses), so this keeps the three call sites identical rather than + * letting their options drift apart. + */ +export function registerExchangeModule(): DynamicModule { + return ExchangeModule.forRootAsync({ + inject: [ConfigService, ExchangeSettingsService], + useFactory: ( + config: ConfigService, + settings: ExchangeSettingsService, + ): ExchangeOptions => ({ + ...(config.get("app.cbeExchange") ?? {}), + loadFallbackRate: () => settings.loadFallbackRate(), + saveFallbackRate: (rate: number) => settings.saveFallbackRate(rate), + }), + }); +} diff --git a/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.controller.ts b/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.controller.ts new file mode 100644 index 000000000..a6c08a317 --- /dev/null +++ b/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.controller.ts @@ -0,0 +1,56 @@ +import { Body, Controller, Get, Patch } from "@nestjs/common"; +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; +import { CurrentUser } from "@edr/api-common"; +import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type"; + +import { FreightAdmin } from "../../common/booking-guards"; +import { UpdateExchangeSettingDto } from "./dto/update-exchange-setting.dto"; +import { ExchangeSettingsService } from "./exchange-settings.service"; + +@ApiTags("exchange-settings") +@ApiBearerAuth() +@Controller("exchange-settings") +export class ExchangeSettingsController { + constructor(private readonly service: ExchangeSettingsService) {} + + @Get() + @FreightAdmin() + @ApiOperation({ + summary: "Current USD→ETB fallback rate and CBE feed health", + }) + async get() { + const setting = await this.service.get(); + const status = this.service.getFeedStatus(); + + return { + fallbackRate: setting.fallbackRate, + fallbackSource: setting.fallbackSource, + lastSyncedAt: setting.lastSyncedAt, + updatedById: setting.updatedById, + feed: status, + }; + } + + @Patch() + @FreightAdmin() + @ApiOperation({ + summary: + "Set the USD→ETB fallback by hand (used only while CBE is unreachable)", + }) + async update( + @Body() dto: UpdateExchangeSettingDto, + @CurrentUser() user: TCurrentUser, + ) { + const updated = await this.service.setManualRate( + dto.fallbackRate, + user?.id ?? null, + ); + + return { + fallbackRate: updated.fallbackRate, + fallbackSource: updated.fallbackSource, + lastSyncedAt: updated.lastSyncedAt, + updatedById: updated.updatedById, + }; + } +} diff --git a/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.module.ts b/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.module.ts new file mode 100644 index 000000000..df0f08c62 --- /dev/null +++ b/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.module.ts @@ -0,0 +1,25 @@ +import { Global, Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { ExchangeSetting } from "./entities/exchange-setting.entity"; +import { ExchangeSettingsController } from "./exchange-settings.controller"; +import { registerExchangeModule } from "./exchange-module-options"; +import { ExchangeSettingsService } from "./exchange-settings.service"; + +/** + * Global so the several `ExchangeModule.forRootAsync` registrations (bookings, + * contracts, warehouses) can inject {@link ExchangeSettingsService} into their + * options factory without each importing this module. + * + * Also registers its own `ExchangeModule` so `ExchangeSettingsController` can + * report the live CBE feed status (`ExchangeService.getProviderStatus()`) + * alongside the DB-backed fallback rate. + */ +@Global() +@Module({ + imports: [TypeOrmModule.forFeature([ExchangeSetting]), registerExchangeModule()], + controllers: [ExchangeSettingsController], + providers: [ExchangeSettingsService], + exports: [ExchangeSettingsService], +}) +export class ExchangeSettingsModule {} diff --git a/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.service.ts b/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.service.ts new file mode 100644 index 000000000..e0b670292 --- /dev/null +++ b/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.service.ts @@ -0,0 +1,141 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { ExchangeSetting } from "./entities/exchange-setting.entity"; + +/** + * Rate used before the row exists and before the first successful CBE fetch — + * the CBE USD transactional selling rate on 2026-08-04. + */ +const SEED_FALLBACK_RATE = 162.4165; + +/** Health of the CBE feed, as surfaced to the backoffice. */ +export interface ExchangeFeedStatus { + /** Rate most recently observed, whatever its source. */ + rate: number | null; + /** `live` means CBE answered; `stored`/`default` mean it is failing. */ + source: "live" | "stored" | null; + /** ISO timestamp of the last successful fetch. */ + lastSuccessAt: string | null; + /** Message from the most recent failure, cleared on success. */ + lastError: string | null; +} + +/** + * Owns the single `exchange_settings` row: the USD→ETB fallback used when the + * CBE endpoint is unreachable. + * + * The live CBE rate is always preferred. This value is only read on failure, + * and every successful fetch overwrites it, so it tracks the last known good + * rate rather than drifting into a stale constant. + */ +@Injectable() +export class ExchangeSettingsService { + private readonly logger = new Logger(ExchangeSettingsService.name); + + /** + * Feed health, recorded from the exchange provider's callbacks rather than + * read off an injected `ExchangeService`. The provider is registered several + * times (bookings, contracts, warehouses), so no single instance sees every + * fetch — and injecting one here would be circular, since those + * registrations inject *this* service. + */ + private feed: ExchangeFeedStatus = { + rate: null, + source: null, + lastSuccessAt: null, + lastError: null, + }; + + constructor( + @InjectRepository(ExchangeSetting) + private readonly repository: Repository, + ) {} + + /** Health of the CBE feed as last observed by any provider instance. */ + getFeedStatus(): ExchangeFeedStatus { + return { ...this.feed }; + } + + /** The settings row, created at the seed rate on first access. */ + async get(): Promise { + const existing = await this.repository.findOne({ where: {} }); + if (existing) return existing; + + return this.repository.save( + this.repository.create({ + fallbackRate: SEED_FALLBACK_RATE, + fallbackSource: "AUTO", + lastSyncedAt: null, + }), + ); + } + + /** + * Reads the stored fallback for the exchange provider. Returns `null` on any + * failure so the provider falls through to its own static default rather + * than propagating a database error into a pricing call. + */ + async loadFallbackRate(): Promise { + // Only reached when the live fetch failed, so this call is itself the + // signal that the feed is down. + try { + const { fallbackRate } = await this.get(); + const usable = Number.isFinite(fallbackRate) && fallbackRate > 0; + this.feed = { + ...this.feed, + rate: usable ? fallbackRate : this.feed.rate, + source: "stored", + lastError: this.feed.lastError ?? "CBE endpoint unreachable", + }; + return usable ? fallbackRate : null; + } catch (err) { + const message = (err as Error).message; + this.feed = { ...this.feed, source: "stored", lastError: message }; + this.logger.warn(`Could not read stored exchange fallback: ${message}`); + return null; + } + } + + /** + * Records a freshly fetched live rate as the new fallback. Marked `AUTO`, + * overwriting a manual entry — a manual rate is a stopgap for while CBE is + * down, so a working CBE feed takes precedence again. + */ + async saveFallbackRate(rate: number): Promise { + // Only called after a successful fetch, so the feed is confirmed healthy. + this.feed = { + rate, + source: "live", + lastSuccessAt: new Date().toISOString(), + lastError: null, + }; + + const current = await this.get(); + await this.repository.update(current.id, { + fallbackRate: rate, + fallbackSource: "AUTO", + lastSyncedAt: new Date(), + updatedById: null, + }); + this.logger.log(`Exchange fallback synced from CBE: ${rate} ETB/USD`); + } + + /** Operator sets the fallback by hand, e.g. during a prolonged CBE outage. */ + async setManualRate( + rate: number, + updatedById?: string | null, + ): Promise { + const current = await this.get(); + await this.repository.update(current.id, { + fallbackRate: rate, + fallbackSource: "MANUAL", + updatedById: updatedById ?? null, + }); + this.logger.warn( + `Exchange fallback set manually to ${rate} ETB/USD by ${updatedById ?? "unknown user"}`, + ); + return this.get(); + } +} diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index b11912b2c..a35978020 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -3,6 +3,7 @@ import { FindOptionsWhere, In, IsNull, Not } from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { attachMileFinancials } from '../../common/mile-financials.util'; +import { estimateMileKm } from '../../common/mile-distance.util'; import { VehicleAvailability } from '../vehicles/entities/vehicle.entity'; import { BookingsRepository } from "../bookings/bookings.repository"; import { DriversService } from "../drivers/drivers.service"; @@ -285,7 +286,7 @@ export class FirstMileService { status: dto.status ?? "READY_TO_TRANSIT", advancedPayment: dto.advancedPayment ?? 0, remainingPayment: dto.remainingPayment ?? 0, - estimatedKm: dto.estimatedKm ?? null, + estimatedKm: dto.estimatedKm ?? (await estimateMileKm(this.dataSource, dto.bookingId, 'FIRST')), exactKm: dto.exactKm ?? null, vehicleId: dto.vehicleId ?? null, paid: (dto as any).paid ?? false, diff --git a/apps/edr-freight-api/src/modules/import-operations/dto/import-operations.dto.ts b/apps/edr-freight-api/src/modules/import-operations/dto/import-operations.dto.ts index 1cf4c72e5..b3ec979b4 100644 --- a/apps/edr-freight-api/src/modules/import-operations/dto/import-operations.dto.ts +++ b/apps/edr-freight-api/src/modules/import-operations/dto/import-operations.dto.ts @@ -169,6 +169,11 @@ export class CreateEmptyContainerReturnDto { @IsOptional() @IsString() performedBy?: string; + + @ApiPropertyOptional({ enum: ['EDR', 'CUSTOMER'] }) + @IsOptional() + @IsIn(['EDR', 'CUSTOMER']) + returnedBy?: 'EDR' | 'CUSTOMER'; } export class UpdateEmptyContainerReturnStatusDto extends ImportOperationActionDto { diff --git a/apps/edr-freight-api/src/modules/import-operations/entities/empty-container-return.entity.ts b/apps/edr-freight-api/src/modules/import-operations/entities/empty-container-return.entity.ts index 727aee322..2538a204b 100644 --- a/apps/edr-freight-api/src/modules/import-operations/entities/empty-container-return.entity.ts +++ b/apps/edr-freight-api/src/modules/import-operations/entities/empty-container-return.entity.ts @@ -53,4 +53,14 @@ export class EmptyContainerReturn extends BaseEntity { @Column({ name: 'performed_by', type: 'varchar', length: 120, nullable: true }) performedBy?: string | null; + + @Column({ name: 'returned_by', type: 'varchar', length: 20, nullable: true }) + returnedBy?: 'EDR' | 'CUSTOMER' | null; + + @Column({ name: 'status_history', type: 'jsonb', default: () => "'[]'" }) + statusHistory!: Array<{ + status: EmptyContainerReturnStatus; + changedAt: string; + performedBy: string | null; + }>; } diff --git a/apps/edr-freight-api/src/modules/import-operations/import-operations.service.ts b/apps/edr-freight-api/src/modules/import-operations/import-operations.service.ts index bbc1228a5..bd40e2ab1 100644 --- a/apps/edr-freight-api/src/modules/import-operations/import-operations.service.ts +++ b/apps/edr-freight-api/src/modules/import-operations/import-operations.service.ts @@ -149,18 +149,23 @@ export class ImportOperationsService { } async createEmptyReturn(dto: CreateEmptyContainerReturnDto) { + const returnDate = dto.returnDate ? new Date(dto.returnDate) : new Date(); return this.emptyReturns.save( this.emptyReturns.create({ containerNumber: dto.containerNumber, bookingId: dto.bookingId ?? null, customerId: dto.customerId ?? null, - returnDate: dto.returnDate ? new Date(dto.returnDate) : new Date(), + returnDate, facility: dto.facility ?? null, yard: dto.yard ?? null, zone: dto.zone ?? null, condition: dto.condition ?? null, handoverNote: dto.handoverNote ?? null, performedBy: dto.performedBy ?? null, + returnedBy: dto.returnedBy ?? null, + statusHistory: [ + { status: 'RETURNED', changedAt: returnDate.toISOString(), performedBy: dto.performedBy ?? null }, + ], }), ); } @@ -175,6 +180,10 @@ export class ImportOperationsService { wagonAllocationReference: dto.wagonAllocationReference ?? row.wagonAllocationReference ?? null, handoverNote: dto.handoverNote ?? row.handoverNote ?? null, performedBy: dto.performedBy ?? row.performedBy ?? null, + statusHistory: [ + ...(row.statusHistory ?? []), + { status: dto.status, changedAt: new Date().toISOString(), performedBy: dto.performedBy ?? row.performedBy ?? null }, + ], }); return this.emptyReturns.findOneOrFail({ where: { id } }); } diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/dto/approve-last-mile-request.dto.ts b/apps/edr-freight-api/src/modules/last-mile-requests/dto/approve-last-mile-request.dto.ts new file mode 100644 index 000000000..56be0c545 --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/dto/approve-last-mile-request.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsNumber, Min } from 'class-validator'; + +export class ApproveLastMileRequestDto { + // The approve dialog prefills this from GET :id/price-estimate (rule-based), + // but the chief can still override — the typed value is what's invoiced. + @ApiProperty({ description: 'Advance amount the customer must pay before execution proceeds', example: 3000 }) + @Transform(({ value }) => Number(value)) + @IsNumber() + @Min(0.01) + advanceAmount!: number; +} diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/dto/reject-last-mile-request.dto.ts b/apps/edr-freight-api/src/modules/last-mile-requests/dto/reject-last-mile-request.dto.ts new file mode 100644 index 000000000..10cee19c3 --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/dto/reject-last-mile-request.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, MaxLength } from 'class-validator'; + +export class RejectLastMileRequestDto { + @ApiProperty({ description: 'Why the request is rejected (e.g. no truck available)' }) + @IsString() + @IsNotEmpty() + @MaxLength(500) + reason!: string; +} diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/dto/sign-last-mile-contract.dto.ts b/apps/edr-freight-api/src/modules/last-mile-requests/dto/sign-last-mile-contract.dto.ts new file mode 100644 index 000000000..9ebd25d38 --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/dto/sign-last-mile-contract.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator'; + +export class SignLastMileContractDto { + @ApiPropertyOptional({ + description: + 'Signature PNG as base64 (data URI or raw). Omitted = reuse the saved profile signature.', + }) + @IsOptional() + @IsString() + signatureImageBase64?: string; + + @ApiProperty({ description: 'Name shown under the signature.' }) + @IsString() + @IsNotEmpty() + @MaxLength(160) + signerDisplayName!: string; + + @ApiPropertyOptional({ description: 'The consent statement the customer agreed to.' }) + @IsOptional() + @IsString() + @MaxLength(500) + consentText?: string; +} diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/dto/submit-last-mile-request.dto.ts b/apps/edr-freight-api/src/modules/last-mile-requests/dto/submit-last-mile-request.dto.ts new file mode 100644 index 000000000..2978f8381 --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/dto/submit-last-mile-request.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ArrayNotEmpty, ArrayUnique, IsArray, IsDateString, IsString } from 'class-validator'; + +export class SubmitLastMileRequestDto { + @ApiProperty({ + type: [String], + description: + 'Container numbers the customer wants delivered via EDR last-mile — pass every booking container to select "all".', + }) + @IsArray() + @ArrayNotEmpty() + @ArrayUnique() + @IsString({ each: true }) + containerNumbers!: string[]; + + @ApiProperty({ + description: + 'Requested last-mile delivery date (ISO date), chosen by the customer against the train departure from Djibouti.', + example: '2026-08-15', + }) + @IsDateString() + deliveryDate!: string; +} diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/entities/last-mile-request.entity.ts b/apps/edr-freight-api/src/modules/last-mile-requests/entities/last-mile-request.entity.ts new file mode 100644 index 000000000..b85c2ad5c --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/entities/last-mile-request.entity.ts @@ -0,0 +1,112 @@ +import { BaseEntity } from '@edr/api-common'; +import { LastMileRequestStatus } from '@edr/types'; +import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; + +import { Booking } from '../../bookings/entities/booking.entity'; +import { TrainSchedule } from '../../train-schedules/entities/train-schedule.entity'; +import { LastMile } from '../../last-mile/entities/last-mile.entity'; + +export const LAST_MILE_REQUEST_STATUSES = [ + LastMileRequestStatus.AwaitingConfirmation, + LastMileRequestStatus.Submitted, + LastMileRequestStatus.Approved, + LastMileRequestStatus.Rejected, +] as const; + +/** + * The pre-approval confirmation stage in front of `LastMile`: fired when a + * train departs Djibouti, filled by the customer, reviewed by the Truck & + * Machinery chief. One row per (bookingId, trainScheduleId) — a booking whose + * containers arrive across several departures gets a request per departure. + */ +@Entity({ name: 'last_mile_requests', schema: 'freight' }) +@Index(['bookingId']) +@Index(['status']) +export class LastMileRequest extends BaseEntity { + @Column({ name: 'booking_id', type: 'uuid' }) + bookingId!: string; + + @ManyToOne(() => Booking, { nullable: false, eager: false }) + @JoinColumn({ name: 'booking_id' }) + booking?: Booking; + + @Column({ name: 'train_schedule_id', type: 'uuid' }) + trainScheduleId!: string; + + @ManyToOne(() => TrainSchedule, { nullable: false, eager: false }) + @JoinColumn({ name: 'train_schedule_id' }) + trainSchedule?: TrainSchedule; + + @Column({ name: 'status', type: 'varchar', length: 30, default: LastMileRequestStatus.AwaitingConfirmation }) + status!: LastMileRequestStatus; + + /** Customer's container selection — "all" is just every booking container listed here. */ + @Column({ name: 'requested_container_numbers', type: 'text', array: true, nullable: true }) + requestedContainerNumbers?: string[] | null; + + @Column({ name: 'reminder_sent_at', type: 'timestamptz', nullable: true }) + reminderSentAt?: Date | null; + + @Column({ name: 'submitted_by_user_id', type: 'uuid', nullable: true }) + submittedByUserId?: string | null; + + @Column({ name: 'submitted_at', type: 'timestamptz', nullable: true }) + submittedAt?: Date | null; + + @Column({ name: 'reviewed_by_staff_id', type: 'uuid', nullable: true }) + reviewedByStaffId?: string | null; + + @Column({ name: 'reviewed_at', type: 'timestamptz', nullable: true }) + reviewedAt?: Date | null; + + @Column({ name: 'rejection_reason', type: 'text', nullable: true }) + rejectionReason?: string | null; + + @Column({ name: 'resulting_last_mile_id', type: 'uuid', nullable: true }) + resultingLastMileId?: string | null; + + /** Customer-chosen last-mile delivery date (guided by the train's Djibouti departure). */ + @Column({ name: 'requested_delivery_date', type: 'date', nullable: true }) + requestedDeliveryDate?: string | null; + + /** Chief-approved advance — invoiced only after the customer signs the LM contract. */ + @Column({ + name: 'approved_advance_amount', + type: 'numeric', + precision: 14, + scale: 2, + nullable: true, + transformer: { + to: (v?: number | null) => v, + from: (v?: string | null) => (v == null ? null : Number(v)), + }, + }) + approvedAdvanceAmount?: number | null; + + /** Rate snapshot taken at approval, rendered into the contract document. */ + @Column({ name: 'contract_summary', type: 'jsonb', nullable: true }) + contractSummary?: { + estimatedKm: number | null; + mode: string | null; + currency: string | null; + total: number | null; + lines: Array<{ description: string; amount: number }>; + advanceAmount: number; + } | null; + + @Column({ name: 'contract_generated_at', type: 'timestamptz', nullable: true }) + contractGeneratedAt?: Date | null; + + @Column({ name: 'customer_signed_at', type: 'timestamptz', nullable: true }) + customerSignedAt?: Date | null; + + @Column({ name: 'signer_display_name', type: 'varchar', length: 160, nullable: true }) + signerDisplayName?: string | null; + + @Column({ name: 'consent_text', type: 'text', nullable: true }) + consentText?: string | null; + + @ManyToOne(() => LastMile, { nullable: true, eager: false }) + @JoinColumn({ name: 'resulting_last_mile_id' }) + resultingLastMile?: LastMile | null; +} diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-contract.service.ts b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-contract.service.ts new file mode 100644 index 000000000..c30112617 --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-contract.service.ts @@ -0,0 +1,318 @@ +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import * as fs from 'fs'; +import * as path from 'path'; +import Handlebars from 'handlebars'; +import { Readable } from 'stream'; +import { DataSource } from 'typeorm'; +import { LastMileRequestStatus } from '@edr/types'; + +import { ContractPdfService } from '../../contracts/contract-pdf.service'; +import { Booking } from '../bookings/entities/booking.entity'; +import { BookingsService } from '../bookings/bookings.service'; +import { FilesService } from '../files/files.service'; +import { FileRecord } from '../files/entities/file.entity'; +import { MinioService } from '../minio/minio.service'; +import { SignaturesService } from '../signatures/signatures.service'; +import { SignLastMileContractDto } from './dto/sign-last-mile-contract.dto'; +import { LastMileRequest } from './entities/last-mile-request.entity'; +import { LastMileRequestsRepository } from './last-mile-requests.repository'; +import { LastMileRequestsService } from './last-mile-requests.service'; + +const FILE_RESOURCE = 'last_mile_requests'; + +/** + * The LM contract in front of the advance payment: generated when the chief + * approves the request, viewed and signed by the customer in the portal, and + * only then invoiced (LastMileRequestsService.generateAdvanceInvoice). Single + * signer (customer), so the signature lives on the request row itself — no + * signature-rows table like bookings/CRSP contracts need for multi-role. + */ +@Injectable() +export class LastMileContractService { + private readonly logger = new Logger(LastMileContractService.name); + private compiledTemplate: Handlebars.TemplateDelegate | null = null; + + constructor( + private readonly requestsRepository: LastMileRequestsRepository, + private readonly requestsService: LastMileRequestsService, + private readonly bookingsService: BookingsService, + private readonly filesService: FilesService, + private readonly minioService: MinioService, + private readonly pdfService: ContractPdfService, + private readonly signaturesService: SignaturesService, + private readonly dataSource: DataSource, + ) {} + + async getContractView(id: string, viewerUserId?: string | null) { + const request = await this.requireApprovedRequest(id); + const booking = await this.requireBooking(request); + const view = await this.buildViewModel(request, booking); + const html = this.render(view); + const savedSignature = viewerUserId + ? ((await this.signaturesService.getForUser(viewerUserId)) ?? undefined) + : undefined; + return { + requestId: request.id, + bookingId: request.bookingId, + bookingReference: booking.reference, + status: request.status, + html, + customerSignedAt: request.customerSignedAt ?? null, + signerDisplayName: request.signerDisplayName ?? null, + canSign: !request.customerSignedAt, + savedSignature, + }; + } + + async streamContract(id: string) { + const request = await this.requireApprovedRequest(id); + const booking = await this.requireBooking(request); + const record = await this.upsertContractPdf(request, booking); + return this.filesService.streamById(record.id); + } + + async sign( + id: string, + dto: SignLastMileContractDto, + signerUserId: string | null, + ): Promise { + const request = await this.requireApprovedRequest(id); + if (request.customerSignedAt) { + throw new BadRequestException('This last-mile contract is already signed'); + } + const booking = await this.requireBooking(request); + + if (signerUserId) { + const companyId = await this.bookingsService.resolveCustomerCompanyId(signerUserId); + if (companyId && booking.companyId && companyId !== booking.companyId) { + throw new BadRequestException('This request does not belong to your company'); + } + } + + // Drawn signature wins; otherwise fall back to the saved profile signature + // (same contract-signing convention as modules/contracts). + let imageBase64 = dto.signatureImageBase64; + if (!imageBase64 && signerUserId) { + const saved = await this.signaturesService.getForUser(signerUserId); + if (saved?.signatureImageUrl?.startsWith('data:')) { + imageBase64 = saved.signatureImageUrl; + } + } + if (!imageBase64) { + throw new BadRequestException( + 'No signature image provided and no saved signature on your profile', + ); + } + + const buffer = this.decodeSignatureImage(imageBase64); + const sigFile = this.toUploadFile( + `signature-customer-${booking.reference ?? request.id}.png`, + 'image/png', + buffer, + ); + const fileRecord = await this.filesService.upsertByCode({ + resourceId: request.id, + resource: FILE_RESOURCE, + code: 'signature_customer', + file: sigFile, + }); + + await this.requestsRepository.update(id, { + customerSignedAt: new Date(), + signerDisplayName: dto.signerDisplayName, + consentText: dto.consentText ?? null, + } as Partial); + + // Best-effort: keep the reusable profile signature fresh for next time. + if (signerUserId && dto.signatureImageBase64) { + try { + await this.signaturesService.upsertForUser({ + userId: signerUserId, + signerDisplayName: dto.signerDisplayName, + signatureImageBase64: dto.signatureImageBase64, + }); + } catch (err) { + this.logger.warn(`Could not save reusable signature for user ${signerUserId}: ${err}`); + } + } + + const signed = (await this.requestsRepository.findById(id, { + relations: { booking: { company: true } }, + }))!; + + // Render + store the signed PDF, then invoice the advance. PDF failure must + // not block the invoice — the document re-renders on view/download. + try { + await this.upsertContractPdf(signed, booking, fileRecord); + } catch (err) { + this.logger.warn( + `Signed LM contract PDF deferred for ${booking.reference}: ${err}. It will render on view/download.`, + ); + } + await this.requestsService.generateAdvanceInvoice(signed); + + return signed; + } + + private async upsertContractPdf( + request: LastMileRequest, + booking: Booking, + signatureRecord?: FileRecord, + ): Promise { + const view = await this.buildViewModel(request, booking, signatureRecord); + const html = this.render(view); + const pdfBuffer = await this.pdfService.htmlToPdfBuffer(html); + const companyName = booking.company?.name ?? 'Customer'; + const fileName = `LM_${companyName.replace(/[^A-Za-z0-9._-]+/g, '_')}.pdf`; + const file = this.toUploadFile(fileName, 'application/pdf', pdfBuffer); + return this.filesService.upsertByCode({ + resourceId: request.id, + resource: FILE_RESOURCE, + code: 'contract', + file, + }); + } + + private async buildViewModel( + request: LastMileRequest, + booking: Booking, + signatureRecord?: FileRecord, + ) { + const summary = request.contractSummary; + const containers = request.requestedContainerNumbers ?? []; + const cargoDescription = + booking.cargoFreeText || booking.cargoType?.cargoTypeName || null; + + const departedRows: Array<{ departedAt: Date | null }> = await this.dataSource.query( + `SELECT departed_from_djibouti_at AS "departedAt" + FROM freight.import_djibouti_operations + WHERE train_schedule_id = $1 AND deleted_at IS NULL + LIMIT 1`, + [request.trainScheduleId], + ); + + return { + companyName: booking.company?.name ?? 'Customer', + bookingReference: booking.reference ?? request.bookingId, + containerCount: containers.length || null, + containerList: containers.join(', '), + cargoDescription, + deliveryAddress: booking.lastMileDeliveryAddress ?? null, + trainDepartureDate: this.formatDate(departedRows[0]?.departedAt), + deliveryDate: this.formatDate(request.requestedDeliveryDate) ?? '—', + requestDate: this.formatDate(request.submittedAt ?? request.reminderSentAt ?? request.createdAt) ?? '—', + approvalDate: this.formatDate(request.reviewedAt) ?? '—', + currency: summary?.currency ?? booking.paymentCurrency ?? 'ETB', + rateLines: (summary?.lines ?? []).map((l) => ({ + description: l.description, + amount: this.formatAmount(l.amount), + })), + estimatedKm: summary?.estimatedKm ?? null, + advanceAmount: this.formatAmount( + summary?.advanceAmount ?? request.approvedAdvanceAmount ?? 0, + ), + signature: request.customerSignedAt + ? { + signerDisplayName: request.signerDisplayName ?? '', + signedAt: this.formatDate(request.customerSignedAt) ?? '', + consentText: request.consentText ?? null, + imageUrl: await this.signatureImageDataUri(request, signatureRecord), + } + : null, + }; + } + + /** Signature PNG as a data URI so the PDF renderer needs no MinIO access. */ + private async signatureImageDataUri( + request: LastMileRequest, + signatureRecord?: FileRecord, + ): Promise { + try { + const record = + signatureRecord ?? + (await this.filesService.findByCode(request.id, FILE_RESOURCE, 'signature_customer')); + if (!record.url) return null; + const objectName = this.minioService.getObjectNameFromUrl(record.url); + const stream = await this.minioService.getFileStream(objectName); + const buffer = await this.streamToBuffer(stream); + return `data:image/png;base64,${buffer.toString('base64')}`; + } catch { + return null; + } + } + + private render(view: Record): string { + if (!this.compiledTemplate) { + const source = fs.readFileSync( + path.join(__dirname, '..', '..', 'contracts', 'templates', 'last-mile.hbs'), + 'utf-8', + ); + this.compiledTemplate = Handlebars.compile(source); + } + return this.compiledTemplate(view); + } + + private async requireApprovedRequest(id: string): Promise { + const request = await this.requestsService.findById(id); + if (request.status !== LastMileRequestStatus.Approved) { + throw new BadRequestException( + `The last-mile contract is available once the request is approved (current status: ${request.status})`, + ); + } + return request; + } + + private async requireBooking(request: LastMileRequest): Promise { + const booking = await this.dataSource.manager.findOne(Booking, { + where: { id: request.bookingId }, + relations: { company: true, cargoType: true }, + }); + if (!booking) throw new BadRequestException(`Booking ${request.bookingId} not found`); + return booking; + } + + private formatDate(value?: Date | string | null): string | null { + if (!value) return null; + const date = value instanceof Date ? value : new Date(value); + if (Number.isNaN(date.getTime())) return null; + return date.toISOString().slice(0, 10); + } + + private formatAmount(value: number): string { + return Number(value).toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + } + + private toUploadFile(name: string, mimetype: string, buffer: Buffer): Express.Multer.File { + return { + fieldname: 'file', + originalname: name, + encoding: '7bit', + mimetype, + size: buffer.length, + buffer, + stream: Readable.from(buffer), + destination: '', + filename: '', + path: '', + }; + } + + private decodeSignatureImage(base64: string): Buffer { + const raw = base64.includes(',') ? base64.split(',')[1]! : base64; + return Buffer.from(raw, 'base64'); + } + + private streamToBuffer(stream: Readable): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + stream.on('data', (chunk: Buffer | string) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks))); + }); + } +} diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts new file mode 100644 index 000000000..4c4ac88e5 --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts @@ -0,0 +1,132 @@ +import { Body, Controller, Get, Param, ParseUUIDPipe, Post, Query, Res } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import type { Response } from 'express'; + +import { CurrentUser } from '@edr/api-common'; +import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; +import { LastMileRequestStatus } from '@edr/types'; + +import { BookingStaff } from '../../common/booking-guards'; +import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; +import { ApproveLastMileRequestDto } from './dto/approve-last-mile-request.dto'; +import { RejectLastMileRequestDto } from './dto/reject-last-mile-request.dto'; +import { SignLastMileContractDto } from './dto/sign-last-mile-contract.dto'; +import { SubmitLastMileRequestDto } from './dto/submit-last-mile-request.dto'; +import { LastMileContractService } from './last-mile-contract.service'; +import { LastMileRequestsService } from './last-mile-requests.service'; + +@ApiTags('last-mile-requests') +@ApiBearerAuth() +@Controller('last-mile-requests') +export class LastMileRequestsController { + constructor( + private readonly requestsService: LastMileRequestsService, + private readonly contractService: LastMileContractService, + ) {} + + @Get() + @BookingStaff(FREIGHT_PERMS.lastMile.requestView) + @ApiOperation({ summary: 'List last-mile confirmation requests' }) + findAll( + @Query('status') status?: string, + @Query('bookingId') bookingId?: string, + @Query('page') page?: string, + @Query('pageSize') pageSize?: string, + ) { + return this.requestsService.findAll({ + status: status as LastMileRequestStatus | undefined, + bookingId, + page: page ? parseInt(page, 10) : undefined, + pageSize: pageSize ? parseInt(pageSize, 10) : undefined, + }); + } + + @Get('free-truck-count') + @BookingStaff(FREIGHT_PERMS.lastMile.requestView) + @ApiOperation({ summary: 'Free (ACTIVE + unassigned) trucks — informational context for approval' }) + freeTruckCount() { + return this.requestsService.freeTruckCount().then((count) => ({ count })); + } + + @Get(':id/price-estimate') + @BookingStaff(FREIGHT_PERMS.lastMile.requestView) + @ApiOperation({ + summary: + 'Rule-based last-mile price estimate (estimated km × live last-mile rates) — informational context for approval', + }) + priceEstimate(@Param('id', ParseUUIDPipe) id: string) { + return this.requestsService.priceEstimate(id); + } + + // Customer-facing like :id/submit — the service ownership-checks against the + // resolved company; staff may also open it (read-only view). + @Get(':id/contract/view') + @ApiOperation({ summary: 'LM contract view model + rendered HTML + saved signature' }) + contractView(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: TCurrentUser) { + return this.contractService.getContractView(id, user?.id ?? null); + } + + @Get(':id/contract/document') + @ApiOperation({ summary: 'Download the LM contract PDF (LM_.pdf)' }) + async contractDocument( + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + ): Promise { + const { stream, record } = await this.contractService.streamContract(id); + res.setHeader('Content-Type', record.mimeType ?? 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename="${record.name}"`); + stream.pipe(res); + } + + @Post(':id/contract/sign') + @ApiOperation({ summary: 'Customer agrees and signs the LM contract — then the advance invoice is issued' }) + signContract( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: SignLastMileContractDto, + @CurrentUser() user: TCurrentUser, + ) { + return this.contractService.sign(id, dto, user?.id ?? null); + } + + @Get(':id') + @BookingStaff(FREIGHT_PERMS.lastMile.requestView) + @ApiOperation({ summary: 'Get a last-mile confirmation request by ID' }) + findOne(@Param('id', ParseUUIDPipe) id: string) { + return this.requestsService.findById(id); + } + + // No @BookingStaff — the customer (portal) fills this, not backoffice staff. + // TODO: integrate @edr/auth — @CurrentUser is a stub until then; the service + // still cross-checks the request's booking against the resolved company. + @Post(':id/submit') + @ApiOperation({ summary: "Customer confirms which containers go via EDR last-mile" }) + submit( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: SubmitLastMileRequestDto, + @CurrentUser() user: TCurrentUser, + ) { + return this.requestsService.submit(id, user?.id ?? null, dto.containerNumbers, dto.deliveryDate); + } + + @Post(':id/approve') + @BookingStaff(FREIGHT_PERMS.lastMile.requestApprove) + @ApiOperation({ summary: 'Truck & Machinery chief approves the request — LM contract becomes signable; the advance invoice follows the customer signature' }) + approve( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: ApproveLastMileRequestDto, + @CurrentUser() user: TCurrentUser, + ) { + return this.requestsService.approve(id, user?.id ?? null, dto.advanceAmount); + } + + @Post(':id/reject') + @BookingStaff(FREIGHT_PERMS.lastMile.requestApprove) + @ApiOperation({ summary: 'Truck & Machinery chief rejects the request with a reason' }) + reject( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: RejectLastMileRequestDto, + @CurrentUser() user: TCurrentUser, + ) { + return this.requestsService.reject(id, user?.id ?? null, dto.reason); + } +} diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.module.ts b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.module.ts new file mode 100644 index 000000000..c58e3d1c8 --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.module.ts @@ -0,0 +1,38 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ContractPdfService } from '../../contracts/contract-pdf.service'; +import { BillingModule } from '../billing/billing.module'; +import { BookingsModule } from '../bookings/bookings.module'; +import { FilesModule } from '../files/files.module'; +import { LastMileModule } from '../last-mile/last-mile.module'; +import { MinioModule } from '../minio/minio.module'; +import { NotificationInboxModule } from '../notification-inbox/notification-inbox.module'; +import { SignaturesModule } from '../signatures/signatures.module'; +import { LastMileRequest } from './entities/last-mile-request.entity'; +import { LastMileContractService } from './last-mile-contract.service'; +import { LastMileRequestsController } from './last-mile-requests.controller'; +import { LastMileRequestsRepository } from './last-mile-requests.repository'; +import { LastMileRequestsService } from './last-mile-requests.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([LastMileRequest]), + BillingModule, + forwardRef(() => BookingsModule), + LastMileModule, + NotificationInboxModule, + FilesModule, + MinioModule, + SignaturesModule, + ], + controllers: [LastMileRequestsController], + providers: [ + LastMileRequestsRepository, + LastMileRequestsService, + LastMileContractService, + ContractPdfService, + ], + exports: [LastMileRequestsService], +}) +export class LastMileRequestsModule {} diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.repository.ts b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.repository.ts new file mode 100644 index 000000000..536a6c022 --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.repository.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseRepository } from '@edr/api-common'; + +import { LastMileRequest } from './entities/last-mile-request.entity'; + +@Injectable() +export class LastMileRequestsRepository extends BaseRepository { + constructor( + @InjectRepository(LastMileRequest) + repository: Repository, + ) { + super(repository); + } +} diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts new file mode 100644 index 000000000..901e7b9b5 --- /dev/null +++ b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts @@ -0,0 +1,421 @@ +import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { DataSource, FindOptionsWhere } from 'typeorm'; +import { Freight, LastMileRequestStatus } from '@edr/types'; + +import { + LastMileCharge, + computeLastMileCharge, + lastMileShipmentShape, +} from '../../common/last-mile-charge.util'; +import { estimateMileKm } from '../../common/mile-distance.util'; +import { usesEdrMileService } from '../../common/mile-haulage.util'; +import { RatesService } from '../rule-engine/services/rates.service'; +import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; +import { BookingsRepository } from '../bookings/bookings.repository'; +import { BookingsService } from '../bookings/bookings.service'; +import { Booking } from '../bookings/entities/booking.entity'; +import { BillingService } from '../billing/billing.service'; +import { NotificationInboxService } from '../notification-inbox/notification-inbox.service'; +import { NotificationAudience, NotificationPriority, NotificationType } from '@edr/types'; +import { LastMileService } from '../last-mile/last-mile.service'; +import { Vehicle, VehicleAvailability, VehicleStatus } from '../vehicles/entities/vehicle.entity'; +import { LastMileRequest } from './entities/last-mile-request.entity'; +import { LastMileRequestsRepository } from './last-mile-requests.repository'; + +type ListFilter = { + status?: LastMileRequestStatus; + bookingId?: string; + page?: number; + pageSize?: number; +}; + +/** Just what remind() needs off a departed schedule — deliberately not the full + * `TrainSchedule` entity so this module never has to import train-scheduling code. */ +type DepartedSchedule = { id: string; trainNumber?: string | null }; + +@Injectable() +export class LastMileRequestsService { + private readonly logger = new Logger(LastMileRequestsService.name); + + constructor( + private readonly requestsRepository: LastMileRequestsRepository, + private readonly bookingsRepository: BookingsRepository, + private readonly bookingsService: BookingsService, + private readonly lastMileService: LastMileService, + private readonly billing: BillingService, + private readonly notifications: NotificationInboxService, + private readonly ratesService: RatesService, + private readonly dataSource: DataSource, + ) {} + + /** Container numbers on the booking (upper-cased) — mirrors LastMileService's own helper. */ + private async bookingContainerNumbers(bookingId: string): Promise { + const rows: Array<{ containerNumber: string }> = await this.dataSource.query( + `SELECT bcu.container_number AS "containerNumber" + FROM freight.booking_container_units bcu + JOIN freight.booking_container bc + ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL + WHERE bc.booking_id = $1 AND bcu.deleted_at IS NULL`, + [bookingId], + ); + return rows.map((r) => r.containerNumber.trim().toUpperCase()); + } + + /** + * Poll for trains that have departed Djibouti and remind their eligible + * bookings. Deliberately a self-contained poller (raw SQL against + * `import_djibouti_operations`/`train_schedules`, no import of train-scheduling + * module code) rather than a hook inside `TrainSchedulingService.dispatchSchedule` + * — keeps this feature decoupled from that module entirely. `remindForDeparture` + * is idempotent per (bookingId, scheduleId), so re-scanning the same recent + * window on every tick is safe — a schedule already fully reminded is a no-op. + */ + @Cron('*/2 * * * *', { name: 'last-mile-request-departure-scan' }) + async scanDepartedSchedules(): Promise { + let schedules: DepartedSchedule[] = []; + try { + schedules = await this.dataSource.query( + `SELECT ts.id AS "id", ts.train_number AS "trainNumber" + FROM freight.import_djibouti_operations op + JOIN freight.train_schedules ts + ON ts.id = op.train_schedule_id AND ts.deleted_at IS NULL + WHERE op.deleted_at IS NULL + AND op.departed_from_djibouti_at IS NOT NULL + AND op.departed_from_djibouti_at > now() - interval '14 days'`, + ); + } catch (err) { + this.logger.warn(`Failed to scan for departed schedules: ${(err as Error).message}`); + return; + } + for (const schedule of schedules) { + await this.remindForDeparture(schedule); + } + } + + /** + * Fired for a train that has departed Djibouti (import direction). For every booking + * already loaded on this schedule that bought EDR last-mile, idempotently + * creates the AWAITING_CONFIRMATION request and reminds both the customer and + * the Truck & Machinery department. Fire-and-forget per booking — one bad + * booking must never block the rest of the departure notification. + */ + async remindForDeparture(schedule: DepartedSchedule): Promise { + let bookingIds: string[] = []; + try { + const rows: Array<{ bookingId: string }> = await this.dataSource.query( + `SELECT booking_id AS "bookingId" + FROM freight.train_schedule_bookings + WHERE train_schedule_id = $1 AND loading_status = 'LOADED' AND deleted_at IS NULL`, + [schedule.id], + ); + bookingIds = rows.map((r) => r.bookingId); + } catch (err) { + this.logger.warn(`Failed to load schedule bookings for ${schedule.id}: ${(err as Error).message}`); + return; + } + if (!bookingIds.length) return; + + for (const bookingId of bookingIds) { + try { + const booking = await this.bookingsRepository.findById(bookingId); + if (!booking) continue; + if ( + !usesEdrMileService({ + tradeDirection: booking.tradeDirection, + firstMile: booking.firstMilePickupAddress ?? null, + lastMile: booking.lastMileDeliveryAddress ?? null, + }) + ) { + continue; + } + await this.remind(booking, schedule); + } catch (err) { + this.logger.warn(`Failed to remind booking ${bookingId} for schedule ${schedule.id}: ${(err as Error).message}`); + } + } + } + + private async remind(booking: Booking, schedule: DepartedSchedule): Promise { + const [existing] = await this.requestsRepository.findAll({ + where: { bookingId: booking.id, trainScheduleId: schedule.id }, + take: 1, + }); + if (existing) return; // already reminded for this departure + + const request = await this.requestsRepository.create({ + bookingId: booking.id, + trainScheduleId: schedule.id, + status: LastMileRequestStatus.AwaitingConfirmation, + reminderSentAt: new Date(), + }); + + const trainLabel = schedule.trainNumber ? `train ${schedule.trainNumber}` : 'your train'; + if (booking.companyId) { + void this.notifications.notify({ + recipients: { companyId: booking.companyId }, + audience: NotificationAudience.PORTAL, + type: NotificationType.SCHEDULE_UPDATE, + title: 'Confirm your last-mile delivery', + body: `${trainLabel} carrying booking ${booking.reference ?? booking.id} has departed Djibouti. Confirm which containers go via EDR last-mile.`, + link: `/bookings/${booking.id}/last-mile-confirm?requestId=${request.id}`, + data: { bookingId: booking.id, requestId: request.id }, + priority: NotificationPriority.HIGH, + }); + } + void this.notifications.notify({ + recipients: { permissionKeys: [FREIGHT_PERMS.lastMile.requestReview] }, + audience: NotificationAudience.BACKOFFICE, + type: NotificationType.SCHEDULE_UPDATE, + title: 'Last-mile confirmation expected', + body: `${trainLabel} carrying booking ${booking.reference ?? booking.id} has departed Djibouti — awaiting the customer's last-mile confirmation.`, + link: `/dashboard/operations/last-mile?tab=requests`, + data: { bookingId: booking.id, requestId: request.id }, + priority: NotificationPriority.HIGH, + }); + } + + async findAll(filter: ListFilter = {}): Promise<{ + data: LastMileRequest[]; + meta: { total: number; page: number; pageSize: number; totalPages: number }; + }> { + const page = filter.page ?? 1; + const pageSize = filter.pageSize ?? 50; + const where: FindOptionsWhere = {}; + if (filter.status) where.status = filter.status; + if (filter.bookingId) where.bookingId = filter.bookingId; + + const [data, total] = await this.requestsRepository.findAndCount({ + where, + relations: { booking: { company: true } }, + order: { createdAt: 'DESC' }, + skip: (page - 1) * pageSize, + take: pageSize, + }); + + return { + data, + meta: { total, page, pageSize, totalPages: Math.max(1, Math.ceil(total / pageSize)) }, + }; + } + + async findById(id: string): Promise { + const record = await this.requestsRepository.findById(id, { + relations: { booking: { company: true } }, + }); + if (!record) throw new NotFoundException(`Last-mile request ${id} not found`); + return record; + } + + /** + * Rule-based price estimate for the approval dialog: estimated km (yard GPS → + * delivery point, straight-line) × the LIVE last-mile rate rules against the + * containers the customer confirmed (or the booking's bulk tonnage). All + * nulls when km or rate coverage is missing — the dialog then behaves as + * before (manually typed advance). + */ + async priceEstimate(id: string): Promise<{ + estimatedKm: number | null; + mode: LastMileCharge['mode'] | null; + currency: string | null; + total: number | null; + lines: Array<{ description: string; amount: number }>; + }> { + const request = await this.findById(id); + const estimatedKm = await estimateMileKm(this.dataSource, request.bookingId, 'LAST'); + if (!estimatedKm) { + return { estimatedKm: null, mode: null, currency: null, total: null, lines: [] }; + } + const shape = await lastMileShipmentShape( + this.dataSource, + request.bookingId, + request.requestedContainerNumbers ?? [], + ); + const charge = computeLastMileCharge({ + ...shape, + km: estimatedKm, + liveRates: await this.ratesService.findLiveRatesDetailed(), + }); + return { + estimatedKm, + mode: charge?.mode ?? null, + currency: charge?.currency ?? null, + total: charge?.total ?? null, + lines: (charge?.lines ?? []).map(({ description, amount }) => ({ description, amount })), + }; + } + + /** Free (ACTIVE + unassigned) truck count — informational only for the approval screen. */ + async freeTruckCount(): Promise { + return this.dataSource.manager.count(Vehicle, { + where: { status: VehicleStatus.ACTIVE, availability: VehicleAvailability.FREE }, + }); + } + + async submit( + id: string, + userId: string | null, + containerNumbers: string[], + deliveryDate: string, + ): Promise { + const request = await this.findById(id); + if (request.status !== LastMileRequestStatus.AwaitingConfirmation) { + throw new BadRequestException(`Request is already ${request.status.toLowerCase()}`); + } + + if (userId) { + const companyId = await this.bookingsService.resolveCustomerCompanyId(userId); + if (companyId && request.booking?.companyId && companyId !== request.booking.companyId) { + throw new BadRequestException('This request does not belong to your company'); + } + } + + const bookingNumbers = await this.bookingContainerNumbers(request.bookingId); + const selected = containerNumbers.map((n) => n.trim().toUpperCase()); + const unknown = selected.filter((n) => !bookingNumbers.includes(n)); + if (unknown.length) { + throw new BadRequestException(`Container(s) not on this booking: ${unknown.join(', ')}`); + } + + await this.requestsRepository.update(id, { + requestedContainerNumbers: selected, + requestedDeliveryDate: deliveryDate, + status: LastMileRequestStatus.Submitted, + submittedByUserId: userId, + submittedAt: new Date(), + } as Partial); + + void this.notifications.notify({ + recipients: { permissionKeys: [FREIGHT_PERMS.lastMile.requestReview] }, + audience: NotificationAudience.BACKOFFICE, + type: NotificationType.REQUEST_SUBMITTED, + title: 'Last-mile request ready for review', + body: `Booking ${request.booking?.reference ?? request.bookingId} confirmed ${selected.length} container(s) for EDR last-mile.`, + link: `/dashboard/operations/last-mile?tab=requests`, + data: { bookingId: request.bookingId, requestId: request.id }, + priority: NotificationPriority.NORMAL, + }); + + return this.findById(id); + } + + async approve(id: string, staffId: string | null, advanceAmount: number): Promise { + const request = await this.findById(id); + if (request.status !== LastMileRequestStatus.Submitted) { + throw new BadRequestException(`Only a submitted request can be approved (current status: ${request.status})`); + } + const booking = request.booking ?? (await this.bookingsRepository.findById(request.bookingId)); + if (!booking) throw new NotFoundException(`Booking ${request.bookingId} not found`); + + // Idempotent per booking — reuses the record if one already exists. + const lastMile = await this.lastMileService.create({ + bookingId: request.bookingId, + status: 'PAYMENT_PENDING', + advancedPayment: 0, + }); + + // No invoice yet: the advance is invoiced by LastMileContractService.sign() + // once the customer has signed the LM contract — doc first, then payment. + // Snapshot the rate estimate now so the contract shows the numbers the + // chief actually approved against, immune to later rate edits. + const estimate = await this.priceEstimate(id); + + await this.requestsRepository.update(id, { + status: LastMileRequestStatus.Approved, + reviewedByStaffId: staffId, + reviewedAt: new Date(), + resultingLastMileId: lastMile.id, + approvedAdvanceAmount: advanceAmount, + contractSummary: { ...estimate, advanceAmount }, + contractGeneratedAt: new Date(), + } as Partial); + + if (booking.companyId) { + void this.notifications.notify({ + recipients: { companyId: booking.companyId }, + audience: NotificationAudience.PORTAL, + type: NotificationType.CONTRACT_STATUS, + title: 'Last-mile contract ready — view and sign', + body: `Your last-mile request for booking ${booking.reference ?? booking.id} was approved. Review and sign the last-mile contract to receive your advance invoice.`, + link: `/bookings/${booking.id}/last-mile-contract?requestId=${id}`, + data: { bookingId: booking.id, requestId: id, lastMileId: lastMile.id }, + priority: NotificationPriority.HIGH, + }); + } + + return this.findById(id); + } + + /** The advance invoice, deferred from approve() until the LM contract is signed. */ + async generateAdvanceInvoice(request: LastMileRequest): Promise { + const booking = request.booking ?? (await this.bookingsRepository.findById(request.bookingId)); + if (!booking) throw new NotFoundException(`Booking ${request.bookingId} not found`); + const advanceAmount = request.approvedAdvanceAmount; + if (!advanceAmount || !request.resultingLastMileId) { + throw new BadRequestException('Request has no approved advance to invoice'); + } + + await this.billing.generateInvoice({ + // 'last_mile' (not the InvoiceSource.LastMile enum value "lastmile") to + // match the existing source string LastMileInvoiceService/LastMileService + // already query by (findBySourceIds/findPayable/attachInvoices). + source: 'last_mile' as Freight.InvoiceSource, + sourceId: request.resultingLastMileId, + type: 'LAST_MILE_ADVANCE', + companyId: booking.companyId, + companyProfileId: booking.companyProfileId || '', + currency: booking.paymentCurrency || 'ETB', + lines: [ + { + chargeType: 'LAST_MILE_ADVANCE', + description: 'Last-mile delivery advance', + amount: advanceAmount, + }, + ], + totalAmount: advanceAmount, + }); + + if (booking.companyId) { + void this.notifications.notify({ + recipients: { companyId: booking.companyId }, + audience: NotificationAudience.PORTAL, + type: NotificationType.INVOICE_ISSUED, + title: 'Last-mile contract signed — payment due', + body: `Thank you for signing the last-mile contract for booking ${booking.reference ?? booking.id}. Pay the advance invoice to proceed.`, + link: '/billing/invoices', + data: { bookingId: booking.id, requestId: request.id, lastMileId: request.resultingLastMileId }, + priority: NotificationPriority.HIGH, + }); + } + } + + async reject(id: string, staffId: string | null, reason: string): Promise { + const request = await this.findById(id); + if (request.status !== LastMileRequestStatus.Submitted) { + throw new BadRequestException(`Only a submitted request can be rejected (current status: ${request.status})`); + } + const booking = request.booking ?? (await this.bookingsRepository.findById(request.bookingId)); + + await this.requestsRepository.update(id, { + status: LastMileRequestStatus.Rejected, + reviewedByStaffId: staffId, + reviewedAt: new Date(), + rejectionReason: reason, + } as Partial); + + if (booking?.companyId) { + void this.notifications.notify({ + recipients: { companyId: booking.companyId }, + audience: NotificationAudience.PORTAL, + type: NotificationType.BOOKING_STATUS, + title: 'Last-mile request rejected', + body: `Your last-mile request for booking ${booking.reference ?? booking.id} was rejected: ${reason}`, + link: `/bookings/${booking.id}`, + data: { bookingId: booking.id, requestId: id }, + priority: NotificationPriority.HIGH, + }); + } + + return this.findById(id); + } +} diff --git a/apps/edr-freight-api/src/modules/last-mile/last-mile-invoice.service.ts b/apps/edr-freight-api/src/modules/last-mile/last-mile-invoice.service.ts index 8a6ec0779..36d0567ad 100644 --- a/apps/edr-freight-api/src/modules/last-mile/last-mile-invoice.service.ts +++ b/apps/edr-freight-api/src/modules/last-mile/last-mile-invoice.service.ts @@ -1,13 +1,16 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; +import { DataSource } from 'typeorm'; import { Freight } from '@edr/types'; +import { ruleBasedLastMileCharge } from '../../common/last-mile-charge.util'; import { BillingService, GenerateInvoiceInput, InvoiceEventPayload, } from '../billing/billing.service'; import { Invoice } from '../billing/entities/invoice.entity'; +import { RatesService } from '../rule-engine/services/rates.service'; import { LastMileRepository } from './last-mile.repository'; import { LastMile } from './entities/last-mile.entity'; @@ -26,6 +29,8 @@ export class LastMileInvoiceService { constructor( private readonly billing: BillingService, private readonly lastMileRepo: LastMileRepository, + private readonly ratesService: RatesService, + private readonly dataSource: DataSource, ) {} /** @@ -54,6 +59,41 @@ export class LastMileInvoiceService { return null; } + // Rule-based pricing first (bulk per-ton-km / container distance bands + // against the exact km): when a LIVE last-mile rate covers the job, it — + // not the per-vehicle price/km — is the delivery fee, with its own + // currency and per-size breakdown. Same resolver setDistances used to + // write remainingPayment, recomputed here so a rate change between the + // two moments settles on the invoice's side. + const exactKm = Number(record.exactKm) || 0; + const rule = + exactKm > 0 + ? await ruleBasedLastMileCharge( + this.dataSource, + await this.ratesService.findLiveRatesDetailed(), + record.id, + exactKm, + ) + : null; + if (rule && rule.total > 0) { + return this.billing.generateInvoice({ + source: 'last_mile' as Freight.InvoiceSource, + sourceId: record.id, + type: 'DELIVERY_FEE', + companyId: lm.booking!.companyId, + companyProfileId: lm.booking!.companyProfileId || '', + currency: rule.currency, + lines: rule.lines.map((line) => ({ + chargeType: 'DELIVERY', + description: line.description, + quantity: line.quantity, + unitRate: line.unitRate, + amount: line.amount, + })), + totalAmount: rule.total, + }); + } + // numeric columns come back as strings — coerce before billing. const totalAmount = Number(record.remainingPayment) || 0; if (!Number.isFinite(totalAmount) || totalAmount <= 0) { diff --git a/apps/edr-freight-api/src/modules/last-mile/last-mile.guard.spec.ts b/apps/edr-freight-api/src/modules/last-mile/last-mile.guard.spec.ts index 232f59d50..78fe4933f 100644 --- a/apps/edr-freight-api/src/modules/last-mile/last-mile.guard.spec.ts +++ b/apps/edr-freight-api/src/modules/last-mile/last-mile.guard.spec.ts @@ -42,6 +42,7 @@ function makeService(opts: { booking?: BookingRow; hasCustomerTruck?: boolean }) { query } as unknown as DataSource, { record: jest.fn() } as never, // history {} as never, // billing + { findLiveRatesDetailed: jest.fn().mockResolvedValue([]) } as never, // ratesService {} as never, // filesService ); diff --git a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts index af6c920fc..ec7d4ccf6 100644 --- a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts +++ b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts @@ -13,6 +13,9 @@ import { usesEdrMileService, } from '../../common/mile-haulage.util'; import { attachMileFinancials } from '../../common/mile-financials.util'; +import { ruleBasedLastMileCharge } from '../../common/last-mile-charge.util'; +import { estimateMileKm } from '../../common/mile-distance.util'; +import { RatesService } from '../rule-engine/services/rates.service'; import { assertBulkTonnageRemains, assertTruckCountWithinContainers, @@ -69,6 +72,7 @@ export class LastMileService { private readonly dataSource: DataSource, private readonly history: FleetHistoryService, private readonly billing: BillingService, + private readonly ratesService: RatesService, private readonly filesService: FilesService, ) {} @@ -426,7 +430,7 @@ export class LastMileService { status: dto.status ?? 'READY_TO_TRANSIT', advancedPayment: dto.advancedPayment ?? 0, remainingPayment: dto.remainingPayment ?? 0, - estimatedKm: dto.estimatedKm ?? null, + estimatedKm: dto.estimatedKm ?? (await estimateMileKm(this.dataSource, dto.bookingId, 'LAST')), exactKm: dto.exactKm ?? null, vehicleId: dto.vehicleId ?? null, paid: (dto as any).paid ?? false, @@ -985,9 +989,18 @@ export class LastMileService { (s, a) => s + (Number(a.distanceKm) || 0) * (Number(a.vehicle?.pricePerKm) || 0), 0, ); + // Prefer the rule-based last-mile rate (bulk per-ton-km / container + // distance bands) over the per-vehicle price; the truck math stays as the + // fallback when no LIVE rule covers this job. + const rule = await ruleBasedLastMileCharge( + this.dataSource, + await this.ratesService.findLiveRatesDetailed(), + id, + total, + ); await this.lastMileRepository.update(id, { exactKm: total, - remainingPayment: amount, + remainingPayment: rule?.total ?? amount, } as any); return this.findById(id); } diff --git a/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts b/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts index 3883f8d99..3b0dc6ec9 100644 --- a/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts +++ b/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts @@ -1,7 +1,23 @@ -import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, +} from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { FleetManage, StaffReference } from '../../common/booking-guards'; +import { + BookingStaff, + FleetManage, + StaffReference, +} from '../../common/booking-guards'; import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { CreateLocomotiveDto } from './dto/create-locomotive.dto'; import { FilterLocomotivesDto } from './dto/filter-locomotives.dto'; @@ -59,4 +75,18 @@ export class LocomotivesController { decommission(@Param('id', ParseUUIDPipe) id: string) { return this.locomotivesService.decommission(id); } + + // BookingStaff, not FleetManage: the latter also accepts the coarse + // fleet:manage key, which would hand an irreversible purge to everyone who + // can edit the fleet. This action requires its own grant, nothing else. + @Delete(':id/permanent') + @BookingStaff(FREIGHT_PERMS.locomotives.hardDelete) + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + summary: + 'Permanently delete a locomotive (irreversible; refused if any train references it)', + }) + purge(@Param('id', ParseUUIDPipe) id: string) { + return this.locomotivesService.purge(id); + } } diff --git a/apps/edr-freight-api/src/modules/locomotives/locomotives.service.ts b/apps/edr-freight-api/src/modules/locomotives/locomotives.service.ts index 1da03072e..1d45cbeff 100644 --- a/apps/edr-freight-api/src/modules/locomotives/locomotives.service.ts +++ b/apps/edr-freight-api/src/modules/locomotives/locomotives.service.ts @@ -222,4 +222,61 @@ export class LocomotivesService { return updated; } + + /** + * Permanently purge a locomotive — irreversible, and only for rows nothing + * references: a mistyped or duplicated entry. + * + * Every FK onto `locomotives` is NO ACTION, so Postgres would reject the + * delete with a raw constraint error. The references are resolved up front + * instead, naming the trains involved so the message says what to detach. + * Decommissioning (`decommission`) stays the answer for a real locomotive + * leaving service. + */ + async purge(id: string): Promise { + const locomotive = await this.locomotivesRepository.findById(id); + if (!locomotive) { + throw new NotFoundException(`Locomotive ${id} not found`); + } + + const [builtTrains, setsViaJoin, setsDirect] = await Promise.all([ + this.dataSource.query>( + `SELECT t.code + FROM freight.train_locomotives tl + JOIN freight.trains t ON t.id = tl.train_id + WHERE tl.locomotive_id = $1`, + [id], + ), + this.dataSource.query>( + `SELECT t.code + FROM freight.train_set_locomotives tsl + JOIN freight.train_sets ts ON ts.id = tsl.train_set_id + LEFT JOIN freight.trains t ON t.id = ts.train_id + WHERE tsl.locomotive_id = $1`, + [id], + ), + this.dataSource.query>( + `SELECT t.code + FROM freight.train_sets ts + LEFT JOIN freight.trains t ON t.id = ts.train_id + WHERE ts.locomotive_id = $1`, + [id], + ), + ]); + + const referencing = [...builtTrains, ...setsViaJoin, ...setsDirect]; + if (referencing.length > 0) { + const codes = [ + ...new Set(referencing.map((r) => r.code).filter(Boolean)), + ]; + const named = codes.length > 0 ? ` (${codes.join(', ')})` : ''; + throw new ConflictException( + `Locomotive ${locomotive.code} is used by ${referencing.length} train record(s)${named}; detach it before deleting it permanently. Decommission it instead to take it out of service.`, + ); + } + + await this.dataSource + .getRepository(Locomotive) + .delete({ id }); + } } diff --git a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts index cddc67b2d..ac021c322 100644 --- a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts +++ b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts @@ -1,62 +1,22 @@ import { Injectable, Logger } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import axios, { isAxiosError } from "axios"; import { NotificationStrategy } from "./notification.strategy"; +import { SmsClientService } from "../sms-client.service"; @Injectable() export class SmsNotificationStrategy implements NotificationStrategy { private readonly logger = new Logger(SmsNotificationStrategy.name); - constructor(private readonly configService: ConfigService) {} + constructor(private readonly smsClient: SmsClientService) {} async send(recipient: string, message: string): Promise { - const url = - this.configService.get("OZIKING_SMS_URL") ?? - "https://notification-dev.license.aafda.gov.et/api/sms-services/ozeking/sms"; - - const appKey = this.configService.get("OZIKING_APP_KEY") ?? ""; - if (!appKey) { - this.logger.warn("OZIKING_APP_KEY is not set — SMS may be rejected by the API"); - } - - this.logger.debug(`Sending SMS to ${recipient} via ${url}`); - - // axios defaults to no timeout — a hanging gateway would block the caller - // (and any transaction it sits in) indefinitely. Always bound the wait. - const timeout = Number(this.configService.get("SMS_TIMEOUT_MS") ?? 8000); - - try { - const response = await axios.post( - url, - { - to: recipient, - sourceId: this.configService.get("OZIKING_SOURCE_ID") ?? "EDR", - sourceName: this.configService.get("OZIKING_SOURCE_NAME") ?? "EDR Freight", - appKey, - text: message, - callbackUrl: "", - }, - { - timeout, - headers: { - accept: "*/*", - "Content-Type": "application/json", - }, - }, - ); - - this.logger.debug(`SMS API response: ${response.status} ${JSON.stringify(response.data)}`); - return true; - } catch (err) { - if (isAxiosError(err)) { - this.logger.error( - `SMS API error: ${err.message} | status=${err.response?.status} | body=${JSON.stringify(err.response?.data)}`, - ); - } else { - this.logger.error(`SMS send failed: ${String(err)}`); - } - throw err; + const { queued } = await this.smsClient.sendSms({ + to: recipient, + message, + }); + if (!queued) { + this.logger.error(`SMS to ${recipient} was not queued to RabbitMQ`); } + return queued; } } diff --git a/apps/edr-freight-api/src/modules/otp/otp.service.ts b/apps/edr-freight-api/src/modules/otp/otp.service.ts index d5688e1c2..a7361fbdd 100644 --- a/apps/edr-freight-api/src/modules/otp/otp.service.ts +++ b/apps/edr-freight-api/src/modules/otp/otp.service.ts @@ -265,10 +265,10 @@ export class OtpService { /** * SMS half of {@link dispatchEmail}; same swallow-and-report contract. Sent - * via NotificationsService's direct-HTTP Ozeking strategy — the same - * transport the notification system uses — rather than the RabbitMQ - * `SMS_SERVICE` queue, so `queued: true` here means the gateway accepted the - * request, not just that a broker took ownership of the message. + * via NotificationsService's `directSend`, which now routes through the + * same RabbitMQ `SMS_SERVICE` queue as every other SMS in freight-api, so + * `queued: true` here means the broker confirmed ownership of the message, + * not that the carrier delivered it. */ private async dispatchSms( phone: string, diff --git a/apps/edr-freight-api/src/modules/overview/dto/overview-response.dto.ts b/apps/edr-freight-api/src/modules/overview/dto/overview-response.dto.ts index ab914deff..005dad08a 100644 --- a/apps/edr-freight-api/src/modules/overview/dto/overview-response.dto.ts +++ b/apps/edr-freight-api/src/modules/overview/dto/overview-response.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; export class OverviewBookingKpisDto { + @ApiProperty() total!: number; @ApiProperty() totalActive!: number; @ApiProperty() needsAction!: number; @ApiProperty() urgent!: number; @@ -9,6 +10,7 @@ export class OverviewBookingKpisDto { } export class OverviewContractKpisDto { + @ApiProperty() total!: number; @ApiProperty() totalActive!: number; @ApiProperty() needsAction!: number; @ApiProperty() inApproval!: number; @@ -21,6 +23,8 @@ export class OverviewOperationsKpisDto { @ApiProperty() wagonsAvailable!: number; @ApiProperty() containersInTransit!: number; @ApiProperty() cargoesLoaded!: number; + @ApiProperty() schedulesUpcoming!: number; + @ApiProperty() dispatchedToday!: number; } export class OverviewCustomerKpisDto { diff --git a/apps/edr-freight-api/src/modules/overview/dto/overview-tab-response.dto.ts b/apps/edr-freight-api/src/modules/overview/dto/overview-tab-response.dto.ts index d4e5c2782..7c60a91a6 100644 --- a/apps/edr-freight-api/src/modules/overview/dto/overview-tab-response.dto.ts +++ b/apps/edr-freight-api/src/modules/overview/dto/overview-tab-response.dto.ts @@ -104,10 +104,40 @@ export class OverviewBillingTabDto { generatedAt!: string; } +export class OverviewDirectionTrendPointDto { + @ApiProperty({ example: '2026-08-01' }) date!: string; + @ApiProperty() importCount!: number; + @ApiProperty() exportCount!: number; + @ApiProperty() domesticCount!: number; +} + +export class OverviewTonnagePointDto { + @ApiProperty() label!: string; + @ApiProperty() tons!: number; +} + export class OverviewOperationsTabDto { @ApiProperty({ type: OverviewOperationsKpisDto }) kpis!: OverviewOperationsKpisDto; + @ApiProperty({ type: [OverviewDirectionTrendPointDto] }) + departureTrend!: OverviewDirectionTrendPointDto[]; + + @ApiProperty({ type: [OverviewStatusCountDto] }) + scheduleStatusBreakdown!: OverviewStatusCountDto[]; + + @ApiProperty({ type: [OverviewLabelCountDto] }) + wagonsByType!: OverviewLabelCountDto[]; + + @ApiProperty({ type: [OverviewLabelCountDto] }) + wagonsByYard!: OverviewLabelCountDto[]; + + @ApiProperty({ type: [OverviewLabelCountDto] }) + containersBySize!: OverviewLabelCountDto[]; + + @ApiProperty({ type: [OverviewTonnagePointDto] }) + cargoTonnageByType!: OverviewTonnagePointDto[]; + @ApiProperty({ type: [OverviewStatusCountDto] }) trainStatusBreakdown!: OverviewStatusCountDto[]; diff --git a/apps/edr-freight-api/src/modules/overview/overview.controller.ts b/apps/edr-freight-api/src/modules/overview/overview.controller.ts index 4e1ecc6d6..661350f49 100644 --- a/apps/edr-freight-api/src/modules/overview/overview.controller.ts +++ b/apps/edr-freight-api/src/modules/overview/overview.controller.ts @@ -99,8 +99,10 @@ export class OverviewController { @BookingView() @ApiOperation({ summary: 'Operations tab metrics and charts' }) @ApiOkResponse({ type: OverviewOperationsTabDto }) - getOperationsTab(): Promise { - return this.overviewService.getOperationsTab(); + getOperationsTab( + @Query() query: OverviewQueryDto, + ): Promise { + return this.overviewService.getOperationsTab(query.range ?? '30d'); } @Get('customers') diff --git a/apps/edr-freight-api/src/modules/overview/overview.module.ts b/apps/edr-freight-api/src/modules/overview/overview.module.ts index ff19bb801..a20f75ed2 100644 --- a/apps/edr-freight-api/src/modules/overview/overview.module.ts +++ b/apps/edr-freight-api/src/modules/overview/overview.module.ts @@ -9,6 +9,7 @@ import { Container } from "../container-management/entities/container.entity"; import { Company } from "../companies/entities/company.entity"; import { Contract } from "../contracts/entities/contract.entity"; import { PaymentEntity } from "../payment/entities/payment.entity"; +import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity"; import { Train } from "../trains/entities/train.entity"; import { Wagon } from "../wagons/entities/wagon.entity"; import { UserTradeAccessModule } from "../user-trade-access/user-trade-access.module"; @@ -23,6 +24,7 @@ import { OverviewService } from "./overview.service"; PaymentEntity, Company, Train, + TrainSchedule, Wagon, Container, Cargo, diff --git a/apps/edr-freight-api/src/modules/overview/overview.repository.ts b/apps/edr-freight-api/src/modules/overview/overview.repository.ts index a8957d760..c61320d8c 100644 --- a/apps/edr-freight-api/src/modules/overview/overview.repository.ts +++ b/apps/edr-freight-api/src/modules/overview/overview.repository.ts @@ -11,7 +11,12 @@ import { Cargo } from "../cargoes/entities/cargoes.entity"; import { Container } from "../container-management/entities/container.entity"; import { Contract } from "../contracts/entities/contract.entity"; import { PaymentEntity } from "../payment/entities/payment.entity"; +import { CargoType } from "../rule-engine/entities/cargo-type.entity"; +import { ContainerType } from "../rule-engine/entities/container-type.entity"; +import { Yard } from "../rule-engine/entities/yard.entity"; +import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity"; import { Train } from "../trains/entities/train.entity"; +import { WagonType } from "../wagon-types/entities/wagon-type.entity"; import { Wagon } from "../wagons/entities/wagon.entity"; import { OVERVIEW_CLOSED_STATUSES, @@ -34,6 +39,7 @@ const EXCLUDE_GENERAL_CONTRACT_BOOKINGS = "(booking.contract_kind IS NULL OR booking.contract_kind <> 'GENERAL')"; export type OverviewBookingKpisRow = { + total: number; totalActive: number; needsAction: number; urgent: number; @@ -53,6 +59,7 @@ export type OverviewRecentBookingRow = { }; export type OverviewContractKpisRow = { + total: number; totalActive: number; needsAction: number; inApproval: number; @@ -83,6 +90,8 @@ export class OverviewRepository { private readonly companyRepository: Repository, @InjectRepository(Train) private readonly trainRepository: Repository, + @InjectRepository(TrainSchedule) + private readonly trainScheduleRepository: Repository, @InjectRepository(Wagon) private readonly wagonRepository: Repository, @InjectRepository(Container) @@ -101,7 +110,8 @@ export class OverviewRepository { const scope = directionScopeSql("booking.trade_direction", dirs); const row = await this.bookingRepository .createQueryBuilder("booking") - .select( + .select("COUNT(*)::int", "total") + .addSelect( `COUNT(*) FILTER (WHERE booking.status NOT IN (:...closedStatuses) AND booking.status != 'DRAFT')::int`, "totalActive", ) @@ -133,6 +143,7 @@ export class OverviewRepository { .getRawOne>(); return { + total: Number(row?.total ?? 0), totalActive: Number(row?.totalActive ?? 0), needsAction: Number(row?.needsAction ?? 0), urgent: Number(row?.urgent ?? 0), @@ -146,9 +157,17 @@ export class OverviewRepository { wagonsAvailable: number; containersInTransit: number; cargoesLoaded: number; + schedulesUpcoming: number; + dispatchedToday: number; }> { - const [trainsActive, wagonsAvailable, containersInTransit, cargoesLoaded] = - await Promise.all([ + const [ + trainsActive, + wagonsAvailable, + containersInTransit, + cargoesLoaded, + schedulesUpcoming, + dispatchedToday, + ] = await Promise.all([ this.trainRepository .createQueryBuilder("train") .where("train.deleted_at IS NULL") @@ -178,6 +197,22 @@ export class OverviewRepository { statuses: ["LOADED", "IN_TRANSIT"], }) .getCount(), + this.trainScheduleRepository + .createQueryBuilder("schedule") + .where("schedule.deleted_at IS NULL") + .andWhere("schedule.status = :status", { + status: Freight.TrainScheduleStatus.Scheduled, + }) + .andWhere("schedule.scheduled_departure_date >= CURRENT_DATE") + .getCount(), + this.trainScheduleRepository + .createQueryBuilder("schedule") + .where("schedule.deleted_at IS NULL") + .andWhere("schedule.status = :status", { + status: Freight.TrainScheduleStatus.Dispatched, + }) + .andWhere("schedule.scheduled_departure_date::date = CURRENT_DATE") + .getCount(), ]); return { @@ -185,6 +220,8 @@ export class OverviewRepository { wagonsAvailable, containersInTransit, cargoesLoaded, + schedulesUpcoming, + dispatchedToday, }; } @@ -533,6 +570,141 @@ export class OverviewRepository { return this.statusBreakdown(this.cargoRepository, "cargo"); } + async getScheduleStatusBreakdown(): Promise< + { status: string; count: number }[] + > { + return this.statusBreakdown(this.trainScheduleRepository, "schedule"); + } + + /** Scheduled departures per day over the range, split by trade direction. */ + async getDepartureTrend(days: number): Promise< + { + date: string; + importCount: number; + exportCount: number; + domesticCount: number; + }[] + > { + const rows = await this.trainScheduleRepository + .createQueryBuilder("schedule") + .select( + `to_char(schedule.scheduled_departure_date::date, 'YYYY-MM-DD')`, + "date", + ) + .addSelect( + `COUNT(*) FILTER (WHERE schedule.direction = 'IMPORT')::int`, + "importCount", + ) + .addSelect( + `COUNT(*) FILTER (WHERE schedule.direction = 'EXPORT')::int`, + "exportCount", + ) + .addSelect( + `COUNT(*) FILTER (WHERE schedule.direction NOT IN ('IMPORT', 'EXPORT') OR schedule.direction IS NULL)::int`, + "domesticCount", + ) + .where("schedule.deleted_at IS NULL") + .andWhere("schedule.status != :draft", { + draft: Freight.TrainScheduleStatus.Draft, + }) + .andWhere( + `schedule.scheduled_departure_date >= CURRENT_DATE - :days::int + 1`, + { days }, + ) + .andWhere( + `schedule.scheduled_departure_date < CURRENT_DATE + :ahead::int`, + { ahead: 8 }, + ) + .groupBy("schedule.scheduled_departure_date::date") + .orderBy("schedule.scheduled_departure_date::date", "ASC") + .getRawMany<{ + date: string; + importCount: string; + exportCount: string; + domesticCount: string; + }>(); + + return rows.map((row) => ({ + date: row.date, + importCount: Number(row.importCount), + exportCount: Number(row.exportCount), + domesticCount: Number(row.domesticCount), + })); + } + + async getWagonsByType(): Promise<{ label: string; count: number }[]> { + const rows = await this.wagonRepository + .createQueryBuilder("wagon") + .leftJoin(WagonType, "wagon_type", "wagon_type.id = wagon.wagon_type_id") + .select(`COALESCE(wagon_type.name, 'Unknown')`, "label") + .addSelect("COUNT(*)::int", "count") + .where("wagon.deleted_at IS NULL") + .groupBy("wagon_type.name") + .orderBy("count", "DESC") + .getRawMany<{ label: string; count: string }>(); + + return rows.map((row) => ({ label: row.label, count: Number(row.count) })); + } + + async getWagonsByYard(limit: number): Promise< + { label: string; count: number }[] + > { + const rows = await this.wagonRepository + .createQueryBuilder("wagon") + .innerJoin(Yard, "yard", "yard.id = wagon.current_yard_id") + .select("yard.label", "label") + .addSelect("COUNT(*)::int", "count") + .where("wagon.deleted_at IS NULL") + .groupBy("yard.label") + .orderBy("count", "DESC") + .limit(limit) + .getRawMany<{ label: string; count: string }>(); + + return rows.map((row) => ({ label: row.label, count: Number(row.count) })); + } + + async getContainersBySize(): Promise<{ label: string; count: number }[]> { + const rows = await this.containerRepository + .createQueryBuilder("container") + .leftJoin( + ContainerType, + "container_type", + "container_type.id = container.container_type_id", + ) + .select( + `COALESCE(container_type.size_ft::text || ' ft', container_type.code, 'Unknown')`, + "label", + ) + .addSelect("COUNT(*)::int", "count") + .where("container.deleted_at IS NULL") + .groupBy("container_type.size_ft") + .addGroupBy("container_type.code") + .orderBy("count", "DESC") + .getRawMany<{ label: string; count: string }>(); + + return rows.map((row) => ({ label: row.label, count: Number(row.count) })); + } + + /** Total cargo weight (tons) grouped by cargo type, heaviest first. */ + async getCargoTonnageByType(limit: number): Promise< + { label: string; tons: number }[] + > { + const rows = await this.cargoRepository + .createQueryBuilder("cargo") + .leftJoin(CargoType, "cargo_type", "cargo_type.id = cargo.cargo_type_id") + .select(`COALESCE(cargo_type.cargo_type_name, 'Other')`, "label") + .addSelect(`ROUND(COALESCE(SUM(cargo.weight), 0) / 1000, 1)`, "tons") + .where("cargo.deleted_at IS NULL") + .groupBy("cargo_type.cargo_type_name") + .orderBy("tons", "DESC") + .limit(limit) + .getRawMany<{ label: string; tons: string }>(); + + return rows + .map((row) => ({ label: row.label, tons: Number(row.tons) })) + .filter((row) => row.tons > 0); + } + private async statusBreakdown( repository: Repository, alias: string, @@ -677,7 +849,8 @@ export class OverviewRepository { const scope = directionScopeSql("contract.trade_direction", dirs); const row = await this.contractRepository .createQueryBuilder("contract") - .select( + .select("COUNT(*)::int", "total") + .addSelect( `COUNT(*) FILTER (WHERE contract.status NOT IN (:...closedStatuses) AND contract.status != 'DRAFT')::int`, "totalActive", ) @@ -708,6 +881,7 @@ export class OverviewRepository { .getRawOne>(); return { + total: Number(row?.total ?? 0), totalActive: Number(row?.totalActive ?? 0), needsAction: Number(row?.needsAction ?? 0), inApproval: Number(row?.inApproval ?? 0), diff --git a/apps/edr-freight-api/src/modules/overview/overview.service.ts b/apps/edr-freight-api/src/modules/overview/overview.service.ts index 4b1f5d6bf..399bf4f72 100644 --- a/apps/edr-freight-api/src/modules/overview/overview.service.ts +++ b/apps/edr-freight-api/src/modules/overview/overview.service.ts @@ -202,15 +202,31 @@ export class OverviewService { }; } - async getOperationsTab(): Promise { + async getOperationsTab( + range: OverviewRangeQuery = '30d', + ): Promise { + const days = OVERVIEW_RANGE_DAYS[range]; + const [ kpis, + departureTrend, + scheduleStatusBreakdown, + wagonsByType, + wagonsByYard, + containersBySize, + cargoTonnageByType, trainStatusBreakdown, wagonStatusBreakdown, containerStatusBreakdown, cargoStatusBreakdown, ] = await Promise.all([ this.overviewRepository.getOperationsKpis(), + this.overviewRepository.getDepartureTrend(days), + this.overviewRepository.getScheduleStatusBreakdown(), + this.overviewRepository.getWagonsByType(), + this.overviewRepository.getWagonsByYard(8), + this.overviewRepository.getContainersBySize(), + this.overviewRepository.getCargoTonnageByType(8), this.overviewRepository.getTrainStatusBreakdown(), this.overviewRepository.getWagonStatusBreakdown(), this.overviewRepository.getContainerStatusBreakdown(), @@ -219,6 +235,12 @@ export class OverviewService { return { kpis, + departureTrend, + scheduleStatusBreakdown, + wagonsByType, + wagonsByYard, + containersBySize, + cargoTonnageByType, trainStatusBreakdown, wagonStatusBreakdown, containerStatusBreakdown, diff --git a/apps/edr-freight-api/src/modules/payment/payment-events.consumer.ts b/apps/edr-freight-api/src/modules/payment/payment-events.consumer.ts index 91237223c..e285c181c 100644 --- a/apps/edr-freight-api/src/modules/payment/payment-events.consumer.ts +++ b/apps/edr-freight-api/src/modules/payment/payment-events.consumer.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from "@nestjs/common"; +import { Injectable, Logger, SetMetadata } from "@nestjs/common"; import { Nack, RabbitSubscribe } from "@golevelup/nestjs-rabbitmq"; import { Public } from "@edr/api-common"; import { @@ -14,6 +14,11 @@ import { PaymentService as PaymentSvc } from "./payment.service"; const FREIGHT_QUEUE = PAYMENT_QUEUES[PaymentService.FREIGHT]; +// @tria-plc/auditlog's global ClientLoggerInterceptor (present in deployed builds) +// crashes on non-HTTP contexts (`originalUrl.split` on a RabbitMQ message) and the +// resulting requeue storm blocks payment.succeeded forever. Its IgnoreLoggerAudit +// decorator is just this metadata key — set it directly so we don't need the package. +@SetMetadata("ignoreAuditLogger", true) @Injectable() export class PaymentEventsConsumer { private readonly logger = new Logger(PaymentEventsConsumer.name); diff --git a/apps/edr-freight-api/src/modules/payment/payment.controller.ts b/apps/edr-freight-api/src/modules/payment/payment.controller.ts index 3602657de..b1dafbc85 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.controller.ts @@ -4,6 +4,7 @@ import { HttpStatus, Param, ParseUUIDPipe, + Post, Query, Res, } from "@nestjs/common"; @@ -84,6 +85,15 @@ export class PaymentController { return this.paymentService.getIntentByBookingId(bookingId); } + @Post("redirect-success/:bookingId") + @ApiOperation({ + summary: + "Success-redirect ack: mark payment processing + invoice PAYMENT_PROCESSING (webhook remains source of truth)", + }) + acknowledgeSuccessRedirect(@Param("bookingId") bookingId: string) { + return this.paymentService.acknowledgeSuccessRedirect(bookingId); + } + @Get("receipt/:orderId") @Public() @ApiOperation({ summary: "Generate a payment receipt HTML page" }) diff --git a/apps/edr-freight-api/src/modules/payment/payment.service.ts b/apps/edr-freight-api/src/modules/payment/payment.service.ts index 99c4aca46..316f3e8fe 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.service.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -242,19 +242,22 @@ export class PaymentService { // debited against the intent amount, so the dev shortcut would break it. // CAC bank rejects amounts below 10 (DJF bounds 10–100,000), so its dev // shortcut floor is 10, not 1. - amountMinor: isCbeBill - ? input.amountMinor - : input.method === ProviderMethod.CAC_BANK - ? 10 - : 1, + // amountMinor: isCbeBill + // ? input.amountMinor + // : input.method === ProviderMethod.CAC_BANK + // ? 10 + // : 1, + amountMinor: input.amountMinor, currency: input.currency, provider: input.method as ProviderMethod, platform: input.platform, payerAccount: input.payerAccount, payerName: input.payerName, expiresAt: input.expiresAt, + // bookingId lets the success page ack the redirect (→ PAYMENT_PROCESSING). returnUrl: - input.returnUrl ?? "https://edrfreight.triaplc.com/payment/success", + input.returnUrl ?? + `https://edrfreight.triaplc.com/payment/success?bookingId=${encodeURIComponent(input.referenceId)}`, failureUrl: input.failureUrl ?? "https://edrfreight.triaplc.com/payment/failure", }); @@ -492,6 +495,37 @@ export class PaymentService { return { alreadyFinalized: false }; } + /** + * Success-redirect ack from the portal: the customer finished provider + * checkout, settlement webhook not (necessarily) in yet. Optimistic + * intermediate only — the webhook stays the source of truth. Never + * downgrades: only action-required → processing, and the invoice moves to + * PAYMENT_PROCESSING only from an open unpaid status. CBE_BILL is excluded + * (bank-counter flow, it has no redirect). + */ + async acknowledgeSuccessRedirect( + referenceId: string, + ): Promise<{ acknowledged: boolean }> { + const intent = await this.paymentRepo.findOneBy({ refId: referenceId }); + if (!intent || intent.method === "cbe-bill") { + return { acknowledged: false }; + } + + if (intent.status === "action-required") { + await this.paymentRepo.update( + { id: intent.id, status: "action-required" }, + { status: "processing" }, + ); + } + // Even if the intent already advanced (e.g. webhook raced the redirect to + // "processing"), the invoice ack is idempotent and status-guarded. + if (intent.status === "action-required" || intent.status === "processing") { + await this.billing.markInvoicePaymentProcessing(intent.id); + return { acknowledged: true }; + } + return { acknowledged: false }; + } + async markPaymentFailed(input: { intentId: string; failureCode?: string; @@ -510,7 +544,9 @@ export class PaymentService { }, ); - // Invoice stays open for retry — nothing to settle. Logged only. + // Invoice stays open for retry — nothing to settle. A redirect-acked + // PAYMENT_PROCESSING invoice is put back to PENDING so it reads payable. + await this.billing.revertInvoicePaymentProcessing(intent.id); this.logger.warn( `Payment ${intent.id} failed for ${intent.refId}` + (input.failureMessage ? `: ${input.failureMessage}` : ""), diff --git a/apps/edr-freight-api/src/modules/reports/dto/report-query.dto.ts b/apps/edr-freight-api/src/modules/reports/dto/report-query.dto.ts new file mode 100644 index 000000000..82b1a76ef --- /dev/null +++ b/apps/edr-freight-api/src/modules/reports/dto/report-query.dto.ts @@ -0,0 +1,54 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsIn, IsOptional, IsString } from 'class-validator'; + +export class ReportQueryDto { + @ApiPropertyOptional({ description: 'Inclusive start date (YYYY-MM-DD). Default: 30 days ago.' }) + @IsOptional() + @IsString() + dateFrom?: string; + + @ApiPropertyOptional({ description: 'Inclusive end date (YYYY-MM-DD). Default: today.' }) + @IsOptional() + @IsString() + dateTo?: string; + + @ApiPropertyOptional({ enum: ['day', 'week', 'month'], default: 'day' }) + @IsOptional() + @IsIn(['day', 'week', 'month']) + granularity?: 'day' | 'week' | 'month'; + + @ApiPropertyOptional({ description: 'Comma-separated company UUIDs' }) + @IsOptional() + @IsString() + companyIds?: string; + + @ApiPropertyOptional({ description: 'Comma-separated route UUIDs' }) + @IsOptional() + @IsString() + routeIds?: string; + + @ApiPropertyOptional({ description: 'Comma-separated yard UUIDs (matches origin or destination)' }) + @IsOptional() + @IsString() + yardIds?: string; + + @ApiPropertyOptional({ description: 'Comma-separated cargo type UUIDs' }) + @IsOptional() + @IsString() + cargoTypeIds?: string; + + @ApiPropertyOptional({ description: 'Comma-separated status values (report-specific)' }) + @IsOptional() + @IsString() + statuses?: string; + + @ApiPropertyOptional({ description: 'Trade direction filter' }) + @IsOptional() + @IsString() + direction?: string; + + @ApiPropertyOptional({ enum: ['CONTAINER', 'BULK'] }) + @IsOptional() + @IsIn(['CONTAINER', 'BULK']) + freightType?: string; +} diff --git a/apps/edr-freight-api/src/modules/reports/dto/report-result.dto.ts b/apps/edr-freight-api/src/modules/reports/dto/report-result.dto.ts new file mode 100644 index 000000000..cc1c215af --- /dev/null +++ b/apps/edr-freight-api/src/modules/reports/dto/report-result.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class ReportKpiDto { + @ApiProperty() + label!: string; + + @ApiProperty() + value!: number; + + @ApiPropertyOptional() + unit?: string; +} + +export class ReportResultDto { + @ApiProperty({ type: [ReportKpiDto] }) + kpis!: ReportKpiDto[]; + + @ApiProperty({ + type: 'array', + items: { type: 'object', additionalProperties: true }, + description: 'Report rows; columns vary per report key', + }) + rows!: Record[]; +} diff --git a/apps/edr-freight-api/src/modules/reports/report-queries.ts b/apps/edr-freight-api/src/modules/reports/report-queries.ts new file mode 100644 index 000000000..9e4a6f617 --- /dev/null +++ b/apps/edr-freight-api/src/modules/reports/report-queries.ts @@ -0,0 +1,669 @@ +import { DataSource } from 'typeorm'; + +export interface ReportFilters { + /** ISO timestamp, inclusive lower bound. null = no lower bound (all time). */ + dateFrom: string | null; + /** ISO timestamp, exclusive upper bound. null = no upper bound. */ + dateTo: string | null; + granularity: 'day' | 'week' | 'month'; + companyIds: string[] | null; + routeIds: string[] | null; + yardIds: string[] | null; + cargoTypeIds: string[] | null; + statuses: string[] | null; + /** Trade-scope-resolved directions. null = unrestricted, [] = show nothing. */ + directions: string[] | null; + freightType: string | null; +} + +export interface ReportKpi { + label: string; + value: number; + unit?: string; +} + +export interface ReportResult { + kpis: ReportKpi[]; + rows: Record[]; +} + +type ReportQuery = (ds: DataSource, f: ReportFilters) => Promise; + +// For PER_ITEM bulk bookings cargo_total_weight_vgm holds an item COUNT, and +// the real tonnage lives in bulk_total_weight_tons — hence the COALESCE order. +const TONS = 'COALESCE(b.bulk_total_weight_tons, b.cargo_total_weight_vgm)'; +// adjusted_total_amount silently overrides total_amount when set. +const REVENUE = 'COALESCE(b.adjusted_total_amount, b.total_amount)'; +// GENERAL contract_kind rows are umbrella contracts, not shipments; counting +// them double-counts every child booking (same guard as overview.repository). +const NOT_UMBRELLA = "(b.contract_kind IS NULL OR b.contract_kind <> 'GENERAL')"; +const DEAD_STATUSES = "'DRAFT','CANCELLED','REJECTED','EXPIRED'"; + +const num = (v: unknown): number => (v === null || v === undefined ? 0 : Number(v)); +const sum = (rows: Record[], col: string): number => + rows.reduce((acc, r) => acc + num(r[col]), 0); + +/** + * Shared WHERE for booking-based reports (alias `b`). + * Params occupy $1..$8 in this fixed order; report SQL continues at $9. + */ +function bookingWhere(f: ReportFilters): { where: string; params: unknown[] } { + return { + where: ` + b.deleted_at IS NULL + AND ${NOT_UMBRELLA} + AND ($1::timestamptz IS NULL OR b.created_at >= $1) + AND ($2::timestamptz IS NULL OR b.created_at < $2) + AND ($3::uuid[] IS NULL OR b.company_id = ANY($3)) + AND ($4::uuid[] IS NULL OR b.cargo_type_id = ANY($4)) + AND ($5::text[] IS NULL OR b.trade_direction = ANY($5)) + AND ($6::text IS NULL OR b.freight_type = $6) + AND (CASE WHEN $7::text[] IS NULL + THEN b.status NOT IN (${DEAD_STATUSES}) + ELSE b.status = ANY($7) END) + AND ($8::uuid[] IS NULL OR b.origin_yard_id = ANY($8) OR b.destination_yard_id = ANY($8))`, + params: [ + f.dateFrom, + f.dateTo, + f.companyIds, + f.cargoTypeIds, + f.directions, + f.freightType, + f.statuses, + f.yardIds, + ], + }; +} + +/** + * Direction scope for rows that reference a booking through a varchar id + * column (invoices.source_id, payments.ref_id). Rows not pointing at a + * booking stay visible — they carry no direction to scope by. + * (Positional-param port of trade-scope.util's bookingRefScopeSql.) + */ +const refDirScope = (refColumn: string, param: string): string => ` + (${param}::text[] IS NULL OR NOT EXISTS ( + SELECT 1 FROM freight.bookings sb + WHERE sb.id::text = ${refColumn} AND NOT (sb.trade_direction = ANY(${param}))))`; + +const bookingsTrend: ReportQuery = async (ds, f) => { + const { where, params } = bookingWhere(f); + const rows = await ds.query( + `SELECT to_char(date_trunc($9, b.created_at), 'YYYY-MM-DD') AS period, + COUNT(*)::int AS bookings, + ROUND(COALESCE(SUM(${TONS}), 0))::float8 AS tons, + ROUND(COALESCE(SUM(${REVENUE}), 0))::float8 AS revenue + FROM freight.bookings b + WHERE ${where} + GROUP BY 1 ORDER BY 1`, + [...params, f.granularity], + ); + return { + kpis: [ + { label: 'Bookings', value: sum(rows, 'bookings') }, + { label: 'Tonnage', value: sum(rows, 'tons'), unit: 't' }, + { label: 'Revenue', value: sum(rows, 'revenue'), unit: 'ETB' }, + ], + rows, + }; +}; + +const revenueByCustomer: ReportQuery = async (ds, f) => { + const { where, params } = bookingWhere(f); + const rows = await ds.query( + `SELECT c.name AS customer, + COUNT(*)::int AS bookings, + ROUND(COALESCE(SUM(${TONS}), 0))::float8 AS tons, + ROUND(COALESCE(SUM(${REVENUE}), 0))::float8 AS revenue + FROM freight.bookings b + JOIN freight.companies c ON c.id = b.company_id + WHERE ${where} + GROUP BY c.name ORDER BY revenue DESC LIMIT 100`, + params, + ); + const total = sum(rows, 'revenue'); + return { + kpis: [ + { label: 'Customers', value: rows.length }, + { label: 'Revenue', value: total, unit: 'ETB' }, + { + label: 'Top customer share', + value: total > 0 ? Math.round((num(rows[0]?.revenue) / total) * 100) : 0, + unit: '%', + }, + ], + rows, + }; +}; + +const revenueByLane: ReportQuery = async (ds, f) => { + const { where, params } = bookingWhere(f); + const rows = await ds.query( + `SELECT o.label AS origin, d.label AS destination, + COUNT(*)::int AS bookings, + ROUND(COALESCE(SUM(${TONS}), 0))::float8 AS tons, + ROUND(COALESCE(SUM(${REVENUE}), 0))::float8 AS revenue + FROM freight.bookings b + JOIN freight.yards o ON o.id = b.origin_yard_id + JOIN freight.yards d ON d.id = b.destination_yard_id + WHERE ${where} + GROUP BY 1, 2 ORDER BY revenue DESC LIMIT 100`, + params, + ); + return { + kpis: [ + { label: 'Lanes', value: rows.length }, + { label: 'Tonnage', value: sum(rows, 'tons'), unit: 't' }, + { label: 'Revenue', value: sum(rows, 'revenue'), unit: 'ETB' }, + ], + rows, + }; +}; + +const contractUtilization: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT ct.reference, c.name AS customer, ct.status, ct.contract_kind AS kind, + to_char(ct.contract_valid_from, 'YYYY-MM-DD') AS valid_from, + to_char(ct.contract_valid_until, 'YYYY-MM-DD') AS valid_until, + cap.committed::float8 AS committed, + booked.tons::float8 AS booked_tons, + booked.cnt AS bookings, + CASE WHEN cap.committed > 0 + THEN ROUND(booked.tons / cap.committed * 100)::float8 END AS utilization_pct + FROM freight.contracts ct + LEFT JOIN freight.companies c ON c.id = ct.company_id + LEFT JOIN LATERAL ( + SELECT COALESCE(SUM(s.quantity_cap), 0) AS committed + FROM freight.contract_cargo_scope s + WHERE s.contract_id = ct.id AND s.deleted_at IS NULL) cap ON true + LEFT JOIN LATERAL ( + SELECT COALESCE(SUM(${TONS}), 0) AS tons, COUNT(*)::int AS cnt + FROM freight.bookings b + WHERE b.contract_id = ct.id AND b.deleted_at IS NULL + AND b.status NOT IN (${DEAD_STATUSES})) booked ON true + WHERE ct.deleted_at IS NULL + AND ct.status NOT IN ('DRAFT') + AND ct.contract_valid_from < COALESCE($2::timestamptz, 'infinity') + AND (ct.contract_valid_until IS NULL + OR ct.contract_valid_until >= COALESCE($1::timestamptz, '-infinity')) + AND ($3::uuid[] IS NULL OR ct.company_id = ANY($3)) + AND ($4::text[] IS NULL OR ct.trade_direction = ANY($4)) + AND ($5::text[] IS NULL OR ct.status = ANY($5)) + ORDER BY utilization_pct DESC NULLS LAST LIMIT 200`, + [f.dateFrom, f.dateTo, f.companyIds, f.directions, f.statuses], + ); + const capped = rows.filter((r: Record) => num(r.committed) > 0); + return { + kpis: [ + { label: 'Contracts', value: rows.length }, + { + label: 'Avg utilization', + value: capped.length + ? Math.round(sum(capped, 'utilization_pct') / capped.length) + : 0, + unit: '%', + }, + { label: 'Booked tonnage', value: sum(rows, 'booked_tons'), unit: 't' }, + ], + rows, + }; +}; + +// ponytail: 60-min departure grace is a constant; make it a query param if ops +// ever wants a configurable threshold. +const trainOnTime: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT o.label AS origin, d.label AS destination, + COUNT(*)::int AS trips, + COUNT(*) FILTER (WHERE ts.actual_departure_at IS NOT NULL)::int AS departed, + ROUND(AVG(EXTRACT(EPOCH FROM (ts.actual_departure_at - ts.scheduled_departure_date)) / 60) + FILTER (WHERE ts.actual_departure_at IS NOT NULL))::float8 AS avg_dep_delay_min, + ROUND(AVG(EXTRACT(EPOCH FROM (ts.actual_arrival_at - ts.scheduled_arrival_date)) / 60) + FILTER (WHERE ts.actual_arrival_at IS NOT NULL + AND ts.scheduled_arrival_date IS NOT NULL))::float8 AS avg_arr_delay_min, + ROUND(100.0 * COUNT(*) FILTER (WHERE ts.actual_departure_at + <= ts.scheduled_departure_date + interval '60 minutes') + / NULLIF(COUNT(*) FILTER (WHERE ts.actual_departure_at IS NOT NULL), 0))::float8 AS on_time_pct + FROM freight.train_schedules ts + JOIN freight.yards o ON o.id = ts.origin_station_id + JOIN freight.yards d ON d.id = ts.destination_station_id + WHERE ts.deleted_at IS NULL + AND ts.status IN ('DISPATCHED', 'ARRIVED') + AND ($1::timestamptz IS NULL OR ts.scheduled_departure_date >= $1) + AND ($2::timestamptz IS NULL OR ts.scheduled_departure_date < $2) + AND ($3::uuid[] IS NULL OR ts.route_id = ANY($3)) + AND ($4::text[] IS NULL OR ts.direction = ANY($4)) + AND ($5::uuid[] IS NULL OR ts.origin_station_id = ANY($5) OR ts.destination_station_id = ANY($5)) + GROUP BY 1, 2 ORDER BY trips DESC`, + [f.dateFrom, f.dateTo, f.routeIds, f.directions, f.yardIds], + ); + const departed = sum(rows, 'departed'); + const weighted = rows.reduce( + (acc: number, r: Record) => + acc + (num(r.on_time_pct) * num(r.departed)) / 100, + 0, + ); + return { + kpis: [ + { label: 'Trips', value: sum(rows, 'trips') }, + { + label: 'On-time departures', + value: departed > 0 ? Math.round((weighted / departed) * 100) : 0, + unit: '%', + }, + { + label: 'Avg departure delay', + value: rows.length ? Math.round(sum(rows, 'avg_dep_delay_min') / rows.length) : 0, + unit: 'min', + }, + ], + rows, + }; +}; + +const scheduleFillRate: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT ts.train_number, ts.reference, + to_char(ts.scheduled_departure_date, 'YYYY-MM-DD') AS departure, + o.label AS origin, d.label AS destination, ts.direction, ts.status, + ts.max_wagons, tset.wagon_count, + ROUND(w.cap_tons)::float8 AS capacity_tons, + ROUND(w.booked_tons)::float8 AS booked_tons, + CASE WHEN w.cap_tons > 0 + THEN ROUND(w.booked_tons / w.cap_tons * 100)::float8 END AS fill_pct + FROM freight.train_schedules ts + JOIN freight.yards o ON o.id = ts.origin_station_id + JOIN freight.yards d ON d.id = ts.destination_station_id + LEFT JOIN freight.train_sets tset ON tset.id = ts.train_set_id + LEFT JOIN LATERAL ( + SELECT COALESCE(SUM(tw.capacity_tons), 0) AS cap_tons, + COALESCE(SUM(tw.assigned_weight_tons), 0) AS booked_tons + FROM freight.train_set_wagons tw + WHERE tw.train_set_id = ts.train_set_id AND tw.deleted_at IS NULL) w ON true + WHERE ts.deleted_at IS NULL + AND ts.status <> 'CANCELLED' + AND ($1::timestamptz IS NULL OR ts.scheduled_departure_date >= $1) + AND ($2::timestamptz IS NULL OR ts.scheduled_departure_date < $2) + AND ($3::uuid[] IS NULL OR ts.route_id = ANY($3)) + AND ($4::text[] IS NULL OR ts.direction = ANY($4)) + AND ($5::uuid[] IS NULL OR ts.origin_station_id = ANY($5) OR ts.destination_station_id = ANY($5)) + ORDER BY ts.scheduled_departure_date DESC LIMIT 200`, + [f.dateFrom, f.dateTo, f.routeIds, f.directions, f.yardIds], + ); + const withCap = rows.filter((r: Record) => num(r.capacity_tons) > 0); + const capTons = sum(withCap, 'capacity_tons'); + return { + kpis: [ + { label: 'Schedules', value: rows.length }, + { + label: 'Avg fill rate', + value: capTons > 0 ? Math.round((sum(withCap, 'booked_tons') / capTons) * 100) : 0, + unit: '%', + }, + { label: 'Booked tonnage', value: sum(rows, 'booked_tons'), unit: 't' }, + ], + rows, + }; +}; + +const tripsPerRoute: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT o.label AS origin, d.label AS destination, ts.direction, + COUNT(*)::int AS trips, + ROUND(COALESCE(SUM(w.booked_tons), 0))::float8 AS tons_hauled, + ROUND(COALESCE(AVG(w.booked_tons), 0))::float8 AS avg_tons_per_trip + FROM freight.train_schedules ts + JOIN freight.yards o ON o.id = ts.origin_station_id + JOIN freight.yards d ON d.id = ts.destination_station_id + LEFT JOIN LATERAL ( + SELECT COALESCE(SUM(tw.assigned_weight_tons), 0) AS booked_tons + FROM freight.train_set_wagons tw + WHERE tw.train_set_id = ts.train_set_id AND tw.deleted_at IS NULL) w ON true + WHERE ts.deleted_at IS NULL + AND ts.status IN ('DISPATCHED', 'ARRIVED') + AND ($1::timestamptz IS NULL OR ts.scheduled_departure_date >= $1) + AND ($2::timestamptz IS NULL OR ts.scheduled_departure_date < $2) + AND ($3::uuid[] IS NULL OR ts.route_id = ANY($3)) + AND ($4::text[] IS NULL OR ts.direction = ANY($4)) + AND ($5::uuid[] IS NULL OR ts.origin_station_id = ANY($5) OR ts.destination_station_id = ANY($5)) + GROUP BY 1, 2, 3 ORDER BY trips DESC`, + [f.dateFrom, f.dateTo, f.routeIds, f.directions, f.yardIds], + ); + return { + kpis: [ + { label: 'Trips', value: sum(rows, 'trips') }, + { label: 'Routes served', value: rows.length }, + { label: 'Tonnage hauled', value: sum(rows, 'tons_hauled'), unit: 't' }, + ], + rows, + }; +}; + +const invoicedVsCollected: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT to_char(date_trunc($5, COALESCE(i.issued_at, i.created_at)), 'YYYY-MM-DD') AS period, + COUNT(*)::int AS invoices, + ROUND(SUM(i.total_amount))::float8 AS invoiced, + ROUND(SUM(i.paid_amount))::float8 AS collected, + ROUND(SUM(i.balance_amount))::float8 AS outstanding + FROM freight.invoices i + WHERE i.deleted_at IS NULL + AND i.status NOT IN ('DRAFT', 'CANCELLED') + AND ($1::timestamptz IS NULL OR COALESCE(i.issued_at, i.created_at) >= $1) + AND ($2::timestamptz IS NULL OR COALESCE(i.issued_at, i.created_at) < $2) + AND ($3::uuid[] IS NULL OR i.company_id = ANY($3)) + AND ${refDirScope('i.source_id', '$4')} + GROUP BY 1 ORDER BY 1`, + [f.dateFrom, f.dateTo, f.companyIds, f.directions, f.granularity], + ); + const invoiced = sum(rows, 'invoiced'); + const collected = sum(rows, 'collected'); + return { + kpis: [ + { label: 'Invoiced', value: invoiced, unit: 'ETB' }, + { label: 'Collected', value: collected, unit: 'ETB' }, + { + label: 'Collection rate', + value: invoiced > 0 ? Math.round((collected / invoiced) * 100) : 0, + unit: '%', + }, + { label: 'Outstanding', value: sum(rows, 'outstanding'), unit: 'ETB' }, + ], + rows, + }; +}; + +// Aging is an as-of snapshot: dateTo is the as-of moment (default now), +// dateFrom is ignored. +const agingReceivables: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT c.name AS customer, + COUNT(*)::int AS invoices, + ROUND(SUM(i.balance_amount))::float8 AS outstanding, + ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at >= COALESCE($1::timestamptz, now())), 0))::float8 AS current, + ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < COALESCE($1::timestamptz, now()) + AND i.due_at >= COALESCE($1::timestamptz, now()) - interval '30 days'), 0))::float8 AS overdue_0_30, + ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < COALESCE($1::timestamptz, now()) - interval '30 days' + AND i.due_at >= COALESCE($1::timestamptz, now()) - interval '60 days'), 0))::float8 AS overdue_31_60, + ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < COALESCE($1::timestamptz, now()) - interval '60 days' + AND i.due_at >= COALESCE($1::timestamptz, now()) - interval '90 days'), 0))::float8 AS overdue_61_90, + ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < COALESCE($1::timestamptz, now()) - interval '90 days'), 0))::float8 AS overdue_90_plus + FROM freight.invoices i + JOIN freight.companies c ON c.id = i.company_id + WHERE i.deleted_at IS NULL + AND i.status IN ('ISSUED', 'PENDING', 'PARTIALLY_PAID', 'OVERDUE') + AND i.balance_amount > 0 + AND ($1::timestamptz IS NULL OR i.created_at < $1) + AND ($2::uuid[] IS NULL OR i.company_id = ANY($2)) + AND ${refDirScope('i.source_id', '$3')} + GROUP BY 1 ORDER BY outstanding DESC LIMIT 200`, + [f.dateTo, f.companyIds, f.directions], + ); + const outstanding = sum(rows, 'outstanding'); + return { + kpis: [ + { label: 'Outstanding', value: outstanding, unit: 'ETB' }, + { label: 'Overdue', value: outstanding - sum(rows, 'current'), unit: 'ETB' }, + { label: 'Customers with balance', value: rows.length }, + ], + rows, + }; +}; + +const revenueByPaymentMethod: ReportQuery = async (ds, f) => { + // payments.status values are lowercase-hyphenated ('success'), unlike every + // other status enum in the schema. No deleted_at on this table. + const rows = await ds.query( + `SELECT p.method::text AS method, + COUNT(*)::int AS payments, + ROUND(SUM(p.amount))::float8 AS amount + FROM freight.payments p + WHERE p.status = 'success' + AND ($1::timestamptz IS NULL OR p.created_at >= $1) + AND ($2::timestamptz IS NULL OR p.created_at < $2) + AND ${refDirScope('p.ref_id', '$3')} + GROUP BY 1 ORDER BY amount DESC`, + [f.dateFrom, f.dateTo, f.directions], + ); + const total = sum(rows, 'amount'); + return { + kpis: [ + { label: 'Collected', value: total, unit: 'ETB' }, + { label: 'Payments', value: sum(rows, 'payments') }, + { + label: 'Top method share', + value: total > 0 ? Math.round((num(rows[0]?.amount) / total) * 100) : 0, + unit: '%', + }, + ], + rows, + }; +}; + +// --------------------------------------------------------------------------- +// Record-level list exports. Same engine, raw rows instead of aggregates. +// ponytail: flat LIMIT 5000 per list — stream/paginate the export if a table +// ever outgrows that. +const LIST_LIMIT = 5000; + +const bookingsList: ReportQuery = async (ds, f) => { + const { where, params } = bookingWhere(f); + const rows = await ds.query( + `SELECT b.reference, + to_char(b.created_at, 'YYYY-MM-DD') AS created, + c.name AS customer, b.status, b.freight_type, + b.trade_direction AS direction, + o.label AS origin, d.label AS destination, + COALESCE(cty.cargo_type_name, b.cargo_free_text) AS cargo, + ROUND(${TONS})::float8 AS tons, + ROUND(${REVENUE})::float8 AS amount, + b.payment_status, b.scheduling_status + FROM freight.bookings b + JOIN freight.companies c ON c.id = b.company_id + JOIN freight.yards o ON o.id = b.origin_yard_id + JOIN freight.yards d ON d.id = b.destination_yard_id + LEFT JOIN freight.cargo_types cty ON cty.id = b.cargo_type_id + WHERE ${where} + ORDER BY b.created_at DESC LIMIT ${LIST_LIMIT}`, + params, + ); + return { + kpis: [ + { label: 'Bookings', value: rows.length }, + { label: 'Tonnage', value: sum(rows, 'tons'), unit: 't' }, + { label: 'Amount', value: sum(rows, 'amount'), unit: 'ETB' }, + ], + rows, + }; +}; + +const contractsList: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT ct.reference, c.name AS customer, ct.contract_kind AS kind, + ct.status, ct.trade_direction AS direction, ct.freight_type, + to_char(ct.contract_valid_from, 'YYYY-MM-DD') AS valid_from, + to_char(ct.contract_valid_until, 'YYYY-MM-DD') AS valid_until, + to_char(ct.created_at, 'YYYY-MM-DD') AS created + FROM freight.contracts ct + LEFT JOIN freight.companies c ON c.id = ct.company_id + WHERE ct.deleted_at IS NULL + AND ($1::timestamptz IS NULL OR ct.created_at >= $1) + AND ($2::timestamptz IS NULL OR ct.created_at < $2) + AND ($3::uuid[] IS NULL OR ct.company_id = ANY($3)) + AND ($4::text[] IS NULL OR ct.trade_direction = ANY($4)) + AND ($5::text[] IS NULL OR ct.status = ANY($5)) + ORDER BY ct.created_at DESC LIMIT ${LIST_LIMIT}`, + [f.dateFrom, f.dateTo, f.companyIds, f.directions, f.statuses], + ); + const active = rows.filter((r: Record) => + ['CONTRACT_ACTIVE', 'ACTIVE_SHIPMENT_IN_PROGRESS'].includes(String(r.status)), + ).length; + return { + kpis: [ + { label: 'Contracts', value: rows.length }, + { label: 'Active', value: active }, + ], + rows, + }; +}; + +const schedulesList: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT ts.train_number, ts.reference, ts.direction, ts.status, + o.label AS origin, d.label AS destination, + to_char(ts.scheduled_departure_date, 'YYYY-MM-DD HH24:MI') AS scheduled_departure, + to_char(ts.actual_departure_at, 'YYYY-MM-DD HH24:MI') AS actual_departure, + to_char(ts.scheduled_arrival_date, 'YYYY-MM-DD HH24:MI') AS scheduled_arrival, + to_char(ts.actual_arrival_at, 'YYYY-MM-DD HH24:MI') AS actual_arrival, + ts.max_wagons, tset.wagon_count + FROM freight.train_schedules ts + JOIN freight.yards o ON o.id = ts.origin_station_id + JOIN freight.yards d ON d.id = ts.destination_station_id + LEFT JOIN freight.train_sets tset ON tset.id = ts.train_set_id + WHERE ts.deleted_at IS NULL + AND ($1::timestamptz IS NULL OR ts.scheduled_departure_date >= $1) + AND ($2::timestamptz IS NULL OR ts.scheduled_departure_date < $2) + AND ($3::text[] IS NULL OR ts.direction = ANY($3)) + AND ($4::text[] IS NULL OR ts.status = ANY($4)) + AND ($5::uuid[] IS NULL OR ts.origin_station_id = ANY($5) OR ts.destination_station_id = ANY($5)) + ORDER BY ts.scheduled_departure_date DESC LIMIT ${LIST_LIMIT}`, + [f.dateFrom, f.dateTo, f.directions, f.statuses, f.yardIds], + ); + const count = (s: string) => + rows.filter((r: Record) => r.status === s).length; + return { + kpis: [ + { label: 'Schedules', value: rows.length }, + { label: 'Dispatched', value: count('DISPATCHED') }, + { label: 'Arrived', value: count('ARRIVED') }, + ], + rows, + }; +}; + +const fleetWagons: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT w.wagon_number, wt.name AS type, + wt.capacity_tons::float8 AS capacity_tons, + w.status, y.label AS current_yard + FROM freight.wagons w + JOIN freight.wagon_types wt ON wt.id = w.wagon_type_id + LEFT JOIN freight.yards y ON y.id = w.current_yard_id + WHERE w.deleted_at IS NULL + AND ($1::text[] IS NULL OR w.status = ANY($1)) + AND ($2::uuid[] IS NULL OR w.current_yard_id = ANY($2)) + ORDER BY w.wagon_number LIMIT ${LIST_LIMIT}`, + [f.statuses, f.yardIds], + ); + const count = (s: string) => + rows.filter((r: Record) => r.status === s).length; + return { + kpis: [ + { label: 'Wagons', value: rows.length }, + { label: 'Available', value: count('AVAILABLE') }, + { label: 'Assigned', value: count('ASSIGNED') }, + { label: 'Maintenance', value: count('MAINTENANCE') }, + ], + rows, + }; +}; + +const fleetLocomotives: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT l.code, l.name, l.locomotive_type, + l.max_pull_weight_tons::float8 AS max_pull_tons, + l.status, y.label AS current_yard + FROM freight.locomotives l + LEFT JOIN freight.yards y ON y.id = l.current_yard_id + WHERE l.deleted_at IS NULL + AND ($1::text[] IS NULL OR l.status = ANY($1)) + AND ($2::uuid[] IS NULL OR l.current_yard_id = ANY($2)) + ORDER BY l.code LIMIT ${LIST_LIMIT}`, + [f.statuses, f.yardIds], + ); + const available = rows.filter( + (r: Record) => r.status === 'AVAILABLE', + ).length; + return { + kpis: [ + { label: 'Locomotives', value: rows.length }, + { label: 'Available', value: available }, + ], + rows, + }; +}; + +const customersList: ReportQuery = async (ds, f) => { + const rows = await ds.query( + `SELECT c.name, c.type, c.kind, c.status, c.tin, + to_char(c.approved_at, 'YYYY-MM-DD') AS approved, + to_char(c.created_at, 'YYYY-MM-DD') AS created + FROM freight.companies c + WHERE c.deleted_at IS NULL + AND ($1::timestamptz IS NULL OR c.created_at >= $1) + AND ($2::timestamptz IS NULL OR c.created_at < $2) + AND ($3::text[] IS NULL OR c.status = ANY($3)) + ORDER BY c.created_at DESC LIMIT ${LIST_LIMIT}`, + [f.dateFrom, f.dateTo, f.statuses], + ); + const active = rows.filter( + (r: Record) => r.status === 'active', + ).length; + return { + kpis: [ + { label: 'Customers', value: rows.length }, + { label: 'Active', value: active }, + ], + rows, + }; +}; + +const paymentsList: ReportQuery = async (ds, f) => { + // No deleted_at on freight.payments; statuses are lowercase-hyphenated. + const rows = await ds.query( + `SELECT to_char(p.created_at, 'YYYY-MM-DD HH24:MI') AS created, + p.method::text AS method, p.status::text AS status, + p.currency::text AS currency, + ROUND(p.amount)::float8 AS amount, + p.transaction_id, p.merchant_order_id, + to_char(p.paid_at, 'YYYY-MM-DD') AS paid + FROM freight.payments p + WHERE ($1::timestamptz IS NULL OR p.created_at >= $1) + AND ($2::timestamptz IS NULL OR p.created_at < $2) + AND ($3::text[] IS NULL OR p.status::text = ANY($3)) + AND ${refDirScope('p.ref_id', '$4')} + ORDER BY p.created_at DESC LIMIT ${LIST_LIMIT}`, + [f.dateFrom, f.dateTo, f.statuses, f.directions], + ); + const success = rows.filter( + (r: Record) => r.status === 'success', + ); + return { + kpis: [ + { label: 'Payments', value: rows.length }, + { label: 'Successful', value: success.length }, + { label: 'Collected', value: sum(success, 'amount'), unit: 'ETB' }, + ], + rows, + }; +}; + +export const REPORT_QUERIES: Record = { + 'bookings-list': bookingsList, + 'contracts-list': contractsList, + 'schedules-list': schedulesList, + 'fleet-wagons': fleetWagons, + 'fleet-locomotives': fleetLocomotives, + 'customers-list': customersList, + 'payments-list': paymentsList, + 'bookings-trend': bookingsTrend, + 'revenue-by-customer': revenueByCustomer, + 'revenue-by-lane': revenueByLane, + 'contract-utilization': contractUtilization, + 'train-on-time': trainOnTime, + 'schedule-fill-rate': scheduleFillRate, + 'trips-per-route': tripsPerRoute, + 'invoiced-vs-collected': invoicedVsCollected, + 'aging-receivables': agingReceivables, + 'revenue-by-payment-method': revenueByPaymentMethod, +}; diff --git a/apps/edr-freight-api/src/modules/reports/reports.controller.ts b/apps/edr-freight-api/src/modules/reports/reports.controller.ts new file mode 100644 index 000000000..bc2671680 --- /dev/null +++ b/apps/edr-freight-api/src/modules/reports/reports.controller.ts @@ -0,0 +1,33 @@ +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '@edr/api-common'; +import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; + +import { BookingView } from '../../common/booking-guards'; +import { UserTradeAccessService } from '../user-trade-access/user-trade-access.service'; +import { ReportQueryDto } from './dto/report-query.dto'; +import { ReportResultDto } from './dto/report-result.dto'; +import { ReportsService } from './reports.service'; + +@ApiTags('Reports') +@ApiBearerAuth() +@Controller('reports') +export class ReportsController { + constructor( + private readonly reportsService: ReportsService, + private readonly userTradeAccessService: UserTradeAccessService, + ) {} + + @Get(':key') + @BookingView() + @ApiOperation({ summary: 'Run a canned report by key with optional filters' }) + @ApiOkResponse({ type: ReportResultDto }) + async run( + @Param('key') key: string, + @Query() query: ReportQueryDto, + @CurrentUser() user: TCurrentUser, + ): Promise { + const allowed = await this.userTradeAccessService.resolveAllowedDirections(user); + return this.reportsService.run(key, query, allowed); + } +} diff --git a/apps/edr-freight-api/src/modules/reports/reports.module.ts b/apps/edr-freight-api/src/modules/reports/reports.module.ts new file mode 100644 index 000000000..a7fe792a5 --- /dev/null +++ b/apps/edr-freight-api/src/modules/reports/reports.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; + +import { UserTradeAccessModule } from '../user-trade-access/user-trade-access.module'; +import { ReportsController } from './reports.controller'; +import { ReportsRepository } from './reports.repository'; +import { ReportsService } from './reports.service'; + +@Module({ + imports: [UserTradeAccessModule], + controllers: [ReportsController], + providers: [ReportsService, ReportsRepository], +}) +export class ReportsModule {} diff --git a/apps/edr-freight-api/src/modules/reports/reports.repository.ts b/apps/edr-freight-api/src/modules/reports/reports.repository.ts new file mode 100644 index 000000000..65f154b22 --- /dev/null +++ b/apps/edr-freight-api/src/modules/reports/reports.repository.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; + +import { REPORT_QUERIES, ReportFilters, ReportResult } from './report-queries'; + +@Injectable() +export class ReportsRepository { + constructor(@InjectDataSource() private readonly dataSource: DataSource) {} + + run(key: keyof typeof REPORT_QUERIES, filters: ReportFilters): Promise { + return REPORT_QUERIES[key](this.dataSource, filters); + } +} diff --git a/apps/edr-freight-api/src/modules/reports/reports.service.ts b/apps/edr-freight-api/src/modules/reports/reports.service.ts new file mode 100644 index 000000000..04e6e9a60 --- /dev/null +++ b/apps/edr-freight-api/src/modules/reports/reports.service.ts @@ -0,0 +1,46 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; + +import { scopedDirections } from '../user-trade-access/trade-scope.util'; +import { ReportQueryDto } from './dto/report-query.dto'; +import { REPORT_QUERIES, ReportFilters, ReportResult } from './report-queries'; +import { ReportsRepository } from './reports.repository'; +import type { Freight } from '@edr/types'; + +const DAY_MS = 24 * 60 * 60 * 1000; + +const list = (csv?: string): string[] | null => { + const items = csv?.split(',').map((s) => s.trim()).filter(Boolean) ?? []; + return items.length ? items : null; +}; + +@Injectable() +export class ReportsService { + constructor(private readonly repository: ReportsRepository) {} + + run( + key: string, + dto: ReportQueryDto, + allowedDirections: Freight.ScheduleTradeDirection[] | null, + ): Promise { + if (!(key in REPORT_QUERIES)) { + throw new NotFoundException(`Unknown report: ${key}`); + } + // No default range: absent dates mean all time, so exports cover everything. + const to = dto.dateTo ? new Date(dto.dateTo) : null; + const from = dto.dateFrom ? new Date(dto.dateFrom) : null; + const filters: ReportFilters = { + dateFrom: from ? from.toISOString() : null, + // dateTo is inclusive in the API; queries treat the bound as exclusive. + dateTo: to ? new Date(to.getTime() + DAY_MS).toISOString() : null, + granularity: dto.granularity ?? 'day', + companyIds: list(dto.companyIds), + routeIds: list(dto.routeIds), + yardIds: list(dto.yardIds), + cargoTypeIds: list(dto.cargoTypeIds), + statuses: list(dto.statuses), + directions: scopedDirections(allowedDirections, dto.direction), + freightType: dto.freightType ?? null, + }; + return this.repository.run(key, filters); + } +} diff --git a/apps/edr-freight-api/src/modules/routes/purge-guard.spec.ts b/apps/edr-freight-api/src/modules/routes/purge-guard.spec.ts new file mode 100644 index 000000000..c737f18be --- /dev/null +++ b/apps/edr-freight-api/src/modules/routes/purge-guard.spec.ts @@ -0,0 +1,61 @@ +import { ConflictException, NotFoundException } from '@nestjs/common'; +import { RoutesService } from './routes.service'; + +const ROUTE = { + id: 'r1', + originYard: { code: 'ADD', label: 'Addis' }, + destinationYard: { code: 'DIR', label: 'Dire Dawa' }, + milestones: [], +}; + +const makeService = (scheduleCount: number, route: unknown = ROUTE) => { + const deletes: string[] = []; + const manager = { + getRepository: (entity: { name?: string }) => ({ + delete: async () => { + deletes.push(entity?.name ?? 'unknown'); + }, + }), + }; + const dataSource = { + query: jest.fn(async () => [{ count: scheduleCount }]), + transaction: jest.fn(async (cb: (m: unknown) => Promise) => cb(manager)), + }; + const routesRepository = {}; + const svc = new RoutesService(dataSource as never, routesRepository as never); + // findById is the service's own loader; stub it to isolate the purge guard. + (svc as unknown as { findById: (id: string) => Promise }).findById = + async () => { + if (!route) throw new NotFoundException('Route not found'); + return route; + }; + return { svc, dataSource, deletes }; +}; + +describe('RoutesService.purge', () => { + it('purges a route no schedule references', async () => { + const { svc, dataSource, deletes } = makeService(0); + await svc.purge('r1'); + expect(dataSource.transaction).toHaveBeenCalled(); + // Milestones then the route itself, inside one transaction. + expect(deletes).toHaveLength(2); + }); + + it('refuses while train schedules reference it', async () => { + const { svc, dataSource } = makeService(4); + await expect(svc.purge('r1')).rejects.toThrow(ConflictException); + await expect(svc.purge('r1')).rejects.toThrow(/4 train schedule\(s\)/); + expect(dataSource.transaction).not.toHaveBeenCalled(); + }); + + it('names the route in the refusal so the message is actionable', async () => { + const { svc } = makeService(1); + await expect(svc.purge('r1')).rejects.toThrow(/ADD|Addis/); + }); + + it('propagates a not-found route', async () => { + const { svc, dataSource } = makeService(0, null); + await expect(svc.purge('nope')).rejects.toThrow(NotFoundException); + expect(dataSource.transaction).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/edr-freight-api/src/modules/routes/routes.controller.ts b/apps/edr-freight-api/src/modules/routes/routes.controller.ts index 259dd4c9f..ed61f08b0 100644 --- a/apps/edr-freight-api/src/modules/routes/routes.controller.ts +++ b/apps/edr-freight-api/src/modules/routes/routes.controller.ts @@ -1,7 +1,23 @@ -import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, +} from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { FleetManage, FleetView } from '../../common/booking-guards'; +import { + BookingStaff, + FleetManage, + FleetView, +} from '../../common/booking-guards'; import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { CreateRouteDto } from './dto/create-route.dto'; import { FilterRoutesDto } from './dto/filter-routes.dto'; @@ -48,6 +64,21 @@ export class RoutesController { return this.routesService.update(id, dto); } + // Declared before @Delete(':id') so "permanent" is never captured as an id. + // BookingStaff, not FleetManage: the latter also accepts the coarse + // fleet:manage key, which would hand an irreversible purge to everyone who + // can edit the fleet. This action requires its own grant, nothing else. + @Delete(':id/permanent') + @BookingStaff(FREIGHT_PERMS.routes.hardDelete) + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + summary: + 'Permanently delete a route (irreversible; refused while any train schedule references it)', + }) + purge(@Param('id', ParseUUIDPipe) id: string) { + return this.routesService.purge(id); + } + @Delete(':id') @FleetManage(FREIGHT_PERMS.routes.delete) @ApiOperation({ summary: 'Deactivate route' }) diff --git a/apps/edr-freight-api/src/modules/routes/routes.service.ts b/apps/edr-freight-api/src/modules/routes/routes.service.ts index 8c2989b87..017c23551 100644 --- a/apps/edr-freight-api/src/modules/routes/routes.service.ts +++ b/apps/edr-freight-api/src/modules/routes/routes.service.ts @@ -206,6 +206,41 @@ export class RoutesService { return this.findById(id); } + /** + * Permanently purge a route — irreversible, and only for corridors nothing + * has run on: a mistyped or duplicated definition. + * + * `train_schedules.route_id` is NO ACTION, so Postgres would reject the + * delete with a raw constraint error; the schedules are counted up front + * instead so the refusal says what is blocking. The route's own milestones + * cascade with it, which is correct — they are the route's definition, not + * history that outlives it. Deactivating (`deactivate`) stays the answer for + * a corridor that has actually been used. + */ + async purge(id: string): Promise { + const route = await this.findById(id); + + const [schedules] = await this.dataSource.query>( + `SELECT count(*)::int AS count + FROM freight.train_schedules + WHERE route_id = $1`, + [id], + ); + + if (schedules?.count > 0) { + throw new ConflictException( + `Route ${formatRouteLabel(route)} cannot be permanently deleted — ${schedules.count} train schedule(s) still reference it. Deactivate it instead, which keeps the history intact.`, + ); + } + + await this.dataSource.transaction(async (manager) => { + // Milestones are FK-cascaded, but delete them explicitly so the intent is + // visible here rather than depending on the constraint alone. + await manager.getRepository(RouteMilestone).delete({ routeId: id }); + await manager.getRepository(Route).delete({ id }); + }); + } + /** * A route IS its ordered stop list — "Addis → Adama → Dire Dawa" and * "Addis → Dire Dawa" share endpoints but are different corridors. So the diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-cargo-type.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-cargo-type.dto.ts index 6d9cb82a2..eefc560e8 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-cargo-type.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-cargo-type.dto.ts @@ -44,6 +44,19 @@ export class CreateCargoTypeDto { @IsObject() itemsPerWagonMap?: Record | null; + @ApiPropertyOptional({ + description: + 'PER_TON cargo only: the most tons of this cargo one wagon may carry, keyed by ' + + 'wagon-type id (e.g. { "": 50 } loads sugar 50T on a 70T wagon, so 200T ' + + 'takes 4 wagons). Optional — omit a wagon type to use its full rated capacity. ' + + 'Rejected when it exceeds that wagon type\'s rated capacity.', + type: 'object', + additionalProperties: { type: 'number', minimum: 0.001 }, + }) + @IsOptional() + @IsObject() + tonsPerWagonMap?: Record | null; + @ApiPropertyOptional({ default: false }) @IsOptional() @IsBoolean() diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-rate.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-rate.dto.ts index 4f9d06870..45a5838bd 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-rate.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-rate.dto.ts @@ -8,7 +8,8 @@ import { } from '../entities/rate.entity'; const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'BOTH'] as const; -const CURRENCIES = ['USD'] as const; +// ETB is accepted only for last-mile rates; the service forces USD elsewhere. +const CURRENCIES = ['USD', 'ETB'] as const; export const INTERCITY_KINDS = ['CONTAINER', 'BULK'] as const; export const CARGO_KINDS = ['CONTAINER', 'BULK'] as const; @@ -92,6 +93,28 @@ export class CreateRateDto { @IsOptional() @IsIn([...RATE_UNITS]) rateUnit?: string; + + @ApiPropertyOptional({ + description: + 'Distance band start (km, inclusive). Container last-mile rates only (rateUnit = PER_KM).', + minimum: 0, + }) + @IsOptional() + @IsNumber() + @Min(0) + @Transform(({ value }) => (value === null || value === undefined || value === '' ? undefined : Number(value))) + minKm?: number; + + @ApiPropertyOptional({ + description: + 'Distance band end (km, exclusive). Null/omitted = open-ended band. Container last-mile rates only.', + minimum: 0, + }) + @IsOptional() + @IsNumber() + @Min(0) + @Transform(({ value }) => (value === null || value === undefined || value === '' ? undefined : Number(value))) + maxKm?: number; } export class SubmitRateForApprovalDto { diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-yard.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-yard.dto.ts index 295e9e72b..94dbd2fb8 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-yard.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-yard.dto.ts @@ -26,6 +26,38 @@ export class CreateYardDto { @IsBoolean() hasFacility?: boolean; + @ApiPropertyOptional({ + default: false, + description: 'Facility can load containers onto a train (contract origin side)', + }) + @IsOptional() + @IsBoolean() + hasContainerFacilityOrigin?: boolean; + + @ApiPropertyOptional({ + default: false, + description: 'Facility can load bulk onto a train (contract origin side)', + }) + @IsOptional() + @IsBoolean() + hasBulkFacilityOrigin?: boolean; + + @ApiPropertyOptional({ + default: false, + description: 'Facility can receive containers off a train (contract destination side)', + }) + @IsOptional() + @IsBoolean() + hasContainerFacilityDestination?: boolean; + + @ApiPropertyOptional({ + default: false, + description: 'Facility can receive bulk off a train (contract destination side)', + }) + @IsOptional() + @IsBoolean() + hasBulkFacilityDestination?: boolean; + @ApiPropertyOptional({ default: 1, description: 'UI display sort order' }) @IsOptional() @IsInt() diff --git a/apps/edr-freight-api/src/modules/rule-engine/entities/cargo-type.entity.ts b/apps/edr-freight-api/src/modules/rule-engine/entities/cargo-type.entity.ts index b096613bf..e685f3a2d 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/entities/cargo-type.entity.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/entities/cargo-type.entity.ts @@ -60,6 +60,17 @@ export class CargoType extends BaseEntity { @Column({ name: 'items_per_wagon_map', type: 'jsonb', nullable: true }) itemsPerWagonMap?: Record | null; + /** + * PER_TON (bulk) only: the most tons of THIS cargo that may ride one wagon of + * each allowed type, keyed by wagon-type id (e.g. sugar → { NW5: 50 } on a + * 70T wagon). Caps both the wagon count and how much each wagon is loaded, so + * 200T of sugar takes 4 wagons at 50T rather than 3 at 70T. A missing key (or + * a null map) means the wagon's full rated capacity — unlike itemsPerWagonMap + * this is optional, so cargo without a loading limit is unaffected. + */ + @Column({ name: 'tons_per_wagon_map', type: 'jsonb', nullable: true }) + tonsPerWagonMap?: Record | null; + @Column({ name: 'requires_director_approval', type: 'boolean', default: false }) requiresDirectorApproval!: boolean; diff --git a/apps/edr-freight-api/src/modules/rule-engine/entities/rate-unit.util.ts b/apps/edr-freight-api/src/modules/rule-engine/entities/rate-unit.util.ts index 1de36bcfd..a3d336605 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/entities/rate-unit.util.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/entities/rate-unit.util.ts @@ -93,8 +93,12 @@ function unitsForShape(input: { case 'INTERCITY': return ['PER_CONTAINER', 'PER_TON', 'PER_WAGON', 'PER_KM']; case 'FIRST_MILE': - case 'LAST_MILE': return ['PER_CONTAINER', 'PER_TON', 'PER_KM', 'FLAT']; + case 'LAST_MILE': + // PER_KM = container mode (banded by distance + container size), + // PER_TON_KM = bulk mode (tons × km × rate). Legacy units kept for + // existing rows. + return ['PER_KM', 'PER_TON_KM', 'PER_CONTAINER', 'PER_TON', 'FLAT']; default: return ['FLAT']; } diff --git a/apps/edr-freight-api/src/modules/rule-engine/entities/rate.entity.ts b/apps/edr-freight-api/src/modules/rule-engine/entities/rate.entity.ts index d62d5647f..e7089f08c 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/entities/rate.entity.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/entities/rate.entity.ts @@ -39,6 +39,8 @@ export const RATE_UNITS = [ 'PER_ITEM', 'PER_CONTAINER', 'PER_KM', + // Last-mile bulk: price = tons × km × rateValue. + 'PER_TON_KM', 'PER_INVOICE', 'FLAT', ] as const; @@ -156,6 +158,17 @@ export class Rate extends BaseEntity { @Column({ name: 'rate_unit', type: 'varchar', length: 30 }) rateUnit!: RateUnit; + /** + * Distance band for container last-mile rates (rateUnit = PER_KM, scoped by + * containerTypeId): the rate applies when minKm <= km < maxKm (maxKm NULL = + * open-ended). NULL on every other rate shape. + */ + @Column({ name: 'min_km', type: 'numeric', precision: 10, scale: 2, nullable: true }) + minKm?: number | null; + + @Column({ name: 'max_km', type: 'numeric', precision: 10, scale: 2, nullable: true }) + maxKm?: number | null; + @Column({ name: 'status', type: 'varchar', length: 20, default: 'DRAFT' }) status!: RateStatus; diff --git a/apps/edr-freight-api/src/modules/rule-engine/entities/yard-location.entity.ts b/apps/edr-freight-api/src/modules/rule-engine/entities/yard-location.entity.ts new file mode 100644 index 000000000..3d33b1998 --- /dev/null +++ b/apps/edr-freight-api/src/modules/rule-engine/entities/yard-location.entity.ts @@ -0,0 +1,26 @@ +import { BaseEntity } from '@edr/api-common'; +import { Column, Entity, Index, JoinColumn, OneToOne } from 'typeorm'; + +import { Yard } from './yard.entity'; + +/** + * GPS position of a yard (decimal degrees, WGS84). One record per yard; + * today only the five facility yards (Sebeta, GMP/Indode, Mojo, Adama, + * Dire Dawa) are seeded. + */ +@Entity({ schema: 'freight', name: 'yard_locations' }) +@Index(['yardId'], { unique: true }) +export class YardLocation extends BaseEntity { + @Column({ name: 'yard_id', type: 'uuid' }) + yardId!: string; + + @OneToOne(() => Yard, { nullable: false, onDelete: 'CASCADE' }) + @JoinColumn({ name: 'yard_id' }) + yard?: Yard; + + @Column({ name: 'latitude', type: 'double precision' }) + latitude!: number; + + @Column({ name: 'longitude', type: 'double precision' }) + longitude!: number; +} diff --git a/apps/edr-freight-api/src/modules/rule-engine/interfaces/rates.repository.interface.ts b/apps/edr-freight-api/src/modules/rule-engine/interfaces/rates.repository.interface.ts index 1570e0d23..9797ff715 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/interfaces/rates.repository.interface.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/interfaces/rates.repository.interface.ts @@ -21,6 +21,8 @@ export interface IRatesRepository { tradeDirection?: string | null; originYardId?: string | null; destinationYardId?: string | null; + /** Band start for container last-mile rates; omitted/null elsewhere. */ + minKm?: number | null; }): Promise; findAll(options?: FindManyOptions): Promise; findAndCount(options?: FindManyOptions): Promise<[Rate[], number]>; diff --git a/apps/edr-freight-api/src/modules/rule-engine/repositories/cargo-types.repository.ts b/apps/edr-freight-api/src/modules/rule-engine/repositories/cargo-types.repository.ts index 4df961aaa..07993c267 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/repositories/cargo-types.repository.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/repositories/cargo-types.repository.ts @@ -59,26 +59,57 @@ export class CargoTypesRepository implements ICargoTypesRepository { } async create(data: Partial): Promise { - const entity = this.repo.create(data); - return this.repo.save(entity); + const { wagonTypes, ...columns } = data; + const entity = this.repo.create(columns); + const saved = await this.repo.save(entity); + if (wagonTypes?.length) { + await this.syncWagonTypes(saved.id, wagonTypes.map((wt) => wt.id), []); + } + return (await this.findById(saved.id)) ?? saved; } async update(id: string, data: Partial): Promise { - // Relation lists can't ride a column UPDATE — sync them via entity save. const { wagonTypes, ...columns } = data; if (Object.keys(columns).length) { await this.repo.update(id, columns as never); } if (wagonTypes) { - const entity = await this.repo.findOne({ where: { id } }); - if (entity) { - entity.wagonTypes = wagonTypes; - await this.repo.save(entity); + const current = await this.repo.findOne({ + where: { id }, + relations: { wagonTypes: true }, + }); + if (current) { + await this.syncWagonTypes( + id, + wagonTypes.map((wt) => wt.id), + (current.wagonTypes ?? []).map((wt) => wt.id), + ); } } return this.findById(id); } + /** + * Diffs the wagon-type links through the relation query builder rather than + * an entity save: junction-row inserts from save() broadcast afterInsert with + * no entity attached, which the @tria-plc/auditlog subscriber (deployed + * builds) dereferences and crashes the request on. + */ + private async syncWagonTypes( + id: string, + nextIds: string[], + currentIds: string[], + ): Promise { + const toAdd = nextIds.filter((x) => !currentIds.includes(x)); + const toRemove = currentIds.filter((x) => !nextIds.includes(x)); + if (!toAdd.length && !toRemove.length) return; + await this.repo + .createQueryBuilder() + .relation(CargoType, 'wagonTypes') + .of(id) + .addAndRemove(toAdd, toRemove); + } + async softDelete(id: string): Promise { await this.repo.softDelete(id); } diff --git a/apps/edr-freight-api/src/modules/rule-engine/repositories/rates.repository.ts b/apps/edr-freight-api/src/modules/rule-engine/repositories/rates.repository.ts index 43c716a3b..7417987dc 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/repositories/rates.repository.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/repositories/rates.repository.ts @@ -74,6 +74,7 @@ export class RatesRepository implements IRatesRepository { tradeDirection?: string | null; originYardId?: string | null; destinationYardId?: string | null; + minKm?: number | null; }): Promise { const qb = this.repo .createQueryBuilder('rate') @@ -111,6 +112,13 @@ export class RatesRepository implements IRatesRepository { } else { qb.andWhere('rate.destination_yard_id IS NULL'); } + // Band start distinguishes sibling last-mile bands, mirroring the + // COALESCE(min_km, -1) column of UQ_rates_pattern. + if (pattern.minKm !== null && pattern.minKm !== undefined) { + qb.andWhere('rate.min_km = :minKm', { minKm: pattern.minKm }); + } else { + qb.andWhere('rate.min_km IS NULL'); + } return qb.getOne(); } diff --git a/apps/edr-freight-api/src/modules/rule-engine/rule-engine.module.ts b/apps/edr-freight-api/src/modules/rule-engine/rule-engine.module.ts index 691992c54..d487387b8 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/rule-engine.module.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/rule-engine.module.ts @@ -27,6 +27,7 @@ import { WeightLimitRule } from './entities/weight-limit-rule.entity'; import { Yard } from './entities/yard.entity'; import { YardDistance } from './entities/yard-distance.entity'; import { YardFacility } from './entities/yard-facility.entity'; +import { YardLocation } from './entities/yard-location.entity'; import { APPROVAL_RULES_REPOSITORY } from './interfaces/approval-rules.repository.interface'; import { CARGO_TYPES_REPOSITORY } from './interfaces/cargo-types.repository.interface'; @@ -68,6 +69,7 @@ import { YardFacilitiesService } from './services/yard-facilities.service'; import { RuleEngineService } from './rule-engine.service'; import { NotificationInboxModule } from '../notification-inbox/notification-inbox.module'; +import { WagonTypesModule } from '../wagon-types/wagon-types.module'; import { BookingCargoModifier } from '../bookings/entities/booking-cargo-modifier.entity'; import { BookingContainer } from '../bookings/entities/booking-container.entity'; @@ -87,6 +89,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot. Yard, YardDistance, YardFacility, + YardLocation, ShippingLine, Rate, ApprovalRule, @@ -96,6 +99,9 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot. ]), // Team notifications for the priority-rule approval workflow. NotificationInboxModule, + // Rated wagon capacities — cargo types validate their per-wagon tonnage cap + // against them (a cap above the rating is a typo, not a policy). + WagonTypesModule, ], controllers: [ CargoTypesController, diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/cargo-types.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/cargo-types.service.ts index 34287a023..1f25b0823 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/cargo-types.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/cargo-types.service.ts @@ -6,6 +6,7 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; +import { In } from 'typeorm'; import { generateCode } from '../../../common/utils/generate-code.util'; import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto'; import { ListCargoTypesQueryDto } from '../dto/list-rule-engine-query.dto'; @@ -13,6 +14,7 @@ import { ReorderItemsDto } from '../dto/reorder-items.dto'; import { UpdateCargoTypeDto } from '../dto/update-cargo-type.dto'; import { CargoType } from '../entities/cargo-type.entity'; import { WagonType } from '../../wagon-types/entities/wagon-type.entity'; +import { WagonTypesRepository } from '../../wagon-types/wagon-types.repository'; import { CARGO_TYPES_REPOSITORY, ICargoTypesRepository, @@ -27,6 +29,7 @@ export class CargoTypesService { private readonly repository: ICargoTypesRepository, @Inject(RATES_REPOSITORY) private readonly ratesRepository: IRatesRepository, + private readonly wagonTypesRepository: WagonTypesRepository, private readonly displayOrder: DisplayOrderService, ) {} @@ -75,6 +78,63 @@ export class CargoTypesService { return map; } + /** + * PER_TON (bulk) cargo may cap how many tons ride one wagon, BELOW that + * wagon's rated capacity: sugar at 50T on a 70T wagon means 200T takes 4 + * wagons, not 3. Unlike the PER_ITEM fit this is optional — an absent key + * means the full rated capacity, so existing cargo types are unaffected. + * + * A cap ABOVE the rated capacity is rejected: nobody loads 90T on a 70T + * wagon, so it is a typo, and silently clamping it would leave the config + * screen showing a number the trains never honour. (Allocation clamps too, via + * `bulkTonsPerWagon`, for caps left stale by a later wagon-type edit — this + * check cannot see those, since the cargo type is never re-saved.) + * + * Returns the map trimmed to the allowed wagon types, or null when the cargo + * is not PER_TON / nothing is capped. + */ + private async resolveTonsPerWagonMap(input: { + unitOfMeasure?: CargoUnitOfMeasure | null; + wagonTypeIds: string[]; + tonsPerWagonMap?: Record | null; + }): Promise | null> { + if (input.unitOfMeasure !== CargoUnitOfMeasure.PerTon || !input.wagonTypeIds.length) { + return null; + } + const capped = input.wagonTypeIds.filter( + (id) => input.tonsPerWagonMap?.[id] !== undefined && input.tonsPerWagonMap[id] !== null, + ); + if (!capped.length) return null; + + const wagonTypes = await this.wagonTypesRepository.findAll({ + where: { id: In(capped) }, + }); + const capacityById = new Map( + wagonTypes.map((wt) => [wt.id, Number(wt.capacityTons) || 0]), + ); + + const map: Record = {}; + for (const wagonTypeId of capped) { + const tons = Number(input.tonsPerWagonMap?.[wagonTypeId]); + if (!Number.isFinite(tons) || tons <= 0) { + throw new BadRequestException( + `tonsPerWagonMap for wagon type ${wagonTypeId} must be a number greater than 0`, + ); + } + const capacity = capacityById.get(wagonTypeId); + if (capacity === undefined) { + throw new BadRequestException(`Wagon type ${wagonTypeId} not found`); + } + if (capacity > 0 && tons > capacity) { + throw new BadRequestException( + `Max tons per wagon (${tons}T) exceeds wagon type ${wagonTypeId} rated capacity ${capacity}T`, + ); + } + map[wagonTypeId] = tons; + } + return map; + } + /** Create a new cargo type. */ async create(dto: CreateCargoTypeDto): Promise { const code = generateCode(dto.cargoTypeName); @@ -90,6 +150,12 @@ export class CargoTypesService { insertAfterId: dto.insertAfterId, }); + const tonsPerWagonMap = await this.resolveTonsPerWagonMap({ + unitOfMeasure: dto.unitOfMeasure ?? null, + wagonTypeIds: dto.wagonTypeIds ?? [], + tonsPerWagonMap: dto.tonsPerWagonMap, + }); + return this.repository.create({ code, cargoTypeName: dto.cargoTypeName, @@ -104,6 +170,7 @@ export class CargoTypesService { wagonTypeIds: dto.wagonTypeIds ?? [], itemsPerWagonMap: dto.itemsPerWagonMap, }), + tonsPerWagonMap, displayOrder, }); } @@ -116,12 +183,32 @@ export class CargoTypesService { const parent = await this.repository.findById(dto.parentGroupId); if (!parent) throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`); } - const { wagonTypeIds, itemsPerWagonMap, insertAfterId: _insertAfterId, ...columns } = dto; + const { + wagonTypeIds, + itemsPerWagonMap, + tonsPerWagonMap, + insertAfterId: _insertAfterId, + ...columns + } = dto; // Re-validate the fit map whenever anything it depends on moves — a partial // update merges with the stored values so e.g. adding a wagon type without // its fit still 400s. Untouched fields leave the stored map alone. const touchesItemsFit = wagonTypeIds !== undefined || itemsPerWagonMap !== undefined || dto.unitOfMeasure !== undefined; + // Same merge rule for the tonnage cap: re-resolve whenever the uom, the + // allowed wagon types, or the caps themselves move, so a wagon type added + // without a cap keeps its full rated capacity and a uom flip drops stale caps. + const touchesTonsCap = + wagonTypeIds !== undefined || tonsPerWagonMap !== undefined || dto.unitOfMeasure !== undefined; + const resolvedTonsPerWagonMap = touchesTonsCap + ? await this.resolveTonsPerWagonMap({ + unitOfMeasure: + dto.unitOfMeasure !== undefined ? dto.unitOfMeasure : existing.unitOfMeasure, + wagonTypeIds: wagonTypeIds ?? (existing.wagonTypes ?? []).map((wt) => wt.id), + tonsPerWagonMap: + tonsPerWagonMap !== undefined ? tonsPerWagonMap : existing.tonsPerWagonMap, + }) + : undefined; const updated = await this.repository.update(id, { ...columns, ...(wagonTypeIds @@ -138,6 +225,7 @@ export class CargoTypesService { }), } : {}), + ...(touchesTonsCap ? { tonsPerWagonMap: resolvedTonsPerWagonMap } : {}), }); if (!updated) throw new NotFoundException(`Cargo type ${id} not found`); // A uom flip renames how existing rates bill (PER_TON ↔ PER_ITEM name the diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/rate-change-requests.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/rate-change-requests.service.ts index 36c55dad3..6dde40d7d 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/rate-change-requests.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/rate-change-requests.service.ts @@ -37,6 +37,10 @@ const DIFFABLE_FIELDS = [ // diffed to nothing and the submit was refused as "nothing changed". 'originYardId', 'destinationYardId', + // Container last-mile distance bands. Missing here, a band-range edit on a + // LIVE last-mile rate would diff to "nothing changed". + 'minKm', + 'maxKm', ] as const; /** diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/rates.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/rates.service.ts index d8ceb97ab..5645334e5 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/rates.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/rates.service.ts @@ -7,6 +7,7 @@ import { NotFoundException, } from '@nestjs/common'; import { PaginatedResponse, YardCountry } from '@edr/types'; +import { Not } from 'typeorm'; import { CreateRateDto } from '../dto/create-rate.dto'; import { ListRatesQueryDto } from '../dto/list-rule-engine-query.dto'; import { UpdateRateDto } from '../dto/update-rate.dto'; @@ -340,6 +341,96 @@ export class RatesService { return appliesTo === 'INTERCITY' ? intercityKind === 'BULK' : appliesTo === 'BULK'; } + /** + * Validate and normalise the last-mile band fields for a rate shape. + * + * Last-mile rates come in two calculation modes: bulk (PER_TON_KM — price = + * tons × km × rate, one row, no scope) and container (PER_KM — one row per + * container type per distance band, price = km × rate × quantity). Every + * other rate shape has its band fields cleared, mirroring how yard scope is + * cleared for non-route rates. + */ + private resolveLastMileBand(input: { + appliesTo: Rate['appliesTo']; + rateUnit: Rate['rateUnit']; + containerTypeId: string | null; + minKm?: number | null; + maxKm?: number | null; + }): { minKm: number | null; maxKm: number | null } { + const { appliesTo, rateUnit, containerTypeId } = input; + if (appliesTo !== 'LAST_MILE') return { minKm: null, maxKm: null }; + + if (rateUnit === 'PER_TON_KM') { + if (containerTypeId) { + throw new BadRequestException( + 'A bulk last-mile rate (per ton per km) cannot be scoped to a container type.', + ); + } + return { minKm: null, maxKm: null }; + } + + if (rateUnit === 'PER_KM') { + if (!containerTypeId) { + throw new BadRequestException( + 'A container last-mile rate must name the container type it covers (20ft and 40ft price differently).', + ); + } + const minKm = input.minKm ?? null; + const maxKm = input.maxKm ?? null; + if (minKm === null) { + throw new BadRequestException( + 'A container last-mile rate needs a distance band — set "From km" (0 for the first band).', + ); + } + if (maxKm !== null && maxKm <= minKm) { + throw new BadRequestException('"To km" must be greater than "From km".'); + } + return { minKm, maxKm }; + } + + // Legacy last-mile shapes (FLAT / PER_CONTAINER / PER_TON) carry no band. + return { minKm: null, maxKm: null }; + } + + /** + * Reject a container last-mile band that overlaps an existing band for the + * same container type. Bands are half-open [minKm, maxKm) with NULL maxKm = + * open-ended, so 0–30 and 30–∞ tile cleanly. Checked across every + * non-superseded row (DRAFT included) — two drafts with colliding bands would + * only defer the conflict to approval. + */ + private async assertNoBandOverlap(input: { + containerTypeId: string; + minKm: number; + maxKm: number | null; + ignoreId?: string; + }): Promise { + const siblings = await this.repository.findAll({ + where: { + rateType: 'LAST_MILE', + rateUnit: 'PER_KM', + containerTypeId: input.containerTypeId, + status: Not('SUPERSEDED'), + }, + }); + const newMax = input.maxKm ?? Number.POSITIVE_INFINITY; + for (const sibling of siblings) { + if (sibling.id === input.ignoreId) continue; + if (sibling.minKm === null || sibling.minKm === undefined) continue; // legacy row, no band + const sibMin = Number(sibling.minKm); + const sibMax = + sibling.maxKm === null || sibling.maxKm === undefined + ? Number.POSITIVE_INFINITY + : Number(sibling.maxKm); + if (input.minKm < sibMax && sibMin < newMax) { + const sibLabel = `${sibMin}–${sibMax === Number.POSITIVE_INFINITY ? 'open' : sibMax} km`; + throw new ConflictException( + `This distance band overlaps the existing ${sibLabel} band for this container type. Adjust the ranges so each distance falls in exactly one band.`, + ); + } + } + } + /** * Reject a second rate with the same identity pattern (rateType + scope). With * effective-date windows gone, two LIVE/DRAFT rates for the same pattern would @@ -360,6 +451,8 @@ export class RatesService { tradeDirection: string | null; originYardId: string | null; destinationYardId: string | null; + /** Band start — part of the identity for container last-mile bands only. */ + minKm?: number | null; ignoreId?: string; }): Promise { const existing = await this.repository.findByPattern(pattern); @@ -438,6 +531,17 @@ export class RatesService { cargoTypeId, ); + const { minKm, maxKm } = this.resolveLastMileBand({ + appliesTo, + rateUnit, + containerTypeId, + minKm: dto.minKm, + maxKm: dto.maxKm, + }); + if (appliesTo === 'LAST_MILE' && rateUnit === 'PER_KM' && containerTypeId && minKm !== null) { + await this.assertNoBandOverlap({ containerTypeId, minKm, maxKm }); + } + await this.assertNoDuplicatePattern({ rateType, ...(this.resolvesSingleRate(appliesTo, trigger) ? {} : { rateUnit }), @@ -446,6 +550,7 @@ export class RatesService { tradeDirection, originYardId, destinationYardId, + minKm, }); return this.repository.create({ @@ -457,9 +562,13 @@ export class RatesService { tradeDirection, originYardId, destinationYardId, - currency: dto.currency ?? 'USD', + // Last-mile is the one shape sold in birr (or USD); everything else is + // USD by contract. + currency: appliesTo === 'LAST_MILE' ? (dto.currency ?? 'ETB') : 'USD', rateValue: dto.rateValue, rateUnit, + minKm, + maxKm, status: 'DRAFT', proposedByStaffId, }); @@ -622,6 +731,29 @@ export class RatesService { ); updates.rateUnit = rateUnit; + const { minKm, maxKm } = this.resolveLastMileBand({ + appliesTo, + rateUnit, + containerTypeId: updates.containerTypeId, + minKm: dto.minKm !== undefined ? dto.minKm : existing.minKm, + maxKm: dto.maxKm !== undefined ? dto.maxKm : existing.maxKm, + }); + updates.minKm = minKm; + updates.maxKm = maxKm; + if ( + appliesTo === 'LAST_MILE' && + rateUnit === 'PER_KM' && + updates.containerTypeId && + minKm !== null + ) { + await this.assertNoBandOverlap({ + containerTypeId: updates.containerTypeId, + minKm, + maxKm, + ignoreId: id, + }); + } + // Guard the pattern uniqueness for the new identity, ignoring this row. await this.assertNoDuplicatePattern({ rateType, @@ -631,10 +763,14 @@ export class RatesService { tradeDirection: updates.tradeDirection, originYardId: updates.originYardId, destinationYardId: updates.destinationYardId, + minKm, ignoreId: id, }); - updates.currency = dto.currency ?? existing.currency ?? 'USD'; + updates.currency = + appliesTo === 'LAST_MILE' + ? (dto.currency ?? existing.currency ?? 'ETB') + : 'USD'; if (dto.rateValue !== undefined) updates.rateValue = dto.rateValue; return updates; } diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/yard-facilities.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/yard-facilities.service.ts index 2579c8d22..9602125bb 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/yard-facilities.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/yard-facilities.service.ts @@ -27,6 +27,18 @@ export interface YardFacilityInfo { /** Which side of the trip a yard is being considered for. */ export type YardSide = 'ORIGIN' | 'DESTINATION'; +/** + * The four per-side capability flags as stored — NOT gated on the coarse + * `handles*` switches. The yards config page edits the stored values; gating + * is applied only when the flows resolve capability (see `toInfo`). + */ +export interface YardSideFlags { + hasContainerFacilityOrigin: boolean; + hasBulkFacilityOrigin: boolean; + hasContainerFacilityDestination: boolean; + hasBulkFacilityDestination: boolean; +} + /** * Which yards can handle cargo, and what kind. * @@ -116,6 +128,61 @@ export class YardFacilitiesService { return rows.map((r: Parameters[0]) => this.toInfo(r)); } + /** Stored per-side flags for a set of yards, keyed by yard id. Yards with no facility record are absent. */ + async sideFlagsForYards(yardIds: string[]): Promise> { + if (yardIds.length === 0) return new Map(); + const rows: Array = await this.dataSource.query( + `SELECT yard_id AS "yardId", + has_container_facility_origin AS "hasContainerFacilityOrigin", + has_bulk_facility_origin AS "hasBulkFacilityOrigin", + has_container_facility_destination AS "hasContainerFacilityDestination", + has_bulk_facility_destination AS "hasBulkFacilityDestination" + FROM freight.yard_facilities + WHERE deleted_at IS NULL AND yard_id = ANY($1)`, + [yardIds], + ); + return new Map( + rows.map((r) => [ + r.yardId, + { + hasContainerFacilityOrigin: r.hasContainerFacilityOrigin, + hasBulkFacilityOrigin: r.hasBulkFacilityOrigin, + hasContainerFacilityDestination: r.hasContainerFacilityDestination, + hasBulkFacilityDestination: r.hasBulkFacilityDestination, + }, + ]), + ); + } + + /** + * Write per-side flags from the yards config form, creating the facility + * record if the yard doesn't have one yet (backoffice-created yards don't). + * Flags left undefined keep their stored value; on first insert they default + * false — an unconfigured facility offers nothing. + */ + async upsertSideFlags(yardId: string, flags: Partial): Promise { + await this.dataSource.query( + `INSERT INTO freight.yard_facilities + (yard_id, has_container_facility_origin, has_bulk_facility_origin, + has_container_facility_destination, has_bulk_facility_destination) + VALUES ($1, COALESCE($2, false), COALESCE($3, false), COALESCE($4, false), COALESCE($5, false)) + ON CONFLICT (yard_id) WHERE deleted_at IS NULL + DO UPDATE SET + has_container_facility_origin = COALESCE($2, yard_facilities.has_container_facility_origin), + has_bulk_facility_origin = COALESCE($3, yard_facilities.has_bulk_facility_origin), + has_container_facility_destination = COALESCE($4, yard_facilities.has_container_facility_destination), + has_bulk_facility_destination = COALESCE($5, yard_facilities.has_bulk_facility_destination), + updated_at = now()`, + [ + yardId, + flags.hasContainerFacilityOrigin ?? null, + flags.hasBulkFacilityOrigin ?? null, + flags.hasContainerFacilityDestination ?? null, + flags.hasBulkFacilityDestination ?? null, + ], + ); + } + /** * Can this facility lift this cargo? Keeps the freight-type rule in one place * so callers can't get it subtly wrong. diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/yards.duplicate-label.spec.ts b/apps/edr-freight-api/src/modules/rule-engine/services/yards.duplicate-label.spec.ts index 8affab199..0ec9fe282 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/yards.duplicate-label.spec.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/yards.duplicate-label.spec.ts @@ -16,6 +16,10 @@ const service = (): YardsService => update: async (_id: string, d: Partial) => d as Yard, } as never, { resolveCreateOrder: async () => 1 } as never, + { + sideFlagsForYards: async () => new Map(), + upsertSideFlags: async () => undefined, + } as never, ); describe('duplicate yard labels are rejected', () => { diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/yards.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/yards.service.ts index 7dd29ded1..995a901ab 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/yards.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/yards.service.ts @@ -8,6 +8,24 @@ import { UpdateYardDto } from '../dto/update-yard.dto'; import { Yard } from '../entities/yard.entity'; import { IYardsRepository, YARDS_REPOSITORY } from '../interfaces/yards.repository.interface'; import { DisplayOrderService } from './display-order.service'; +import { YardFacilitiesService, YardSideFlags } from './yard-facilities.service'; + +/** Yard rows the config page lists/edits carry the stored per-side facility flags. */ +export type YardWithSideFlags = Yard & YardSideFlags; + +const SIDE_FLAG_KEYS = [ + 'hasContainerFacilityOrigin', + 'hasBulkFacilityOrigin', + 'hasContainerFacilityDestination', + 'hasBulkFacilityDestination', +] as const; + +const NO_FLAGS: YardSideFlags = { + hasContainerFacilityOrigin: false, + hasBulkFacilityOrigin: false, + hasContainerFacilityDestination: false, + hasBulkFacilityDestination: false, +}; @Injectable() export class YardsService { @@ -15,18 +33,34 @@ export class YardsService { @Inject(YARDS_REPOSITORY) private readonly repository: IYardsRepository, private readonly displayOrder: DisplayOrderService, + private readonly facilities: YardFacilitiesService, ) {} /** List yards — standard paginated envelope with server-side search. */ - async findAll(query: ListYardsQueryDto): Promise> { - return this.repository.findPaged(query); + async findAll(query: ListYardsQueryDto): Promise> { + const page = await this.repository.findPaged(query); + const flags = await this.facilities.sideFlagsForYards(page.items.map((y) => y.id)); + return { + ...page, + items: page.items.map((y) => ({ ...y, ...NO_FLAGS, ...flags.get(y.id) })), + }; } /** Get a yard by ID. */ - async findById(id: string): Promise { + async findById(id: string): Promise { const entity = await this.repository.findById(id); if (!entity) throw new NotFoundException(`Yard ${id} not found`); - return entity; + const flags = await this.facilities.sideFlagsForYards([id]); + return { ...entity, ...NO_FLAGS, ...flags.get(id) }; + } + + /** The per-side facility flags present in the dto, or null when none were sent. */ + private pickSideFlags(dto: Partial): Partial | null { + const flags: Partial = {}; + for (const key of SIDE_FLAG_KEYS) { + if (dto[key] !== undefined) flags[key] = dto[key]; + } + return Object.keys(flags).length > 0 ? flags : null; } /** Create a yard. */ @@ -43,7 +77,7 @@ export class YardsService { insertAfterId: dto.insertAfterId, }); - return this.repository.create({ + const yard = await this.repository.create({ code, label: dto.label, country: dto.country, @@ -51,15 +85,28 @@ export class YardsService { hasFacility: dto.hasFacility ?? false, displayOrder, }); + + const flags = this.pickSideFlags(dto); + if (flags) await this.facilities.upsertSideFlags(yard.id, flags); + return this.findById(yard.id); } /** Update a yard. */ async update(id: string, dto: UpdateYardDto): Promise { await this.findById(id); if (dto.label !== undefined) await this.assertLabelAvailable(dto.label, id); - const updated = await this.repository.update(id, dto); - if (!updated) throw new NotFoundException(`Yard ${id} not found`); - return updated; + + // Per-side flags live on yard_facilities, not the yards row — split them out. + const flags = this.pickSideFlags(dto); + const yardDto = { ...dto }; + for (const key of SIDE_FLAG_KEYS) delete yardDto[key]; + + if (Object.keys(yardDto).length > 0) { + const updated = await this.repository.update(id, yardDto); + if (!updated) throw new NotFoundException(`Yard ${id} not found`); + } + if (flags) await this.facilities.upsertSideFlags(id, flags); + return this.findById(id); } /** No two active yards may share a label (case/whitespace-insensitive). */ diff --git a/apps/edr-freight-api/src/modules/train-schedules/entities/train-schedule.entity.ts b/apps/edr-freight-api/src/modules/train-schedules/entities/train-schedule.entity.ts index 0581f6000..5730120e9 100644 --- a/apps/edr-freight-api/src/modules/train-schedules/entities/train-schedule.entity.ts +++ b/apps/edr-freight-api/src/modules/train-schedules/entities/train-schedule.entity.ts @@ -161,6 +161,15 @@ export class TrainSchedule extends BaseEntity { @Column({ name: 'rule_payment_window_minutes', type: 'int', nullable: true }) rulePaymentWindowMinutes?: number | null; + /** + * Staff configured this schedule's booking window by hand (at creation or via + * the per-schedule override) instead of inheriting the live global rules. + * `restampPendingWindows` skips these, so a later global-rules edit cannot + * silently overwrite the hand-picked settings. + */ + @Column({ name: 'window_rule_custom', type: 'boolean', default: false }) + windowRuleCustom!: boolean; + @Column({ name: 'rule_import_window_lead_days', type: 'int', nullable: true }) ruleImportWindowLeadDays?: number | null; diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.constants.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.constants.ts index d43cff60b..45370f546 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.constants.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.constants.ts @@ -7,11 +7,51 @@ * (BookingWindowService) drives all timing off that config. */ -export const BATCH_TIMEZONE = 'Africa/Addis_Ababa'; +export const BATCH_TIMEZONE = "Africa/Addis_Ababa"; /** How long before the pay deadline the one reminder notification goes out. */ export const PAYMENT_REMINDER_LEAD_MS = 10 * 60_000; +/** Drain tail applied to every pay window when FREIGHT_PAYMENT_DRAIN_MINUTES is unset. */ +export const DEFAULT_PAYMENT_DRAIN_MINUTES = 7; + +/** + * Drain tail on every pay window, in ms. Read per call so the env var can be + * changed without a rebuild (and so tests can set it). + */ +export function paymentDrainMs(): number { + // An empty/blank value is UNSET, not zero — a bare `FREIGHT_PAYMENT_DRAIN_MINUTES=` + // left in a .env must not silently disable the drain (Number('') is 0). + const raw = process.env.FREIGHT_PAYMENT_DRAIN_MINUTES?.trim(); + const minutes = raw ? Number(raw) : NaN; + return ( + (Number.isFinite(minutes) && minutes >= 0 + ? minutes + : DEFAULT_PAYMENT_DRAIN_MINUTES) * 60_000 + ); +} + +/** + * A pay window AND its drain tail have closed. + * + * Settlement is asynchronous (provider confirm → payment-api → outbox relay), so + * a payment made in the last seconds of the window lands after `paymentDeadline`. + * The drain defers the WHOLE expiry pipeline — wagons stay held, the waiting list + * is not promoted, the window cycle does not conclude — so that settlement still + * has a live booking to land on. A payment that arrives even later is not lost + * either: it settles the expired invoice and revives the booking (see + * BillingService.settleByPaymentId + BookingInvoiceService.advanceBookingOnPayment). + * + * No deadline ⇒ never lapsed; callers decide what an unknown deadline means. + */ +export function payWindowLapsed( + deadline: Date | null | undefined, + now: number, + drainMs: number = paymentDrainMs(), +): boolean { + return deadline != null && deadline.getTime() + drainMs <= now; +} + /** Fallback wagons-per-booking when a booking has no computed `wagonsRequired`. */ export const DEFAULT_WAGONS_PER_BOOKING = 1; diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.spec.ts index 1a1a75275..ce0e77a3e 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.spec.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.spec.ts @@ -1,7 +1,14 @@ import { BookingBatchService } from './booking-batch.service'; +import { paymentDrainMs } from './booking-batch.constants'; import { Booking } from '../bookings/entities/booking.entity'; import { WagonStockLedger } from './wagon-stock-ledger.util'; +/** + * A pay window closed long enough ago to be past its drain tail as well — i.e. + * genuinely expirable. Inside the tail nothing expires (see payWindowLapsed). + */ +const fullyLapsedDeadline = () => new Date(Date.now() - 60_000 - paymentDrainMs()); + describe('BookingBatchService — PAID reconcile', () => { const scheduleId = 'schedule-1'; const bookingId = 'booking-1'; @@ -856,7 +863,7 @@ describe('BookingBatchService — PAID reconcile', () => { // One reservation whose pay window lapsed, and one booking on the waiting list. const lapsed = booking('lapsed', 50, { status: 'SELECTED_FOR_BATCH', - paymentDeadline: new Date(Date.now() - 60_000), + paymentDeadline: fullyLapsedDeadline(), }); const waiting = booking('waiting', 10, { trainScheduleId: null }); @@ -888,10 +895,41 @@ describe('BookingBatchService — PAID reconcile', () => { expect((notifier.payNow.mock.calls[0][0] as Booking).id).toBe('waiting'); }); + it('holds a reservation whose deadline passed but whose drain tail has not', async () => { + // Settlement is asynchronous, so a payment made in the window's last + // seconds lands after the deadline. Expiring here would free the wagons + // out from under it — the swallowed-payment finding. + const draining = booking('draining', 50, { + status: 'SELECTED_FOR_BATCH', + paymentDeadline: new Date(Date.now() - 60_000), + }); + const waiting = booking('waiting', 10, { trainScheduleId: null }); + + bookingsRepository.findReservedForSchedule + .mockResolvedValueOnce([draining]) + .mockResolvedValue([]); + bookingsRepository.findBatchPoolByCorridorDay + .mockResolvedValueOnce([waiting]) + .mockResolvedValue([]); + const byId: Record = { draining, waiting }; + dataSource + .getRepository() + .findOne.mockImplementation( + async (opts: { where?: { id?: string } }) => + byId[opts?.where?.id ?? ''] ?? null, + ); + + await service.settleDueReservations(trainId); + + expect(notifier.expired).not.toHaveBeenCalled(); + // ...and its wagons were NOT handed to the waiting list either. + expect(notifier.payNow).not.toHaveBeenCalled(); + }); + it('serialises concurrent settles so the same reservation is not settled twice', async () => { const lapsed = booking('lapsed', 50, { status: 'SELECTED_FOR_BATCH', - paymentDeadline: new Date(Date.now() - 60_000), + paymentDeadline: fullyLapsedDeadline(), }); // Both callers read the reservation; the lock must stop the second from // acting on rows the first already expired. (The PAYMENT transition and the @@ -922,7 +960,7 @@ describe('BookingBatchService — PAID reconcile', () => { it('never expires a reservation whose payment landed — allocates it instead', async () => { const latePaid = booking('late-paid', 50, { status: 'SELECTED_FOR_BATCH', - paymentDeadline: new Date(Date.now() - 60_000), + paymentDeadline: fullyLapsedDeadline(), }); bookingsRepository.findReservedForSchedule .mockResolvedValueOnce([latePaid]) @@ -1036,7 +1074,7 @@ describe('BookingBatchService — PAID reconcile', () => { ...(waiting as unknown as Record), status: 'SELECTED_FOR_BATCH', trainScheduleId: exportScheduleId, - paymentDeadline: new Date(Date.now() - 60_000), + paymentDeadline: fullyLapsedDeadline(), originYardId: 'yard-a', destinationYardId: 'yard-b', priorityScore: 0, diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts index efdf4f691..5185dcd99 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts @@ -64,6 +64,8 @@ import { DEFAULT_CONTAINER_WAGON_TARE_TONS, DEFAULT_WAGONS_PER_BOOKING, PAYMENT_REMINDER_LEAD_MS, + payWindowLapsed, + paymentDrainMs, } from "./booking-batch.constants"; import { LocomotiveLimits, @@ -71,6 +73,7 @@ import { bookingCargoTons, bulkItemsFitFor, bulkItemWagonsRequired, + bulkTonsPerWagon, bookingGrossWeightTons, deriveTrainCapacityFromLocomotive, sizePartialOfferWagons, @@ -150,6 +153,14 @@ export interface ExportTrainOption { }>; } +/** A train a paid-unallocated booking can board (route + capacity verified). */ +export interface AllocationCandidate { + id: string; + reference: string | null; + direction: string | null; + scheduledDepartureDate: Date; +} + /** A day-level pool key: all trains on this route departing on this EAT day. */ interface RouteDayGroup { originYardId: string; @@ -411,7 +422,9 @@ export class BookingBatchService implements OnModuleInit { .getRepository(Booking) .createQueryBuilder("b") .select("DISTINCT b.train_schedule_id", "scheduleId") - .where(`b.status IN ('SELECTED_FOR_BATCH', 'AWAITING_PAYMENT')`) + .where( + `b.status IN ('SELECTED_FOR_BATCH', 'AWAITING_PAYMENT', 'PAYMENT_VERIFICATION_IN_PROGRESS')`, + ) .andWhere("b.train_schedule_id IS NOT NULL") .getRawMany<{ scheduleId: string }>(); for (const { scheduleId } of reserved) this.armSettle(scheduleId); @@ -692,6 +705,15 @@ export class BookingBatchService implements OnModuleInit { await this.ensurePaidBookingAllocated(bookingId); } + /** + * A late settlement paid a partial offer whose window had lapsed — bring the + * offer back so `ensurePaidBookingAllocated`'s applySplit still reduces the + * booking to what was actually bought. No-op without the split feature. + */ + async reviveOfferForInvoice(invoiceId: string): Promise { + await this.splitService?.reviveOfferForInvoice(invoiceId); + } + /** * Day-level backstop for stranded PAID bookings: reconcilePaidUnlinked is * keyed on train_schedule_id, so a booking whose hold was expired (schedule @@ -2098,7 +2120,9 @@ export class BookingBatchService implements OnModuleInit { if (linked) return "ALLOCATED"; if ( booking.status === "SELECTED_FOR_BATCH" || - booking.status === "AWAITING_PAYMENT" + booking.status === "AWAITING_PAYMENT" || + // Redirect-acked, webhook pending — still a reserved (unpaid) hold. + booking.status === "PAYMENT_VERIFICATION_IN_PROGRESS" ) { return "SELECTED_FOR_BATCH"; } @@ -2736,11 +2760,13 @@ export class BookingBatchService implements OnModuleInit { const isPaid = (b: Booking) => b.paymentStatus === "PAID" || b.status === "PAID"; - // Deadline is the line — no fixed slack. A payment that beat the deadline - // but whose webhook is late is caught by expire()'s gateway reconcile. + // The deadline carries a drain tail (payWindowLapsed): settlement is async, + // so a payment made in the window's last seconds lands after it. Nothing is + // expired until the tail passes. expire()'s gateway reconcile is the second + // line of defence, not the first. const isExpired = (b: Booking) => b.paymentDeadline - ? b.paymentDeadline.getTime() <= now + ? payWindowLapsed(b.paymentDeadline, now) : expireUnpaidUnknownDeadline; for (const booking of reserved) { @@ -3020,6 +3046,7 @@ export class BookingBatchService implements OnModuleInit { : booking.status; await manager.getRepository(Booking).update(bookingId, { trainScheduleId: newScheduleId, + scheduledDate: schedule.scheduledDepartureDate, status: restoredStatus, // A paid booking still hunting for a wagon keeps its flag through the // move — it only clears when wagons are actually assigned. @@ -3038,6 +3065,104 @@ export class BookingBatchService implements OnModuleInit { this.notifyBoardChanged(newScheduleId, "booking_moved"); } + /** + * Trains a paid-but-unallocated booking can board right now: OPEN window, + * future departure, route covers the booking's leg, and remaining corridor + * capacity fits it. Split by the booking's own scheduled day so the UI can + * offer one-click same-day allocation vs an explicit "another date" choice. + */ + async allocationCandidates(bookingId: string): Promise<{ + sameDay: AllocationCandidate[]; + otherDays: AllocationCandidate[]; + }> { + const booking = await this.dataSource.getRepository(Booking).findOne({ + where: { id: bookingId }, + relations: { + bookingContainers: { containerType: true }, + // wagonTypes drives the break-bulk items-per-wagon fit — size the + // booking exactly as the intercity accept check does. + cargoType: { wagonTypes: true }, + }, + }); + if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`); + const schedules = await this.trainSchedulesRepository.findAll({ + where: [ + { status: TrainScheduleStatusEnum.Draft }, + { status: TrainScheduleStatusEnum.Scheduled }, + ], + }); + const today = eatDay(new Date()); + const bookingDay = booking.scheduledDate ? eatDay(booking.scheduledDate) : null; + const sameDay: AllocationCandidate[] = []; + const otherDays: AllocationCandidate[] = []; + for (const s of schedules) { + if (!s.scheduledDepartureDate || eatDay(s.scheduledDepartureDate) < today) continue; + if (s.bookingWindowStatus !== "OPEN") continue; + if (s.id === booking.trainScheduleId) continue; + const stops = await this.stopsForSchedule(s); + const fromIdx = stops.indexOf(booking.originYardId); + const toIdx = stops.indexOf(booking.destinationYardId); + if (fromIdx < 0 || toIdx < 0 || fromIdx >= toIdx) continue; + // ponytail: full capacity build per candidate is heavy; the set is small + // (future OPEN trains on the booking's route) — precompute if it grows. + const cap = await this.intercityCapacity(s.id); + if (!cap) continue; + const leg = cap.budget.legForYards(booking.originYardId, booking.destinationYardId); + if (!cap.budget.fits(cap.needFor(booking), leg)) continue; + const candidate: AllocationCandidate = { + id: s.id, + reference: s.reference ?? s.trainNumber ?? null, + direction: s.direction ?? null, + scheduledDepartureDate: s.scheduledDepartureDate, + }; + (eatDay(s.scheduledDepartureDate) === bookingDay ? sameDay : otherDays).push(candidate); + } + const byDate = (a: AllocationCandidate, b: AllocationCandidate) => + new Date(a.scheduledDepartureDate).getTime() - new Date(b.scheduledDepartureDate).getTime(); + sameDay.sort(byDate); + otherDays.sort(byDate); + return { sameDay, otherDays }; + } + + /** + * Place a PAID booking that lost (or never got) its train: re-point via + * moveToSchedule (window/route validation + day sync), then allocate it + * immediately — payment already landed, so no new pay window opens. The + * customer gets an in-app notice when the new train departs on a different + * day than their original choice. + */ + async allocatePaid(bookingId: string, scheduleId: string): Promise { + const before = await this.dataSource + .getRepository(Booking) + .findOne({ where: { id: bookingId } }); + if (!before) throw new NotFoundException(`Booking ${bookingId} not found`); + if (before.paymentStatus !== "PAID" && before.status !== "PAID") { + throw new BadRequestException( + "Booking is not paid — use the regular scheduling flow", + ); + } + const previousDay = before.scheduledDate ? eatDay(before.scheduledDate) : null; + await this.moveToSchedule(bookingId, scheduleId); + const fresh = await this.dataSource.getRepository(Booking).findOne({ + where: { id: bookingId }, + relations: { bookingContainers: { containerType: true }, cargoType: true }, + }); + if (!fresh) return; + if (!(await this.holdIfWagonShort(scheduleId, fresh))) { + await this.allocate(scheduleId, fresh, "paid"); + } + const schedule = await this.dataSource + .getRepository(TrainSchedule) + .findOne({ where: { id: scheduleId } }); + if ( + previousDay && + schedule?.scheduledDepartureDate && + eatDay(schedule.scheduledDepartureDate) !== previousDay + ) { + this.notifier.allocatedOtherDay(fresh, schedule.scheduledDepartureDate); + } + } + /** * One reminder per hold, shortly before its pay deadline (the window tick * calls this every pass; `payment_reminder_sent_at` dedups). Skips paid @@ -3524,6 +3649,16 @@ export class BookingBatchService implements OnModuleInit { } return; } + // Paid but detached from any train (staff removed it from an allocation, + // or a sweep caught it unpinned): money was taken, so it must board — it + // stays paid-unallocated for staff to place via the allocate action. + if (paid) { + this.logger.log( + `[BATCH] expire skipped for ${booking.reference} — payment landed ` + + `but no train attached; left paid-unallocated for manual placement`, + ); + return; + } // Reconcile-before-expire (only when a pay window was actually open): // no webhook arrived, so ask the gateway DIRECTLY whether the money // landed. A late capture found there is registered as SUCCEEDED and @@ -3852,12 +3987,26 @@ export class BookingBatchService implements OnModuleInit { // booking can use — don't kill it for nothing. const overlaps = victimLeg.fromEdge < leg.toEdge && leg.fromEdge < victimLeg.toEdge; if (!overlaps) continue; + const victimPaid = + victim.paymentStatus === "PAID" || victim.status === "PAID"; await this.dataSource.transaction(async (manager) => { await this.trainScheduleBookingsRepository.deleteByScheduleAndBooking( scheduleId, victim.id, manager, ); + if (victimPaid) { + // Paid bookings are never expired — money was taken, so it boards. + // Detach it so it surfaces in the paid-unallocated queue for staff + // to re-place; the settled invoice stays untouched. + await manager.getRepository(Booking).update(victim.id, { + trainScheduleId: null, + schedulingStatus: "ELIGIBLE", + paymentDeadline: null, + selectedForBatchAt: null, + } as never); + return; + } await manager.getRepository(Booking).update(victim.id, { status: "EXPIRED", schedulingStatus: "ELIGIBLE", @@ -4099,8 +4248,15 @@ export class BookingBatchService implements OnModuleInit { const capacityTons = this.dimsFor(booking, wagonDims).capacityTons; const cargoTons = bookingCargoTons(booking); + // PER_TON cargo may cap tons per wagon below the rating (sugar 50T on a 70T + // wagon), so divide by the cap where one is configured for this type. + const tonsPerWagon = bulkTonsPerWagon( + booking.cargoType, + booking.cargoType?.wagonTypes?.[0]?.id, + capacityTons, + ); const byWeight = - cargoTons > 0 && capacityTons > 0 ? Math.ceil(cargoTons / capacityTons) : 0; + cargoTons > 0 && tonsPerWagon > 0 ? Math.ceil(cargoTons / tonsPerWagon) : 0; // Break-bulk (PER_ITEM): indivisible items can need more wagons than raw // tonnage suggests (floor items-per-wagon loses the fractional capacity). @@ -4168,6 +4324,13 @@ export class BookingBatchService implements OnModuleInit { .filter((o) => o.wagonTypeId && (stockByTypeId.get(o.wagonTypeId) ?? 0) > 0) .map((o) => { const wagonTypeId = o.wagonTypeId as string; + // Each type sized on its OWN per-wagon tonnage cap, not just its rating + // — a type capped lower swallows less per wagon. + const tonsPerWagon = bulkTonsPerWagon( + booking.cargoType, + wagonTypeId, + o.dims.capacityTons, + ); const wagonsIfAlone = Math.max( 1, bulkItemWagonsRequired( @@ -4175,8 +4338,8 @@ export class BookingBatchService implements OnModuleInit { o.dims.capacityTons, bulkItemsFitFor(booking.cargoType, wagonTypeId), ) || - (o.dims.capacityTons > 0 - ? Math.ceil(bookingCargoTons(booking) / o.dims.capacityTons) + (tonsPerWagon > 0 + ? Math.ceil(bookingCargoTons(booking) / tonsPerWagon) : total), ); return { @@ -4602,11 +4765,12 @@ export class BookingBatchService implements OnModuleInit { const allocated = (schedule.scheduleBookings ?? []) .map((sb) => sb.booking) .filter((b): b is Booking => Boolean(b)); - // Lazy-expiry guard: a hold whose deadline lapsed no longer blocks - // capacity, even before the 10s sweep flips it to EXPIRED — availability - // shown to the next customer is honest between ticks. A late capture the - // gateway reconcile later confirms lands as PAID and, if the wagons went - // meanwhile, degrades to WAITING_FOR_WAGON for manual placement. + // Lazy-expiry guard: a hold whose deadline AND drain tail lapsed no longer + // blocks capacity, even before the 10s sweep flips it to EXPIRED — + // availability shown to the next customer is honest between ticks. The drain + // has to be honoured here too: releasing the wagons at the raw deadline + // would resell them to someone else while the paying customer's settlement + // is still in flight, stranding it into WAITING_FOR_WAGON. const deadlineCutoff = Date.now(); const reserved = ( await this.bookingsRepository.findReservedForSchedule(schedule.id) @@ -4614,8 +4778,7 @@ export class BookingBatchService implements OnModuleInit { (b) => b.paymentStatus === "PAID" || b.status === "PAID" || - b.paymentDeadline == null || - b.paymentDeadline.getTime() > deadlineCutoff, + !payWindowLapsed(b.paymentDeadline, deadlineCutoff), ); for (const b of [...allocated, ...reserved]) { budget.subtract( @@ -4690,7 +4853,9 @@ export class BookingBatchService implements OnModuleInit { } /** - * A reservation on this schedule still has time left to pay. + * A reservation on this schedule still has time left to pay — including its + * drain tail, so the cycle cannot conclude out from under a settlement that is + * still in flight. * * The PAYMENT phase ends a hair BEFORE its own reservations do: `paymentPhaseEndsAt` * is stamped when the phase starts, then `reserve()` gives each booking @@ -4711,7 +4876,7 @@ export class BookingBatchService implements OnModuleInit { b.paymentStatus !== "PAID" && b.status !== "PAID" && b.paymentDeadline != null && - b.paymentDeadline.getTime() > now, + !payWindowLapsed(b.paymentDeadline, now), ); } @@ -4920,7 +5085,10 @@ export class BookingBatchService implements OnModuleInit { */ private armSettle(scheduleId: string): void { void this.scheduleById(scheduleId) + // + drain tail: firing at the raw deadline is a guaranteed no-op pass now + // that nothing expires until the tail passes. .then((schedule) => this.paymentWindowMsFor(schedule)) + .then((windowMs: number) => windowMs + paymentDrainMs()) .then((delayMs: number) => { this.removeTimeout(scheduleId); const handle = setTimeout(() => { diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.spec.ts new file mode 100644 index 000000000..0a8cc99fe --- /dev/null +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.spec.ts @@ -0,0 +1,145 @@ +import { BookingJourneyService } from './booking-journey.service'; + +/** + * autoPlaceOnFreedWagons: intercity cargo boards the wagons freed by earlier + * unloads. Exercised directly with a stubbed EntityManager — the surrounding + * loadBooking flow is integration-tested through the running app. + */ +describe('BookingJourneyService.autoPlaceOnFreedWagons', () => { + const service = new BookingJourneyService( + {} as never, // dataSource + {} as never, // yardFacilities + {} as never, // facilityHandling + { emit: jest.fn() } as never, // events + ); + + const schedule = { id: 'sched-1', trainSetId: 'ts-1' }; + const booking = { + id: 'booking-1', + reference: 'BK-1', + cargoTotalWeightVgm: 50, + freightType: 'CONTAINER', + }; + + const makeManager = (slots: unknown[], existingAllocs: unknown[] = []) => { + const savedAllocs: Array> = []; + const savedItems: Array> = []; + const allocQb = { + innerJoinAndSelect: jest.fn().mockReturnThis(), + innerJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(existingAllocs), + }; + const slotQb = { + leftJoinAndSelect: jest.fn().mockReturnThis(), + innerJoin: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + orderBy: jest.fn().mockReturnThis(), + getMany: jest.fn().mockResolvedValue(slots), + }; + let allocId = 0; + const manager = { + getRepository: jest.fn((entity: { name?: string }) => { + const name = entity?.name; + if (name === 'WagonBookingAllocation') { + return { + createQueryBuilder: jest.fn(() => allocQb), + create: jest.fn((v: Record) => v), + save: jest.fn(async (v: Record) => { + const row = { ...v, id: `alloc-${++allocId}` }; + savedAllocs.push(row); + return row; + }), + update: jest.fn(), + }; + } + if (name === 'TrainSetWagon') { + return { createQueryBuilder: jest.fn(() => slotQb) }; + } + if (name === 'BookingContainer') { + return { + find: jest.fn().mockResolvedValue([ + { + id: 'line-1', + containerNumber: 'LINE-001', + containerTypeId: 'ct-20', + units: [{ containerNumber: 'UNIT-001' }, { containerNumber: 'UNIT-002' }], + }, + ]), + }; + } + if (name === 'WagonAllocationContainerItem') { + return { + create: jest.fn((v: Record) => v), + save: jest.fn(async (v: Record) => { + savedItems.push(v); + return v; + }), + }; + } + throw new Error(`Unexpected repository ${name}`); + }), + }; + return { manager, savedAllocs, savedItems }; + }; + + const call = (manager: unknown) => + (service as never as { + autoPlaceOnFreedWagons: (m: unknown, s: unknown, b: unknown) => Promise; + }).autoPlaceOnFreedWagons(manager, schedule, booking); + + it('places the booking on freed slots in consist order, with container items', async () => { + const slots = [ + // Active cargo still riding — NOT freed. + { id: 'slot-1', sequenceNo: 1, capacityTons: 60, allocations: [{ status: 'LOADED' }] }, + // Freed by an earlier unload. + { id: 'slot-2', sequenceNo: 2, capacityTons: 60, allocations: [{ status: 'DEPARTED' }] }, + { id: 'slot-3', sequenceNo: 3, capacityTons: 60, allocations: [] }, + ]; + const { manager, savedAllocs, savedItems } = makeManager(slots); + + await call(manager); + + // 50 t fits on the first freed slot alone. + expect(savedAllocs).toHaveLength(1); + expect(savedAllocs[0]).toMatchObject({ + trainSetWagonId: 'slot-2', + bookingId: 'booking-1', + allocatedWeightTons: 50, + status: 'LOADED', + }); + // One item per physical unit, on the first allocation. + expect(savedItems.map((i) => i.containerNumber)).toEqual(['UNIT-001', 'UNIT-002']); + expect(savedItems.every((i) => i.wagonBookingAllocationId === 'alloc-1')).toBe(true); + }); + + it('spills over onto the next freed slot when one is not enough', async () => { + const slots = [ + { id: 'slot-2', sequenceNo: 2, capacityTons: 30, allocations: [{ status: 'DEPARTED' }] }, + { id: 'slot-3', sequenceNo: 3, capacityTons: 30, allocations: [] }, + ]; + const { manager, savedAllocs } = makeManager(slots); + + await call(manager); + + expect(savedAllocs.map((a) => [a.trainSetWagonId, a.allocatedWeightTons])).toEqual([ + ['slot-2', 30], + ['slot-3', 20], + ]); + }); + + it('does nothing when the booking already has allocations', async () => { + const { manager, savedAllocs } = makeManager([], [{ id: 'existing' }]); + await call(manager); + expect(savedAllocs).toHaveLength(0); + }); + + it('loads without allocation when no wagon is free', async () => { + const slots = [ + { id: 'slot-1', sequenceNo: 1, capacityTons: 60, allocations: [{ status: 'LOADED' }] }, + ]; + const { manager, savedAllocs } = makeManager(slots); + await expect(call(manager)).resolves.toBeUndefined(); + expect(savedAllocs).toHaveLength(0); + }); +}); diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts index 87e756ba6..8668b4f75 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts @@ -13,6 +13,8 @@ import { Freight } from '@edr/types'; import { YardFacilitiesService } from '../rule-engine/services/yard-facilities.service'; import { FacilityHandlingService } from './facility-handling.service'; import { Booking } from '../bookings/entities/booking.entity'; +import { BookingContainer } from '../bookings/entities/booking-container.entity'; +import { WagonAllocationContainerItem } from '../train-schedules/entities/wagon-allocation-container-item.entity'; import { ClearanceMilestoneService } from '../contracts/clearance-milestone.service'; import { Yard } from '../rule-engine/entities/yard.entity'; import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity'; @@ -82,6 +84,11 @@ export class BookingJourneyService { loadedAt: now, loadedByUserId: userId ?? null, } as never); + // Intercity cargo rides the wagons freed by earlier unloads along the + // corridor — place it before the status flip so it boards with a wagon. + if (booking.tradeDirection === 'DOMESTIC') { + await this.autoPlaceOnFreedWagons(manager, schedule, booking); + } await this.setAllocationStatuses(manager, scheduleId, bookingId, 'LOADED'); // Keep the schedule↔booking link's tracking flag in sync — the dispatch // readiness warnings and workspace badges read loading_status, not loadedAt. @@ -327,19 +334,48 @@ export class BookingJourneyService { RETURNING b.id, b.trade_direction`, [schedule.id, schedule.destinationStationId, now], ); + if (rows.length === 0) return []; + + // The facility took the cargo off the train at the final yard — raise its + // GRN, same as the per-booking unloadBooking() path does. Only when that + // yard also has a warehouse (or has no facility at all, e.g. Kality) does + // WarehouseInventoryService additionally get to allocate a warehouse/yard/ + // zone row: a pure facility yard (Dire Dawa, Modjo, Sebeta, Adama) is + // fully represented by the facility event alone — there is nothing there + // for warehouse_inventory's NOT NULL warehouse/yard/zone to point at. + const facility = await this.yardFacilities.facilityForYard(schedule.destinationStationId); + const bookings = await manager + .getRepository(Booking) + .find({ where: { id: In(rows.map((r) => r.id)) }, relations: ['company'] }); + const bookingById = new Map(bookings.map((b) => [b.id, b])); + for (const row of rows) { // Intercity rows just completed — let a ONE_TIME contract close on delivery. if (row.trade_direction === 'DOMESTIC') { this.events.emit('booking.completed', { bookingId: row.id }); } + + const booking = bookingById.get(row.id); + if (booking) { + await this.facilityHandling.recordHandling(manager, { + booking, + yardId: schedule.destinationStationId, + trainScheduleId: schedule.id, + eventType: 'UNLOAD', + occurredAt: now, + }); + } + // Same event the per-booking unloadBooking() path emits — WarehouseInventoryService // listens for this to auto-create the warehouse_inventory row (import/intercity only, // it filters EXPORT itself). The bulk SQL update above skipped this entirely, so // bookings caught by this fallback never left "awaiting unload". - this.events.emit('booking.unloadedAtYard', { - bookingId: row.id, - tradeDirection: row.trade_direction, - }); + if (row.trade_direction !== 'EXPORT' && (!facility?.hasFacility || facility.hasWarehouse)) { + this.events.emit('booking.unloadedAtYard', { + bookingId: row.id, + tradeDirection: row.trade_direction, + }); + } } return rows.map((r) => r.id); } @@ -435,6 +471,100 @@ export class BookingJourneyService { } } + /** + * INTERCITY ONLY. Intercity cargo does not get its own wagons — it rides the + * slots freed by cargo already unloaded along the corridor (e.g. import + * containers uncoupled at Dire Dawa). Staff pinning is a pre-dispatch tool, + * so a DOMESTIC booking loaded mid-corridor is auto-placed here: greedy over + * on-train slots (not DEPARTED) with no active cargo (every allocation + * DEPARTED, or none), in consist order, by capacity. Container numbers are + * copied onto the first allocation so the marshalling document and its + * 40ft/20ft tally stay truthful. When nothing is free the load proceeds + * unallocated — the marshalling document then lists the booking as on board + * without a recorded wagon. + * ponytail: remainder over free capacity is dumped on the last used slot + * (paper overload beats missing cargo); upgrade path is a capacity guard in + * the intercity accept step. + */ + private async autoPlaceOnFreedWagons( + manager: EntityManager, + schedule: TrainSchedule, + booking: Booking, + ): Promise { + const existing = await this.allocationsForBooking(manager, schedule.id, booking.id); + if (existing.length) return; + + const slots = await manager + .getRepository(TrainSetWagon) + .createQueryBuilder('slot') + .leftJoinAndSelect('slot.allocations', 'alloc') + .innerJoin( + TrainSchedule, + 'schedule', + 'schedule.train_set_id = slot.train_set_id AND schedule.id = :scheduleId', + { scheduleId: schedule.id }, + ) + .where(`slot.status != 'DEPARTED'`) + .orderBy('slot.sequence_no', 'ASC') + .getMany(); + const freed = slots.filter((slot) => + (slot.allocations ?? []).every((a) => a.status === 'DEPARTED'), + ); + if (!freed.length) { + this.logger.warn( + `No freed wagon for intercity booking ${booking.reference} on schedule ${schedule.id} — loading without wagon allocation`, + ); + return; + } + + let remaining = Number(booking.cargoTotalWeightVgm) || 0; + const allocRepo = manager.getRepository(WagonBookingAllocation); + const created: WagonBookingAllocation[] = []; + for (const slot of freed) { + const capacity = Number(slot.capacityTons) || remaining || 1; + const take = Math.min(remaining || capacity, capacity); + created.push( + await allocRepo.save( + allocRepo.create({ + trainSetWagonId: slot.id, + bookingId: booking.id, + allocatedWeightTons: take, + loadType: booking.freightType ?? null, + status: 'LOADED', + }), + ), + ); + remaining = Math.max(0, remaining - take); + if (remaining <= 0) break; + } + if (remaining > 0 && created.length) { + await allocRepo.update(created[created.length - 1].id, { + allocatedWeightTons: () => `allocated_weight_tons + ${remaining}`, + } as never); + } + + // Container numbers onto the first allocation, from the booking's container + // lines (per physical unit when recorded, else per line). + const lines = await manager + .getRepository(BookingContainer) + .find({ where: { bookingId: booking.id }, relations: { units: true } }); + const itemRepo = manager.getRepository(WagonAllocationContainerItem); + const first = created[0]; + for (const line of lines) { + const units = line.units?.length ? line.units : [null]; + for (const unit of units) { + await itemRepo.save( + itemRepo.create({ + wagonBookingAllocationId: first.id, + bookingContainerId: line.id, + containerNumber: unit?.containerNumber ?? line.containerNumber ?? null, + containerTypeId: line.containerTypeId ?? null, + }), + ); + } + } + } + private async setAllocationStatuses( manager: EntityManager, scheduleId: string, diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.ts index b806de5ca..efb540066 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-notifier.service.ts @@ -275,6 +275,19 @@ export class BookingNotifierService { this.inApp(b, 'Booking rescheduled', msg); } + /** + * Staff placed a paid booking onto a train departing on a DIFFERENT day than + * the customer's original choice. In-app only — staff drove the change and + * the allocation itself already notifies through the secured path. + */ + allocatedOtherDay(b: Booking, newDeparture: Date): void { + const when = newDeparture.toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE }); + const msg = + `Booking ${b.reference ?? b.id} has been allocated to a train on a different date. ` + + `New departure date: ${when}.`; + this.inApp(b, 'Booking allocated to another date', msg); + } + /** * Booking was removed from its train during a staff reschedule (not a government * pre-empt). It returns to eligible — the customer must rebook or reschedule. @@ -287,6 +300,19 @@ export class BookingNotifierService { this.inApp(b, 'Removed from train', msg); } + /** + * The train carrying this booking was cancelled. The booking is detached and + * returns to the eligible pool — the customer must rebook or pick a new schedule. + */ + scheduleCancelled(b: Booking): void { + const msg = + `The train for booking ${b.reference ?? b.id} has been cancelled. ` + + `Your booking is not lost — please rebook or select a new schedule from the portal.`; + void this.notifyContact(b, msg, 'TRAIN CANCELLED'); + // HIGH: a cancelled train invalidates the customer's plans — must reach SMS/email. + this.inApp(b, 'Train cancelled', msg, { priority: NotificationPriority.HIGH }); + } + /** * The train carrying this booking was moved for maintenance to a new departure * date. The booking stays on the train — only the date moved. diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-split.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-split.service.ts index 9121e4841..ba4f08552 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-split.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-split.service.ts @@ -302,6 +302,19 @@ export class BookingSplitService { .update({ bookingId, status: 'OFFERED' }, { status: 'EXPIRED' }); } + /** + * A late gateway settlement paid the invoice of an offer whose window had + * already lapsed. The customer bought the offered part, so the offer is live + * again and {@link applySplit} must reduce the booking to it — otherwise the + * booking boards WHOLE having paid only the offered portion. Keyed on the paid + * invoice, never on the booking: an offer that simply timed out stays dead. + */ + async reviveOfferForInvoice(invoiceId: string): Promise { + await this.dataSource + .getRepository(BookingBatchOffer) + .update({ invoiceId, status: 'EXPIRED' }, { status: 'OFFERED' }); + } + async findOpenOffer(bookingId: string): Promise { return this.dataSource.getRepository(BookingBatchOffer).findOne({ where: { bookingId, status: 'OFFERED' }, diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-window.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-window.service.ts index 6cdef8c05..1a3baa6b4 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-window.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-window.service.ts @@ -91,7 +91,16 @@ export class BookingWindowService implements OnModuleInit { // 10-second cadence: every transition is derived from persisted timestamps // and applied idempotently, so a finer tick only shrinks the lag between a // deadline passing and the phase actually moving (was a full minute). - @Cron('*/10 * * * * *', { name: 'booking-window-tick', timeZone: BATCH_TIMEZONE }) + // + // Overridable because that lag is the integration suite's pacing floor: every + // window phase, wagon allocation and expiry it waits on lands on this tick, so + // a 10s cadence costs ~5s of pure latency per wait across a few hundred waits. + // The suite runs it at `*/1 * * * * *`. Read at class-definition time, so the + // env var must be set before the module is imported. + @Cron(process.env.BOOKING_WINDOW_TICK_CRON ?? '*/10 * * * * *', { + name: 'booking-window-tick', + timeZone: BATCH_TIMEZONE, + }) async tick(): Promise { if (this.ticking) return; this.ticking = true; @@ -601,7 +610,9 @@ export class BookingWindowService implements OnModuleInit { .getRepository(Booking) .createQueryBuilder('b') .select('DISTINCT b.train_schedule_id', 'scheduleId') - .where(`b.status IN ('SELECTED_FOR_BATCH', 'AWAITING_PAYMENT')`) + .where( + `b.status IN ('SELECTED_FOR_BATCH', 'AWAITING_PAYMENT', 'PAYMENT_VERIFICATION_IN_PROGRESS')`, + ) // Deadline is the line — expire() itself reconciles against the gateway // before actually expiring, so a late in-window payment is still caught. .andWhere('b.payment_deadline <= now()') diff --git a/apps/edr-freight-api/src/modules/train-scheduling/dto/create-container-train-schedule.dto.ts b/apps/edr-freight-api/src/modules/train-scheduling/dto/create-container-train-schedule.dto.ts index 9dfea0781..e8716eb99 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/dto/create-container-train-schedule.dto.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/dto/create-container-train-schedule.dto.ts @@ -9,9 +9,107 @@ import { IsNumber, IsOptional, IsUUID, + Max, Min, + ValidateNested, } from 'class-validator'; +/** + * Per-schedule booking-window rule chosen AT CREATION, instead of inheriting the + * live global rules. Mirrors {@link UpdateScheduleWindowRuleDto}, plus the + * booking-close offset (which the post-creation override deliberately never + * touches). Every field is optional — an omitted field falls back to the global + * value, so staff can override just the one knob they care about. + */ +export class CreateScheduleWindowRuleDto { + @ApiPropertyOptional({ example: 8, description: 'Local EAT hour the booking desk opens each day' }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + @Max(23) + windowOpenHour?: number; + + @ApiPropertyOptional({ + example: 17, + description: + 'Local EAT hour the booking desk shuts each day. Equal to windowOpenHour = 24-hour desk', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + @Max(23) + windowCloseHour?: number; + + @ApiPropertyOptional({ example: 3, description: 'How long each booking cycle stays open, in hours' }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0.0166) + @Max(12) + windowDurationHours?: number; + + @ApiPropertyOptional({ example: 30, description: 'Max staff document-review minutes after the window closes' }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + docReviewMinutes?: number; + + @ApiPropertyOptional({ example: 60, description: 'Customer payment window minutes' }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + paymentWindowMinutes?: number; + + @ApiPropertyOptional({ + example: 3, + description: 'Days before departure the IMPORT/DOMESTIC booking window starts', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + importWindowLeadDays?: number; + + @ApiPropertyOptional({ + example: 24, + description: 'Hours before departure the single FCFS EXPORT window opens', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + exportBookingLeadHours?: number; + + @ApiPropertyOptional({ + example: 180, + nullable: true, + description: + 'Minutes before departure the booking window closes; 0/null = close at departure. ' + + 'Only the offset matching the schedule direction is used (import offset for ' + + 'IMPORT/DOMESTIC, export offset for EXPORT).', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + importCloseOffsetMinutes?: number | null; + + @ApiPropertyOptional({ + example: 1440, + nullable: true, + description: 'Minutes before departure an EXPORT booking window closes; 0/null = at departure', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + exportCloseOffsetMinutes?: number | null; +} + export class CreateContainerTrainScheduleDto { @ApiProperty({ format: 'uuid' }) @IsUUID() @@ -73,4 +171,19 @@ export class CreateContainerTrainScheduleDto { @IsOptional() @IsBoolean() reverseWagonOrder?: boolean; + + @ApiPropertyOptional({ + type: CreateScheduleWindowRuleDto, + description: + 'Configure the booking window for THIS schedule instead of inheriting the live ' + + 'global rules. Omit to use the global rules (the default). The values sent are ' + + 'frozen onto the schedule as its rule snapshot, exactly as a post-creation ' + + 'override would. Rejected for an IMPORT/DOMESTIC train that joins an existing ' + + 'route+day group — those siblings share one window timeline, so edit the group ' + + "window instead of giving one member its own.", + }) + @IsOptional() + @ValidateNested() + @Type(() => CreateScheduleWindowRuleDto) + windowRule?: CreateScheduleWindowRuleDto; } diff --git a/apps/edr-freight-api/src/modules/train-scheduling/fleet-plan.util.ts b/apps/edr-freight-api/src/modules/train-scheduling/fleet-plan.util.ts index bea01af0a..2769c212b 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/fleet-plan.util.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/fleet-plan.util.ts @@ -1,4 +1,4 @@ -import { bookingCargoTons, bulkItemWagonsForAllowedTypes } from './train-capacity.util'; +import { bookingCargoTons, bulkWagonsForAllowedTypes } from './train-capacity.util'; import type { Booking } from '../bookings/entities/booking.entity'; import type { WagonType } from '../wagon-types/entities/wagon-type.entity'; import { @@ -56,8 +56,10 @@ export function wagonsRequiredForBooking(booking: Booking, bulkWagonCapacity?: n // holds the item count there, not tons. No wagon type is fixed yet, so use // the best count across the cargo's allowed types (per-type items-fit // respected); falls back to `capacity` when the relation isn't loaded. - const byItems = bulkItemWagonsForAllowedTypes(booking, booking.cargoType, capacity); - if (byItems > 0) return byItems; + // PER_TON cargo may cap tons per wagon below the rating (sugar 50T on a 70T + // wagon), so tonnage divides by that cap, not by raw capacity. + const byWagons = bulkWagonsForAllowedTypes(booking, booking.cargoType, capacity); + if (byWagons > 0) return byWagons; const weight = Number(booking.cargoTotalWeightVgm ?? 0); return Math.max(1, Math.ceil(weight / capacity)); } diff --git a/apps/edr-freight-api/src/modules/train-scheduling/pay-window-drain.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/pay-window-drain.spec.ts new file mode 100644 index 000000000..6054f28b2 --- /dev/null +++ b/apps/edr-freight-api/src/modules/train-scheduling/pay-window-drain.spec.ts @@ -0,0 +1,67 @@ +import { + DEFAULT_PAYMENT_DRAIN_MINUTES, + paymentDrainMs, + payWindowLapsed, +} from "./booking-batch.constants"; + +/** + * The drain tail is what keeps a pay window's LAST payment from being thrown + * away: settlement is asynchronous (provider confirm → payment-api → outbox + * relay), so a payment made in the window's final seconds lands after + * `paymentDeadline`. Nothing may expire, no wagons may be resold and no window + * cycle may conclude until the tail has passed. + */ +describe("payWindowLapsed — pay-window drain tail", () => { + const deadline = new Date("2026-08-02T22:04:41Z"); + const at = (offsetMs: number) => deadline.getTime() + offsetMs; + const MIN = 60_000; + + afterEach(() => { + delete process.env.FREIGHT_PAYMENT_DRAIN_MINUTES; + }); + + it("is not lapsed before the deadline", () => { + expect(payWindowLapsed(deadline, at(-1 * MIN))).toBe(false); + }); + + it("is not lapsed inside the drain tail", () => { + // The reproduced finding: settled ~7 minutes late. With the default 5-minute + // tail the booking is still live at 4 minutes. + expect(payWindowLapsed(deadline, at(4 * MIN))).toBe(false); + expect(payWindowLapsed(deadline, at(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN - 1))).toBe( + false, + ); + }); + + it("is lapsed once the tail passes", () => { + expect(payWindowLapsed(deadline, at(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN))).toBe( + true, + ); + expect(payWindowLapsed(deadline, at(7 * MIN))).toBe(true); + }); + + it("never lapses without a deadline — callers decide what unknown means", () => { + expect(payWindowLapsed(null, at(60 * MIN))).toBe(false); + expect(payWindowLapsed(undefined, at(60 * MIN))).toBe(false); + }); + + it("honours FREIGHT_PAYMENT_DRAIN_MINUTES", () => { + process.env.FREIGHT_PAYMENT_DRAIN_MINUTES = "20"; + expect(paymentDrainMs()).toBe(20 * MIN); + expect(payWindowLapsed(deadline, at(10 * MIN))).toBe(false); + expect(payWindowLapsed(deadline, at(20 * MIN))).toBe(true); + }); + + it("allows an explicit zero drain (old deadline-is-the-line behaviour)", () => { + process.env.FREIGHT_PAYMENT_DRAIN_MINUTES = "0"; + expect(paymentDrainMs()).toBe(0); + expect(payWindowLapsed(deadline, at(0))).toBe(true); + }); + + it("falls back to the default on garbage or negative values", () => { + for (const bad of ["", "abc", "-3"]) { + process.env.FREIGHT_PAYMENT_DRAIN_MINUTES = bad; + expect(paymentDrainMs()).toBe(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN); + } + }); +}); diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-capacity.util.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-capacity.util.spec.ts index ead3a4e8b..ed476a6e1 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-capacity.util.spec.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-capacity.util.spec.ts @@ -4,6 +4,10 @@ import { bookingTrainLengthMeters, bulkItemWagonsForAllowedTypes, bulkItemWagonsRequired, + bulkTonsPerWagon, + bulkTonWagonsForAllowedTypes, + bulkTonWagonsRequired, + bulkWagonsForAllowedTypes, consistUsage, consistViolations, deriveTrainCapacityFromLocomotive, @@ -135,6 +139,61 @@ describe('train-capacity.util', () => { }); }); + describe('bulkTonsPerWagon / bulkTonWagonsRequired (PER_TON loading cap)', () => { + // Sugar is loaded 50T per wagon even on a 70T wagon. + const sugar = { wagonTypes: [{ id: 'nw5', capacityTons: 70 }], tonsPerWagonMap: { nw5: 50 } }; + const bulk = (tons: number) => ({ freightType: 'BULK', cargoTotalWeightVgm: tons }); + + it('uses the configured cap instead of the rated capacity', () => { + expect(bulkTonsPerWagon(sugar, 'nw5', 70)).toBe(50); + }); + + it('falls back to rated capacity when the cargo type caps nothing', () => { + expect(bulkTonsPerWagon(null, 'nw5', 70)).toBe(70); + expect(bulkTonsPerWagon({ wagonTypes: [] }, 'nw5', 70)).toBe(70); + expect(bulkTonsPerWagon({ tonsPerWagonMap: { other: 50 } }, 'nw5', 70)).toBe(70); + }); + + it('clamps a stale cap that now exceeds the rating (wagon type edited down)', () => { + // Saved when NW5 was rated 70T; the type was later re-rated to 45T. + expect(bulkTonsPerWagon(sugar, 'nw5', 45)).toBe(45); + }); + + it('sizes 200T of capped sugar at 4 wagons, not the 3 raw capacity implies', () => { + expect(bulkTonWagonsRequired(bulk(200), sugar, 'nw5', 70)).toBe(4); + // Same booking, no cap → the old 3-wagon answer. + expect(bulkTonWagonsRequired(bulk(200), null, 'nw5', 70)).toBe(3); + }); + + it('picks the fewest-wagon allowed type, each on its own cap', () => { + const cargoType = { + wagonTypes: [ + { id: 'nw5', capacityTons: 70 }, + { id: 'nw7', capacityTons: 80 }, + ], + tonsPerWagonMap: { nw5: 50 }, + }; + // NW5 capped 50 → 4 wagons; NW7 uncapped 80 → 3 wagons. Best = 3. + expect(bulkTonWagonsForAllowedTypes(bulk(200), cargoType, 70)).toBe(3); + }); + + it('routes PER_ITEM and PER_TON through one call', () => { + expect(bulkWagonsForAllowedTypes(bulk(200), sugar, 70)).toBe(4); + // PER_ITEM still wins where an item count is present. + const cars = { + wagonTypes: [{ id: 'nw5', capacityTons: 70 }], + itemsPerWagonMap: { nw5: 4 }, + }; + expect( + bulkWagonsForAllowedTypes( + { freightType: 'BULK', cargoTotalWeightVgm: 50, bulkTotalWeightTons: 1000 }, + cars, + 70, + ), + ).toBe(17); + }); + }); + describe('bookingCargoTons (break-bulk weight preference)', () => { it('prefers bulkTotalWeightTons over the item-count VGM column', () => { expect( diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-capacity.util.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-capacity.util.ts index 867da564b..63feb6e8c 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-capacity.util.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-capacity.util.ts @@ -152,8 +152,98 @@ export function bulkItemWagonsRequired( type ItemFitCargoType = { wagonTypes?: Array<{ id: string; capacityTons?: number | string | null }> | null; itemsPerWagonMap?: Record | null; + tonsPerWagonMap?: Record | null; } | null; +/** + * Tons of THIS cargo one wagon of this type may carry: the cargo type's + * configured loading limit when set, else the wagon's full rated capacity. + * Sugar capped at 50T rides 50T on a 70T wagon, so 200T needs 4 wagons and each + * is loaded to 50 — both the count and the fill follow from this one number. + * + * The configured cap is CLAMPED to the rated capacity rather than trusted: the + * cargo-types service rejects a cap above capacity at save time, but a wagon + * type edited DOWN afterwards would leave a stale cap that overloads the wagon. + * Clamping here means no call site can ever load past the physical rating. + */ +export function bulkTonsPerWagon( + cargoType: ItemFitCargoType | undefined, + wagonTypeId: string | null | undefined, + capacityTons: number | string | null | undefined, +): number { + const capacity = num(capacityTons); + const cap = wagonTypeId ? num(cargoType?.tonsPerWagonMap?.[wagonTypeId]) : 0; + if (!(cap > 0)) return capacity; + return capacity > 0 ? Math.min(cap, capacity) : cap; +} + +/** + * Wagons a PER_TON bulk booking needs on one wagon type, respecting the cargo + * type's per-wagon loading limit: 200T of sugar capped at 50T → 4 wagons even + * though the wagon is rated 70T. Returns 0 when there is no tonnage or no + * usable per-wagon figure, so callers can fall back as before. + */ +export function bulkTonWagonsRequired( + booking: Parameters[0], + cargoType: ItemFitCargoType | undefined, + wagonTypeId: string | null | undefined, + capacityTons: number | string | null | undefined, +): number { + const perWagon = bulkTonsPerWagon(cargoType, wagonTypeId, capacityTons); + const tons = bookingCargoTons(booking); + if (!(perWagon > 0) || !(tons > 0)) return 0; + return Math.max(1, Math.ceil(tons / perWagon)); +} + +/** + * Best (fewest-wagon) PER_TON count across the cargo type's allowed wagon + * types, each sized on its OWN loading limit — the tonnage twin of + * {@link bulkItemWagonsForAllowedTypes}, for the call sites that have no single + * wagon type fixed yet. Falls back to `fallbackCapacityTons` when the cargo + * type has no usable allowed types. + */ +export function bulkTonWagonsForAllowedTypes( + booking: Parameters[0], + cargoType: ItemFitCargoType | undefined, + fallbackCapacityTons: number, +): number { + const allowed = (cargoType?.wagonTypes ?? []).filter((wt) => num(wt.capacityTons) > 0); + if (!allowed.length) { + return bulkTonWagonsRequired(booking, cargoType, null, fallbackCapacityTons); + } + let best = 0; + for (const wagonType of allowed) { + const wagons = bulkTonWagonsRequired( + booking, + cargoType, + wagonType.id, + wagonType.capacityTons, + ); + if (wagons > 0 && (best === 0 || wagons < best)) best = wagons; + } + return best; +} + +/** + * Wagons a BULK booking needs, whichever way its cargo is measured: PER_ITEM + * sizes by indivisible items, everything else by tonnage under the cargo type's + * per-wagon loading limit. One call so no site has to remember both paths. + */ +export function bulkWagonsForAllowedTypes( + booking: Parameters[0] & { + freightType?: string | null; + cargoTotalWeightVgm?: number | string | null; + bulkTotalWeightTons?: number | string | null; + }, + cargoType: ItemFitCargoType | undefined, + fallbackCapacityTons: number, +): number { + return ( + bulkItemWagonsForAllowedTypes(booking, cargoType, fallbackCapacityTons) || + bulkTonWagonsForAllowedTypes(booking, cargoType, fallbackCapacityTons) + ); +} + /** Configured whole-items fit of one wagon type for a cargo type; null if unset. */ export function bulkItemsFitFor( cargoType: ItemFitCargoType | undefined, diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts index 485a3ceea..742bd2837 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts @@ -711,6 +711,20 @@ export class TrainSchedulingController { return res.send(buffer); } + @Get("schedules/:id/intercity/marshalling/document") + @TrainSchedulingView() + @ApiOperation({ summary: "Download current on-board intercity marshalling (Marshalling 2) PDF" }) + async intercityMarshallingDocument( + @Param("id", ParseUUIDPipe) id: string, + @Res() res: Response, + ) { + const { filename, buffer } = await this.trainSchedulingService.intercityMarshallingDocument(id); + res.setHeader("Content-Type", "application/pdf"); + res.setHeader("Content-Disposition", `inline; filename="${filename}"`); + res.setHeader("Content-Length", buffer.length); + return res.send(buffer); + } + // ---- batch / booking-window staff actions ---- @Post("schedules/:id/run-batch") @@ -842,6 +856,32 @@ export class TrainSchedulingController { return { ok: true }; } + @Get("bookings/:bookingId/allocation-candidates") + @TrainSchedulingView() + @ApiOperation({ + summary: + "Trains a paid-unallocated booking fits, split same-day vs other days", + }) + getAllocationCandidates( + @Param("bookingId", ParseUUIDPipe) bookingId: string, + ) { + return this.bookingBatchService.allocationCandidates(bookingId); + } + + @Post("bookings/:bookingId/allocate") + @TrainSchedulingUpdate() + @ApiOperation({ + summary: + "Staff: place a paid booking onto a fitting train (notifies customer on date change)", + }) + async allocatePaidBooking( + @Param("bookingId", ParseUUIDPipe) bookingId: string, + @Body("trainScheduleId", ParseUUIDPipe) trainScheduleId: string, + ) { + await this.bookingBatchService.allocatePaid(bookingId, trainScheduleId); + return { ok: true }; + } + @Get("schedules/:id/checkpoints") @TrainSchedulingView() @ApiOperation({ diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts index 406f51988..455dc3d9c 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts @@ -805,6 +805,51 @@ describe('TrainSchedulingService', () => { ).rejects.toBeInstanceOf(BadRequestException); }); + describe('restampPendingWindows (hand-configured windows are exempt)', () => { + const future = new Date(Date.now() + 30 * 24 * 3600_000); + const update = jest.fn(); + + beforeEach(() => { + update.mockClear(); + // Global rules read + the TrainSchedule repo the restamp writes through. + dataSource.getRepository.mockImplementation((entity: unknown) => { + const name = (entity as { name?: string })?.name; + if (name === 'TrainSchedulingGlobalRules') { + return { find: jest.fn().mockResolvedValue([]) }; + } + return { update }; + }); + }); + + it('re-stamps a schedule that follows the global rules', async () => { + trainSchedulesRepository.findAll.mockResolvedValue([ + { + id: 'sched-global', + direction: 'IMPORT', + scheduledDepartureDate: future, + windowRuleCustom: false, + }, + ]); + await expect(service.restampPendingWindows()).resolves.toBe(1); + expect(update).toHaveBeenCalledWith('sched-global', expect.anything()); + }); + + it('leaves a hand-configured schedule alone', async () => { + trainSchedulesRepository.findAll.mockResolvedValue([ + { + id: 'sched-custom', + direction: 'IMPORT', + scheduledDepartureDate: future, + windowRuleCustom: true, + }, + ]); + // Staff picked these times deliberately — a global-rules edit must not + // overwrite them, or the per-schedule configuration would be pointless. + await expect(service.restampPendingWindows()).resolves.toBe(0); + expect(update).not.toHaveBeenCalled(); + }); + }); + describe('getUnassignedBookings', () => { const scheduleId = 'sched-unassigned-1'; const trainSetId = 'train-set-unassigned'; @@ -1121,6 +1166,132 @@ describe('TrainSchedulingService', () => { expect(html).not.toContain('empty)'); expect(html).not.toContain('EMPTY'); }); + + // ---- intercity marshalling (Marshalling 2): the current on-board view ---- + + const onBoardView = (schedule: unknown) => + (service as never as { + intercityOnBoardView: (s: unknown) => { wagons: unknown[]; unassignedBookings: unknown[] }; + }).intercityOnBoardView(schedule); + + const buildWithOpts = (schedule: unknown, opts: unknown) => + (service as never as { + buildExportLoadListHtml: (s: unknown, o?: unknown) => string; + }).buildExportLoadListHtml(schedule, opts); + + const allocWith = (over: Record) => ({ ...loadedAllocation, ...over }); + + it('drops DEPARTED wagon slots and DEPARTED allocations from the on-board view', () => { + const schedule = { + trainSet: { + wagons: [ + { ...makeWagon(1, 'W-001', [allocWith({ status: 'LOADED' })]), status: 'RESERVED' }, + { ...makeWagon(2, 'W-002', [allocWith({ status: 'LOADED' })]), status: 'DEPARTED' }, + { + ...makeWagon(3, 'W-003', [ + allocWith({ status: 'LOADED', bookingId: 'booking-3' }), + allocWith({ status: 'DEPARTED', bookingId: 'booking-4' }), + ]), + status: 'RESERVED', + }, + ], + }, + scheduleBookings: [], + }; + + const { wagons } = onBoardView(schedule); + const numbers = (wagons as Array<{ physicalWagon: { wagonNumber: string } }>).map( + (w) => w.physicalWagon.wagonNumber, + ); + expect(numbers).toEqual(['W-001', 'W-003']); + const w3 = (wagons as Array<{ physicalWagon: { wagonNumber: string }; allocations: Array<{ bookingId: string }> }>).find( + (w) => w.physicalWagon.wagonNumber === 'W-003', + ); + expect(w3?.allocations.map((a) => a.bookingId)).toEqual(['booking-3']); + }); + + it('keeps an attached wagon whose cargo all departed, as an EMPTY row', () => { + const schedule = { + id: 'schedule-1', + trainNumber: '8302', + direction: 'EXPORT', + trainSet: { + wagons: [ + { ...makeWagon(1, 'W-001', [allocWith({ status: 'LOADED' })]), status: 'RESERVED' }, + { ...makeWagon(2, 'W-002', [allocWith({ status: 'DEPARTED' })]), status: 'RESERVED' }, + ], + }, + scheduleBookings: [], + }; + + const { wagons, unassignedBookings } = onBoardView(schedule); + const html = buildWithOpts(schedule, { wagons, unassignedBookings }); + expect(html).toContain('W-002'); + expect(html.match(/EMPTY — no cargo allocated/g)).toHaveLength(1); + expect(html).toContain('2 (1 empty)'); + }); + + it('hides a leg slot (boardYardId set) until it has confirmed LOADED cargo', () => { + const legWagonEmpty = { ...makeWagon(2, 'W-LEG', [allocWith({ status: 'RESERVED' })]), status: 'RESERVED', boardYardId: 'yard-mid' }; + const legWagonLoaded = { ...makeWagon(3, 'W-LEG2', [allocWith({ status: 'LOADED' })]), status: 'RESERVED', boardYardId: 'yard-mid' }; + const schedule = { + trainSet: { wagons: [legWagonEmpty, legWagonLoaded] }, + scheduleBookings: [], + }; + + const { wagons } = onBoardView(schedule); + const numbers = (wagons as Array<{ physicalWagon: { wagonNumber: string } }>).map( + (w) => w.physicalWagon.wagonNumber, + ); + expect(numbers).toEqual(['W-LEG2']); + }); + + it('lists an IN_TRANSIT booking with no wagon allocation in the unassigned section', () => { + const rider = { + id: 'booking-9', + reference: 'BK-2026-000009', + status: 'IN_TRANSIT', + company: { name: 'Rider Co' }, + cargoType: { cargoTypeName: 'Cement', code: 'CEM' }, + originYard: { label: 'Adama' }, + destinationYard: { label: 'Dire Dawa' }, + bookingContainers: [{ containerNumber: 'RIDE-001' }], + }; + const done = { id: 'booking-8', reference: 'BK-2026-000008', status: 'COMPLETED' }; + const schedule = { + id: 'schedule-1', + trainNumber: '8302', + direction: 'EXPORT', + trainSet: { wagons: [{ ...makeWagon(1, 'W-001', [allocWith({ status: 'LOADED' })]), status: 'RESERVED' }] }, + scheduleBookings: [{ bookingId: rider.id, booking: rider }, { bookingId: done.id, booking: done }], + }; + + const { wagons, unassignedBookings } = onBoardView(schedule); + expect((unassignedBookings as Array<{ id: string }>).map((b) => b.id)).toEqual(['booking-9']); + + const html = buildWithOpts(schedule, { + title: 'Intercity Marshalling Document / Load List (Marshalling 2)', + positionLabel: 'After Dire Dawa', + wagons, + unassignedBookings, + }); + expect(html).toContain('ON BOARD — WAGON NOT RECORDED'); + expect(html).toContain('BK-2026-000009'); + expect(html).toContain('RIDE-001'); + expect(html).not.toContain('BK-2026-000008'); + expect(html).toContain('Intercity Marshalling Document / Load List (Marshalling 2)'); + expect(html).toContain('After Dire Dawa'); + }); + + it('rejects the intercity marshalling document for a train that has not been dispatched', async () => { + trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({ + id: 'schedule-1', + status: 'SCHEDULED', + }); + await expect( + service.intercityMarshallingDocument('schedule-1'), + ).rejects.toBeInstanceOf(BadRequestException); + }); }); describe('moveWagonLoad — staff rearrange', () => { diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts index 495feedf0..df5c44fde 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts @@ -138,6 +138,7 @@ import { bookingCargoTons, bulkItemsFitFor, bulkItemWagonsRequired, + bulkTonsPerWagon, deriveTrainCapacityFromLocomotive, combinedLocomotiveLimits, trainSetLocomotiveLimits, @@ -180,6 +181,13 @@ import { const SCHEDULABLE_BOOKING_STATUSES = ['PAID'] as const; +/** Drops the keys a partial override left undefined, so `...` merges keep the base value. */ +function pickDefined(source: T): Partial { + return Object.fromEntries( + Object.entries(source).filter(([, v]) => v !== undefined), + ) as Partial; +} + /** * The booking-window rule fields frozen onto a train schedule at creation (and * refreshed by restampPendingWindows for not-yet-open schedules). The board draws @@ -905,6 +913,9 @@ export class TrainSchedulingService { windowClosesAt: cap(times.windowClosesAt, t.departure), ...ruleFields, rulePaymentWindowMinutes, + // Deliberately overridden — exempt from the global re-stamp, which would + // otherwise revert this schedule the next time global rules are saved. + windowRuleCustom: true, }); } this.logger.log( @@ -1196,6 +1207,9 @@ export class TrainSchedulingService { let restamped = 0; for (const s of schedules) { if (!s.scheduledDepartureDate || s.scheduledDepartureDate <= now) continue; + // Hand-configured windows are not "pending the global rule" — staff picked + // these times deliberately, so a global-rules edit must leave them alone. + if (s.windowRuleCustom) continue; const times = s.direction === 'EXPORT' ? computeExportWindowTimes(s.scheduledDepartureDate, cfg) @@ -1459,29 +1473,8 @@ export class TrainSchedulingService { // it on schedule. DOMESTIC runs the same one-booking-day cycle as IMPORT // (opens at 08:00 EAT `importWindowLeadDays` before departure); EXPORT opens // 24h before departure (FCFS). No schedule is ever always-open now. - const windowCfg = await this.getWindowConfig(); + const globalCfg = await this.getWindowConfig(); - // Staff cannot schedule inside the lead window — there must be room for a - // booking window before departure. IMPORT/DOMESTIC lead is in whole EAT - // days (lead 3, today 11th → first allowed departure is the 14th); EXPORT - // lead is in hours (24h = 1 day ahead). - const earliest = earliestSchedulableDeparture(direction, windowCfg, new Date()); - if (departure.getTime() < earliest.getTime()) { - const detail = - direction === 'EXPORT' - ? `at least ${windowCfg.exportBookingLeadHours} hour(s) ahead` - : `at least ${windowCfg.importWindowLeadDays} day(s) ahead`; - throw new BadRequestException( - `Departure ${departure.toISOString()} is inside the booking lead window; ` + - `${direction === 'EXPORT' ? 'export' : 'import'} trains must be scheduled ${detail} ` + - `(earliest ${earliest.toISOString()})`, - ); - } - // Freeze the rule this schedule is born with. A later global-rules edit - // only re-derives NOT-YET-OPEN schedules (see restampPendingWindows); an - // already-open schedule keeps this snapshot, and the batch board draws its - // windows from it rather than the live config. - const ruleSnapshot = windowRuleSnapshot(windowCfg); // Route+day grouping (IMPORT/DOMESTIC only): if a schedule already exists // on this origin + destination + EAT departure day, this new train JOINS // its group and adopts the group's shared window timeline (open/close + @@ -1505,6 +1498,77 @@ export class TrainSchedulingService { route.destinationYardId, departure, ); + + // Per-schedule window rule chosen at creation. Refused for a train that + // JOINS an existing route+day group: the group shares ONE window timeline, + // so a joining train adopts the anchor's times verbatim and its own + // settings would be silently discarded. Staff edit the group's window + // instead (Booking window settings, which fans out to every sibling). + if (dto.windowRule && groupAnchor) { + throw new BadRequestException( + 'This train joins an existing booking group (same route and departure day), ' + + 'which shares one booking window across all its trains. Create it with the ' + + 'group settings, then use Booking window settings to change the window for ' + + 'the whole group.', + ); + } + + // The rule this schedule is born under: staff overrides on top of the live + // global config, so an omitted field still follows the global value. + const windowCfg: BookingWindowConfig = dto.windowRule + ? { + ...globalCfg, + ...pickDefined({ + windowOpenHour: dto.windowRule.windowOpenHour, + windowCloseHour: dto.windowRule.windowCloseHour, + windowDurationHours: dto.windowRule.windowDurationHours, + docReviewMinutes: dto.windowRule.docReviewMinutes, + importWindowLeadDays: dto.windowRule.importWindowLeadDays, + exportBookingLeadHours: dto.windowRule.exportBookingLeadHours, + }), + // One pay-window override drives both directions (only the one + // matching this schedule's direction is ever read). + ...(dto.windowRule.paymentWindowMinutes !== undefined + ? { + paymentWindowMinutes: dto.windowRule.paymentWindowMinutes, + exportPaymentWindowMinutes: dto.windowRule.paymentWindowMinutes, + } + : {}), + // Close offsets are nullable-by-intent: null/0 means "close at + // departure", which must override a non-null global, so these are + // merged on presence rather than on definedness. + ...(dto.windowRule.importCloseOffsetMinutes !== undefined + ? { importCloseOffsetMinutes: dto.windowRule.importCloseOffsetMinutes ?? null } + : {}), + ...(dto.windowRule.exportCloseOffsetMinutes !== undefined + ? { exportCloseOffsetMinutes: dto.windowRule.exportCloseOffsetMinutes ?? null } + : {}), + } + : globalCfg; + + // Staff cannot schedule inside the lead window — there must be room for a + // booking window before departure. IMPORT/DOMESTIC lead is in whole EAT + // days (lead 3, today 11th → first allowed departure is the 14th); EXPORT + // lead is in hours (24h = 1 day ahead). Checked against the schedule's OWN + // lead, so a custom lead is honoured rather than rejected by the global one. + const earliest = earliestSchedulableDeparture(direction, windowCfg, new Date()); + if (departure.getTime() < earliest.getTime()) { + const detail = + direction === 'EXPORT' + ? `at least ${windowCfg.exportBookingLeadHours} hour(s) ahead` + : `at least ${windowCfg.importWindowLeadDays} day(s) ahead`; + throw new BadRequestException( + `Departure ${departure.toISOString()} is inside the booking lead window; ` + + `${direction === 'EXPORT' ? 'export' : 'import'} trains must be scheduled ${detail} ` + + `(earliest ${earliest.toISOString()})`, + ); + } + + // Freeze the rule this schedule is born with. A later global-rules edit + // only re-derives NOT-YET-OPEN schedules (see restampPendingWindows); an + // already-open schedule keeps this snapshot, and the batch board draws its + // windows from it rather than the live config. + const ruleSnapshot = windowRuleSnapshot(windowCfg); const computedTimes = direction === 'EXPORT' ? { ...ruleSnapshot, ...computeExportWindowTimes(departure, windowCfg) } @@ -1513,12 +1577,30 @@ export class TrainSchedulingService { ...ruleSnapshot, ...computeImportWindowTimes(departure, windowCfg, new Date()), }; + if ( + computedTimes.windowOpensAt.getTime() >= computedTimes.windowClosesAt.getTime() + ) { + throw new BadRequestException( + 'These booking-window settings leave no window before departure — with the ' + + 'desk hours and close offset applied, the window would only open once the ' + + 'train has left.', + ); + } const windowFields = { bookingWindowStatus: 'CLOSED', windowPhase: 'PRE_WINDOW', ...(groupAnchor ? this.groupWindowFieldsFrom(groupAnchor, departure) : computedTimes), + // `windowRuleSnapshot` never stamps the pay window (NULL = follow the + // live global value for the direction), so an explicit staff override is + // persisted here — the same field the post-creation override writes. + ...(dto.windowRule?.paymentWindowMinutes !== undefined + ? { rulePaymentWindowMinutes: dto.windowRule.paymentWindowMinutes } + : {}), + // Hand-configured windows opt OUT of the global re-stamp, or the next + // global-rules edit would overwrite exactly what staff chose here. + windowRuleCustom: dto.windowRule != null, }; // A built train's own consist is the schedule's capacity: full when all // its wagons are allocated. Trains built without wagons yet fall back to @@ -2571,7 +2653,9 @@ export class TrainSchedulingService { paymentDeadline: null, }) .where('train_schedule_id = :scheduleId', { scheduleId }) - .andWhere(`status IN ('SELECTED_FOR_BATCH', 'AWAITING_PAYMENT')`) + .andWhere( + `status IN ('SELECTED_FOR_BATCH', 'AWAITING_PAYMENT', 'PAYMENT_VERIFICATION_IN_PROGRESS')`, + ) .execute(); }); @@ -2928,6 +3012,78 @@ export class TrainSchedulingService { }; } + /** + * The train's composition as it stands right now — the source for the + * intercity marshalling ("Marshalling 2") document printed after mid-corridor + * station work. A wagon slot is on the train iff it has not DEPARTED and + * either rides the whole corridor (no boardYardId) or has confirmed LOADED + * cargo. Kept wagons carry only their LOADED allocations (DEPARTED = + * unloaded, PLANNED/RESERVED = not on board yet). + * ponytail: boardYardId presence is the "boarded yet?" heuristic; upgrade + * path is comparing the board yard against the latest checkpoint sequence. + */ + private intercityOnBoardView(schedule: TrainSchedule): { + wagons: TrainSetWagon[]; + unassignedBookings: Booking[]; + } { + const wagons = (schedule.trainSet?.wagons ?? []) + .filter((wagon) => { + if (wagon.status === 'DEPARTED') return false; + const hasLoaded = (wagon.allocations ?? []).some((a) => a.status === 'LOADED'); + return wagon.boardYardId == null || hasLoaded; + }) + .map((wagon) => ({ + ...wagon, + allocations: (wagon.allocations ?? []).filter((a) => a.status === 'LOADED'), + })) as TrainSetWagon[]; + + const onBoardBookingIds = new Set( + wagons.flatMap((wagon) => (wagon.allocations ?? []).map((a) => a.bookingId)), + ); + // IN_TRANSIT bookings with no kept allocation: intercity riders accepted + // after dispatch (never wagon-pinned) and loads whose allocation was never + // confirmed LOADED. They are physically on the train, so they get a row. + const unassignedBookings = (schedule.scheduleBookings ?? []) + .map((link) => link.booking) + .filter((booking): booking is Booking => Boolean(booking)) + .filter((booking) => booking.status === 'IN_TRANSIT' && !onBoardBookingIds.has(booking.id)); + + return { wagons, unassignedBookings }; + } + + async intercityMarshallingDocument(scheduleId: string): Promise<{ filename: string; buffer: Buffer }> { + const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId); + if (!schedule) { + throw new NotFoundException(`Train schedule ${scheduleId} not found`); + } + if (schedule.status !== 'DISPATCHED' && schedule.status !== 'ARRIVED') { + throw new BadRequestException( + 'Intercity marshalling document applies only to dispatched or arrived trains', + ); + } + + const checkpoints = await this.trainCheckpointEventsRepository.findBySchedule(scheduleId); + const last = checkpoints[checkpoints.length - 1]; + const positionLabel = last + ? `After ${last.yard?.label ?? last.yard?.code ?? 'checkpoint'}` + : `At ${schedule.originStation?.label ?? schedule.originStation?.code ?? 'origin'} — no checkpoint recorded`; + + const { wagons, unassignedBookings } = this.intercityOnBoardView(schedule); + const html = this.buildExportLoadListHtml(schedule, { + title: 'Intercity Marshalling Document / Load List (Marshalling 2)', + positionLabel, + wagons, + unassignedBookings, + }); + // Styled table-aware fallback (marshalling grid) — see importLoadListDocument. + const buffer = await this.pdfDocuments.renderTabularDocument(html, 'Intercity marshalling / load list'); + const reference = schedule.trainNumber ?? schedule.id; + return { + filename: `intercity-marshalling-${this.safeDocumentName(reference)}.pdf`, + buffer, + }; + } + /** * A container item's size in feet, for the marshalling document's 40ft/20ft * tally. Two independent sources, since only one is populated depending on @@ -2954,7 +3110,15 @@ export class TrainSchedulingService { return null; } - private buildExportLoadListHtml(schedule: TrainSchedule): string { + private buildExportLoadListHtml( + schedule: TrainSchedule, + opts?: { + title?: string; + positionLabel?: string; + wagons?: TrainSetWagon[]; + unassignedBookings?: Booking[]; + }, + ): string { const esc = (value: unknown) => String(value ?? '-') .replace(/&/g, '&') @@ -2967,7 +3131,7 @@ export class TrainSchedulingService { const bookingById = new Map((schedule.scheduleBookings ?? []).map((link) => [link.bookingId, link.booking])); // The document is checked against the physical train, so it has to run in // consist order — the relation comes back unordered. - const wagons = [...(schedule.trainSet?.wagons ?? [])].sort( + const wagons = [...(opts?.wagons ?? schedule.trainSet?.wagons ?? [])].sort( (a, b) => Number(a.sequenceNo ?? 0) - Number(b.sequenceNo ?? 0), ); const rows = wagons @@ -3011,6 +3175,29 @@ export class TrainSchedulingService { }); }) .join(''); + // Intercity riders accepted after dispatch have no wagon slot recorded — + // they are still physically on the train, so they get rows of their own. + const unassigned = opts?.unassignedBookings ?? []; + const unassignedRows = unassigned.length + ? `` + + unassigned + .map((booking) => { + const containerNumbers = (booking.bookingContainers ?? []) + .map((container) => container.containerNumber) + .filter(Boolean) + .join(', '); + const leg = `${booking.originYard?.label ?? booking.originYard?.code ?? '-'} → ${booking.destinationYard?.label ?? booking.destinationYard?.code ?? '-'}`; + return ` + + + + + + + `; + }) + .join('') + : ''; const emptyWagons = wagons.filter((wagon) => (wagon.allocations ?? []).length === 0).length; const totalWeight = wagons.reduce( (sum, wagon) => @@ -3034,7 +3221,7 @@ export class TrainSchedulingService { - Export Marshalling Document + ${esc(opts?.title ?? 'Export Marshalling Document')}
ON BOARD — WAGON NOT RECORDED
${esc(booking.reference)} — ${esc(leg)}${esc(booking.cargoType?.cargoTypeName ?? booking.cargoType?.code)}${esc(booking.company?.name)}${esc(containerNumbers)}--