343 lines
12 KiB
Markdown
343 lines
12 KiB
Markdown
# Database Schema - Xpeditis
|
|
|
|
## Overview
|
|
|
|
PostgreSQL 15 database schema for the Xpeditis maritime freight booking platform.
|
|
|
|
**Extensions Required**:
|
|
- `uuid-ossp` - UUID generation
|
|
- `pg_trgm` - Trigram fuzzy search for ports
|
|
|
|
---
|
|
|
|
## Tables
|
|
|
|
### 1. organizations
|
|
|
|
**Purpose**: Store business organizations (freight forwarders, carriers, shippers)
|
|
|
|
| Column | Type | Constraints | Description |
|
|
|--------|------|-------------|-------------|
|
|
| id | UUID | PRIMARY KEY | Organization ID |
|
|
| name | VARCHAR(255) | NOT NULL, UNIQUE | Organization name |
|
|
| type | VARCHAR(50) | NOT NULL | FREIGHT_FORWARDER, CARRIER, SHIPPER |
|
|
| scac | CHAR(4) | UNIQUE, NULLABLE | Standard Carrier Alpha Code (carriers only) |
|
|
| address_street | VARCHAR(255) | NOT NULL | Street address |
|
|
| address_city | VARCHAR(100) | NOT NULL | City |
|
|
| address_state | VARCHAR(100) | NULLABLE | State/Province |
|
|
| address_postal_code | VARCHAR(20) | NOT NULL | Postal code |
|
|
| address_country | CHAR(2) | NOT NULL | ISO 3166-1 alpha-2 country code |
|
|
| logo_url | TEXT | NULLABLE | Logo URL |
|
|
| documents | JSONB | DEFAULT '[]' | Array of document metadata |
|
|
| is_active | BOOLEAN | DEFAULT TRUE | Active status |
|
|
| created_at | TIMESTAMP | DEFAULT NOW() | Creation timestamp |
|
|
| updated_at | TIMESTAMP | DEFAULT NOW() | Last update timestamp |
|
|
|
|
**Indexes**:
|
|
- `idx_organizations_type` on (type)
|
|
- `idx_organizations_scac` on (scac)
|
|
- `idx_organizations_active` on (is_active)
|
|
|
|
**Business Rules**:
|
|
- SCAC must be 4 uppercase letters
|
|
- SCAC is required for CARRIER type, null for others
|
|
- Name must be unique
|
|
|
|
---
|
|
|
|
### 2. users
|
|
|
|
**Purpose**: User accounts for authentication and authorization
|
|
|
|
| Column | Type | Constraints | Description |
|
|
|--------|------|-------------|-------------|
|
|
| id | UUID | PRIMARY KEY | User ID |
|
|
| organization_id | UUID | NOT NULL, FK | Organization reference |
|
|
| email | VARCHAR(255) | NOT NULL, UNIQUE | Email address (lowercase) |
|
|
| password_hash | VARCHAR(255) | NOT NULL | Bcrypt password hash |
|
|
| role | VARCHAR(50) | NOT NULL | ADMIN, MANAGER, USER, VIEWER |
|
|
| first_name | VARCHAR(100) | NOT NULL | First name |
|
|
| last_name | VARCHAR(100) | NOT NULL | Last name |
|
|
| phone_number | VARCHAR(20) | NULLABLE | Phone number |
|
|
| totp_secret | VARCHAR(255) | NULLABLE | 2FA TOTP secret |
|
|
| is_email_verified | BOOLEAN | DEFAULT FALSE | Email verification status |
|
|
| is_active | BOOLEAN | DEFAULT TRUE | Account active status |
|
|
| last_login_at | TIMESTAMP | NULLABLE | Last login timestamp |
|
|
| created_at | TIMESTAMP | DEFAULT NOW() | Creation timestamp |
|
|
| updated_at | TIMESTAMP | DEFAULT NOW() | Last update timestamp |
|
|
|
|
**Indexes**:
|
|
- `idx_users_email` on (email)
|
|
- `idx_users_organization` on (organization_id)
|
|
- `idx_users_role` on (role)
|
|
- `idx_users_active` on (is_active)
|
|
|
|
**Foreign Keys**:
|
|
- `organization_id` → organizations(id) ON DELETE CASCADE
|
|
|
|
**Business Rules**:
|
|
- Email must be unique and lowercase
|
|
- Password must be hashed with bcrypt (12+ rounds)
|
|
|
|
---
|
|
|
|
### 3. carriers
|
|
|
|
**Purpose**: Shipping carrier information and API configuration
|
|
|
|
| Column | Type | Constraints | Description |
|
|
|--------|------|-------------|-------------|
|
|
| id | UUID | PRIMARY KEY | Carrier ID |
|
|
| name | VARCHAR(255) | NOT NULL | Carrier name (e.g., "Maersk") |
|
|
| code | VARCHAR(50) | NOT NULL, UNIQUE | Carrier code (e.g., "MAERSK") |
|
|
| scac | CHAR(4) | NOT NULL, UNIQUE | Standard Carrier Alpha Code |
|
|
| logo_url | TEXT | NULLABLE | Logo URL |
|
|
| website | TEXT | NULLABLE | Carrier website |
|
|
| api_config | JSONB | NULLABLE | API configuration (baseUrl, credentials, timeout, etc.) |
|
|
| is_active | BOOLEAN | DEFAULT TRUE | Active status |
|
|
| supports_api | BOOLEAN | DEFAULT FALSE | Has API integration |
|
|
| created_at | TIMESTAMP | DEFAULT NOW() | Creation timestamp |
|
|
| updated_at | TIMESTAMP | DEFAULT NOW() | Last update timestamp |
|
|
|
|
**Indexes**:
|
|
- `idx_carriers_code` on (code)
|
|
- `idx_carriers_scac` on (scac)
|
|
- `idx_carriers_active` on (is_active)
|
|
- `idx_carriers_supports_api` on (supports_api)
|
|
|
|
**Business Rules**:
|
|
- SCAC must be 4 uppercase letters
|
|
- Code must be uppercase letters and underscores only
|
|
- api_config is required if supports_api is true
|
|
|
|
---
|
|
|
|
### 4. ports
|
|
|
|
**Purpose**: Maritime port database (based on UN/LOCODE)
|
|
|
|
| Column | Type | Constraints | Description |
|
|
|--------|------|-------------|-------------|
|
|
| id | UUID | PRIMARY KEY | Port ID |
|
|
| code | CHAR(5) | NOT NULL, UNIQUE | UN/LOCODE (e.g., "NLRTM") |
|
|
| name | VARCHAR(255) | NOT NULL | Port name |
|
|
| city | VARCHAR(255) | NOT NULL | City name |
|
|
| country | CHAR(2) | NOT NULL | ISO 3166-1 alpha-2 country code |
|
|
| country_name | VARCHAR(100) | NOT NULL | Full country name |
|
|
| latitude | DECIMAL(9,6) | NOT NULL | Latitude (-90 to 90) |
|
|
| longitude | DECIMAL(9,6) | NOT NULL | Longitude (-180 to 180) |
|
|
| timezone | VARCHAR(50) | NULLABLE | IANA timezone |
|
|
| is_active | BOOLEAN | DEFAULT TRUE | Active status |
|
|
| created_at | TIMESTAMP | DEFAULT NOW() | Creation timestamp |
|
|
| updated_at | TIMESTAMP | DEFAULT NOW() | Last update timestamp |
|
|
|
|
**Indexes**:
|
|
- `idx_ports_code` on (code)
|
|
- `idx_ports_country` on (country)
|
|
- `idx_ports_active` on (is_active)
|
|
- `idx_ports_name_trgm` GIN on (name gin_trgm_ops) -- Fuzzy search
|
|
- `idx_ports_city_trgm` GIN on (city gin_trgm_ops) -- Fuzzy search
|
|
- `idx_ports_coordinates` on (latitude, longitude)
|
|
|
|
**Business Rules**:
|
|
- Code must be 5 uppercase alphanumeric characters (UN/LOCODE format)
|
|
- Latitude: -90 to 90
|
|
- Longitude: -180 to 180
|
|
|
|
---
|
|
|
|
### 5. rate_quotes
|
|
|
|
**Purpose**: Shipping rate quotes from carriers
|
|
|
|
| Column | Type | Constraints | Description |
|
|
|--------|------|-------------|-------------|
|
|
| id | UUID | PRIMARY KEY | Rate quote ID |
|
|
| carrier_id | UUID | NOT NULL, FK | Carrier reference |
|
|
| carrier_name | VARCHAR(255) | NOT NULL | Carrier name (denormalized) |
|
|
| carrier_code | VARCHAR(50) | NOT NULL | Carrier code (denormalized) |
|
|
| origin_code | CHAR(5) | NOT NULL | Origin port code |
|
|
| origin_name | VARCHAR(255) | NOT NULL | Origin port name (denormalized) |
|
|
| origin_country | VARCHAR(100) | NOT NULL | Origin country (denormalized) |
|
|
| destination_code | CHAR(5) | NOT NULL | Destination port code |
|
|
| destination_name | VARCHAR(255) | NOT NULL | Destination port name (denormalized) |
|
|
| destination_country | VARCHAR(100) | NOT NULL | Destination country (denormalized) |
|
|
| base_freight | DECIMAL(10,2) | NOT NULL | Base freight amount |
|
|
| surcharges | JSONB | DEFAULT '[]' | Array of surcharges |
|
|
| total_amount | DECIMAL(10,2) | NOT NULL | Total price |
|
|
| currency | CHAR(3) | NOT NULL | ISO 4217 currency code |
|
|
| container_type | VARCHAR(20) | NOT NULL | Container type (e.g., "40HC") |
|
|
| mode | VARCHAR(10) | NOT NULL | FCL or LCL |
|
|
| etd | TIMESTAMP | NOT NULL | Estimated Time of Departure |
|
|
| eta | TIMESTAMP | NOT NULL | Estimated Time of Arrival |
|
|
| transit_days | INTEGER | NOT NULL | Transit days |
|
|
| route | JSONB | NOT NULL | Array of route segments |
|
|
| availability | INTEGER | NOT NULL | Available container slots |
|
|
| frequency | VARCHAR(50) | NOT NULL | Service frequency |
|
|
| vessel_type | VARCHAR(100) | NULLABLE | Vessel type |
|
|
| co2_emissions_kg | INTEGER | NULLABLE | CO2 emissions in kg |
|
|
| valid_until | TIMESTAMP | NOT NULL | Quote expiry (createdAt + 15 min) |
|
|
| created_at | TIMESTAMP | DEFAULT NOW() | Creation timestamp |
|
|
| updated_at | TIMESTAMP | DEFAULT NOW() | Last update timestamp |
|
|
|
|
**Indexes**:
|
|
- `idx_rate_quotes_carrier` on (carrier_id)
|
|
- `idx_rate_quotes_origin_dest` on (origin_code, destination_code)
|
|
- `idx_rate_quotes_container_type` on (container_type)
|
|
- `idx_rate_quotes_etd` on (etd)
|
|
- `idx_rate_quotes_valid_until` on (valid_until)
|
|
- `idx_rate_quotes_created_at` on (created_at)
|
|
- `idx_rate_quotes_search` on (origin_code, destination_code, container_type, etd)
|
|
|
|
**Foreign Keys**:
|
|
- `carrier_id` → carriers(id) ON DELETE CASCADE
|
|
|
|
**Business Rules**:
|
|
- base_freight > 0
|
|
- total_amount > 0
|
|
- eta > etd
|
|
- transit_days > 0
|
|
- availability >= 0
|
|
- valid_until = created_at + 15 minutes
|
|
- Automatically delete expired quotes (valid_until < NOW())
|
|
|
|
---
|
|
|
|
### 6. containers
|
|
|
|
**Purpose**: Container information for bookings
|
|
|
|
| Column | Type | Constraints | Description |
|
|
|--------|------|-------------|-------------|
|
|
| id | UUID | PRIMARY KEY | Container ID |
|
|
| booking_id | UUID | NULLABLE, FK | Booking reference (nullable until assigned) |
|
|
| type | VARCHAR(20) | NOT NULL | Container type (e.g., "40HC") |
|
|
| category | VARCHAR(20) | NOT NULL | DRY, REEFER, OPEN_TOP, FLAT_RACK, TANK |
|
|
| size | CHAR(2) | NOT NULL | 20, 40, 45 |
|
|
| height | VARCHAR(20) | NOT NULL | STANDARD, HIGH_CUBE |
|
|
| container_number | VARCHAR(11) | NULLABLE, UNIQUE | ISO 6346 container number |
|
|
| seal_number | VARCHAR(50) | NULLABLE | Seal number |
|
|
| vgm | INTEGER | NULLABLE | Verified Gross Mass (kg) |
|
|
| tare_weight | INTEGER | NULLABLE | Empty container weight (kg) |
|
|
| max_gross_weight | INTEGER | NULLABLE | Maximum gross weight (kg) |
|
|
| temperature | DECIMAL(4,1) | NULLABLE | Temperature for reefer (°C) |
|
|
| humidity | INTEGER | NULLABLE | Humidity for reefer (%) |
|
|
| ventilation | VARCHAR(100) | NULLABLE | Ventilation settings |
|
|
| is_hazmat | BOOLEAN | DEFAULT FALSE | Hazmat cargo |
|
|
| imo_class | VARCHAR(10) | NULLABLE | IMO hazmat class |
|
|
| cargo_description | TEXT | NULLABLE | Cargo description |
|
|
| created_at | TIMESTAMP | DEFAULT NOW() | Creation timestamp |
|
|
| updated_at | TIMESTAMP | DEFAULT NOW() | Last update timestamp |
|
|
|
|
**Indexes**:
|
|
- `idx_containers_booking` on (booking_id)
|
|
- `idx_containers_number` on (container_number)
|
|
- `idx_containers_type` on (type)
|
|
|
|
**Foreign Keys**:
|
|
- `booking_id` → bookings(id) ON DELETE SET NULL
|
|
|
|
**Business Rules**:
|
|
- container_number must follow ISO 6346 format if provided
|
|
- vgm > 0 if provided
|
|
- temperature between -40 and 40 for reefer containers
|
|
- imo_class required if is_hazmat = true
|
|
|
|
---
|
|
|
|
## Relationships
|
|
|
|
```
|
|
organizations 1──* users
|
|
carriers 1──* rate_quotes
|
|
```
|
|
|
|
---
|
|
|
|
## Data Volumes
|
|
|
|
**Estimated Sizes**:
|
|
- `organizations`: ~1,000 rows
|
|
- `users`: ~10,000 rows
|
|
- `carriers`: ~50 rows
|
|
- `ports`: ~10,000 rows (seeded from UN/LOCODE)
|
|
- `rate_quotes`: ~1M rows/year (auto-deleted after expiry)
|
|
- `containers`: ~100K rows/year
|
|
|
|
---
|
|
|
|
## Migrations Strategy
|
|
|
|
**Migration Order**:
|
|
1. Create extensions (uuid-ossp, pg_trgm)
|
|
2. Create organizations table + indexes
|
|
3. Create users table + indexes + FK
|
|
4. Create carriers table + indexes
|
|
5. Create ports table + indexes (with GIN indexes)
|
|
6. Create rate_quotes table + indexes + FK
|
|
7. Create containers table + indexes + FK (Phase 2)
|
|
|
|
---
|
|
|
|
## Seed Data
|
|
|
|
**Required Seeds**:
|
|
1. **Carriers** (5 major carriers)
|
|
- Maersk (MAEU)
|
|
- MSC (MSCU)
|
|
- CMA CGM (CMDU)
|
|
- Hapag-Lloyd (HLCU)
|
|
- ONE (ONEY)
|
|
|
|
2. **Ports** (~10,000 from UN/LOCODE dataset)
|
|
- Major ports: Rotterdam (NLRTM), Shanghai (CNSHA), Singapore (SGSIN), etc.
|
|
|
|
3. **Test Organizations** (3 test orgs)
|
|
- Test Freight Forwarder
|
|
- Test Carrier
|
|
- Test Shipper
|
|
|
|
---
|
|
|
|
## Performance Optimizations
|
|
|
|
1. **Indexes**:
|
|
- Composite index on rate_quotes (origin, destination, container_type, etd) for search
|
|
- GIN indexes on ports (name, city) for fuzzy search with pg_trgm
|
|
- Indexes on all foreign keys
|
|
- Indexes on frequently filtered columns (is_active, type, etc.)
|
|
|
|
2. **Partitioning** (Future):
|
|
- Partition rate_quotes by created_at (monthly partitions)
|
|
- Auto-drop old partitions (>3 months)
|
|
|
|
3. **Materialized Views** (Future):
|
|
- Popular trade lanes (top 100)
|
|
- Carrier performance metrics
|
|
|
|
4. **Cleanup Jobs**:
|
|
- Delete expired rate_quotes (valid_until < NOW()) - Daily cron
|
|
- Archive old bookings (>1 year) - Monthly
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
1. **Row-Level Security** (Phase 2)
|
|
- Users can only access their organization's data
|
|
- Admins can access all data
|
|
|
|
2. **Sensitive Data**:
|
|
- password_hash: bcrypt with 12+ rounds
|
|
- totp_secret: encrypted at rest
|
|
- api_config: encrypted credentials
|
|
|
|
3. **Audit Logging** (Phase 3)
|
|
- Track all sensitive operations (login, booking creation, etc.)
|
|
|
|
---
|
|
|
|
**Schema Version**: 1.0.0
|
|
**Last Updated**: 2025-10-08
|
|
**Database**: PostgreSQL 15+
|