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/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/rule-engine.module.ts b/apps/edr-freight-api/src/modules/rule-engine/rule-engine.module.ts index 2e1fd8090..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'; @@ -88,6 +89,7 @@ import { BookingRateSnapshot } from '../bookings/entities/booking-rate-snapshot. Yard, YardDistance, YardFacility, + YardLocation, ShippingLine, Rate, ApprovalRule,