BEGIN; CREATE EXTENSION IF NOT EXISTS pgcrypto; -- ----------------------------------------------------------------------------- -- Enum Types -- ----------------------------------------------------------------------------- CREATE TYPE user_status_enum AS ENUM ( 'ACTIVE', 'DISABLED' ); CREATE TYPE assignment_status_enum AS ENUM ( 'ACTIVE', 'ENDED' ); CREATE TYPE trip_status_enum AS ENUM ( 'PLANNED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED' ); CREATE TYPE device_type_enum AS ENUM ( 'SENSOR_NODE', 'GATEWAY_NODE' ); CREATE TYPE alert_type_enum AS ENUM ( 'HIGH_TEMPERATURE', 'GAS_SPIKE', 'SHOCK_DETECTED', 'OFFLINE', 'GPS_LOST' ); CREATE TYPE alert_severity_enum AS ENUM ( 'INFO', 'WARNING', 'CRITICAL' ); CREATE TYPE alert_status_enum AS ENUM ( 'OPEN', 'ACKNOWLEDGED', 'RESOLVED' ); CREATE TYPE alert_event_type_enum AS ENUM ( 'OPENED', 'ACKNOWLEDGED', 'RESOLVED', 'REOPENED', 'NOTE' ); -- ----------------------------------------------------------------------------- -- Utility Functions -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION reject_update_delete_on_append_only() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION '% is append-only; % operations are not allowed', TG_TABLE_NAME, TG_OP; END; $$ LANGUAGE plpgsql; -- ----------------------------------------------------------------------------- -- Core Identity / Access -- ----------------------------------------------------------------------------- CREATE TABLE tenants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_tenants_code_format CHECK (tenant_code ~ '^[a-z0-9][a-z0-9_-]{1,63}$') ); CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, email TEXT NOT NULL, full_name TEXT NOT NULL, password_hash TEXT NOT NULL, status user_status_enum NOT NULL DEFAULT 'ACTIVE', is_active BOOLEAN NOT NULL DEFAULT TRUE, last_login_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_users_tenant_email UNIQUE (tenant_id, email), CONSTRAINT uq_users_id_tenant UNIQUE (id, tenant_id), CONSTRAINT chk_users_email_format CHECK (position('@' in email) > 1), CONSTRAINT chk_users_deleted_after_created CHECK (deleted_at IS NULL OR deleted_at >= created_at) ); CREATE TABLE roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role_code TEXT NOT NULL UNIQUE, role_name TEXT NOT NULL, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_roles_code_format CHECK (role_code ~ '^[a-z][a-z0-9_]{2,63}$') ); CREATE TABLE user_roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, user_id UUID NOT NULL, role_id UUID NOT NULL REFERENCES roles(id) ON DELETE RESTRICT, assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), assigned_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_user_roles_tenant_user_role UNIQUE (tenant_id, user_id, role_id), CONSTRAINT fk_user_roles_user_tenant FOREIGN KEY (user_id, tenant_id) REFERENCES users(id, tenant_id) ON DELETE CASCADE ); -- ----------------------------------------------------------------------------- -- Fleet Domain -- ----------------------------------------------------------------------------- CREATE TABLE fleets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, fleet_code TEXT NOT NULL, name TEXT NOT NULL, description TEXT, is_active BOOLEAN NOT NULL DEFAULT TRUE, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_fleets_tenant_code UNIQUE (tenant_id, fleet_code), CONSTRAINT uq_fleets_id_tenant UNIQUE (id, tenant_id), CONSTRAINT chk_fleets_code_format CHECK (fleet_code ~ '^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$'), CONSTRAINT chk_fleets_deleted_after_created CHECK (deleted_at IS NULL OR deleted_at >= created_at) ); CREATE TABLE trucks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, fleet_id UUID NOT NULL, truck_code TEXT NOT NULL, plate_number TEXT, model TEXT, is_active BOOLEAN NOT NULL DEFAULT TRUE, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_trucks_tenant_code UNIQUE (tenant_id, truck_code), CONSTRAINT uq_trucks_id_tenant UNIQUE (id, tenant_id), CONSTRAINT fk_trucks_fleet_tenant FOREIGN KEY (fleet_id, tenant_id) REFERENCES fleets(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT chk_trucks_code_format CHECK (truck_code ~ '^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$'), CONSTRAINT chk_trucks_deleted_after_created CHECK (deleted_at IS NULL OR deleted_at >= created_at) ); CREATE TABLE containers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, container_code TEXT NOT NULL, container_type TEXT, cargo_type TEXT, is_active BOOLEAN NOT NULL DEFAULT TRUE, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_containers_tenant_code UNIQUE (tenant_id, container_code), CONSTRAINT uq_containers_id_tenant UNIQUE (id, tenant_id), CONSTRAINT chk_containers_code_format CHECK (container_code ~ '^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$'), CONSTRAINT chk_containers_deleted_after_created CHECK (deleted_at IS NULL OR deleted_at >= created_at) ); CREATE TABLE truck_container_assignments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, truck_id UUID NOT NULL, container_id UUID NOT NULL, assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), unassigned_at TIMESTAMPTZ, status assignment_status_enum NOT NULL DEFAULT 'ACTIVE', notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT fk_assignments_truck_tenant FOREIGN KEY (truck_id, tenant_id) REFERENCES trucks(id, tenant_id) ON DELETE CASCADE, CONSTRAINT fk_assignments_container_tenant FOREIGN KEY (container_id, tenant_id) REFERENCES containers(id, tenant_id) ON DELETE CASCADE, CONSTRAINT chk_assignments_status_window CHECK ( (status = 'ACTIVE' AND unassigned_at IS NULL) OR (status = 'ENDED' AND unassigned_at IS NOT NULL) ), CONSTRAINT chk_assignments_unassigned_after_assigned CHECK ( unassigned_at IS NULL OR unassigned_at >= assigned_at ) ); CREATE TABLE routes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, route_code TEXT NOT NULL, route_name TEXT, origin_name TEXT NOT NULL, destination_name TEXT NOT NULL, origin_lat NUMERIC(9,6), origin_lon NUMERIC(9,6), destination_lat NUMERIC(9,6), destination_lon NUMERIC(9,6), waypoints_json JSONB NOT NULL DEFAULT '[]'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_routes_tenant_code UNIQUE (tenant_id, route_code), CONSTRAINT uq_routes_id_tenant UNIQUE (id, tenant_id), CONSTRAINT chk_routes_code_format CHECK (route_code ~ '^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$'), CONSTRAINT chk_routes_origin_lat CHECK (origin_lat IS NULL OR origin_lat BETWEEN -90 AND 90), CONSTRAINT chk_routes_origin_lon CHECK (origin_lon IS NULL OR origin_lon BETWEEN -180 AND 180), CONSTRAINT chk_routes_destination_lat CHECK (destination_lat IS NULL OR destination_lat BETWEEN -90 AND 90), CONSTRAINT chk_routes_destination_lon CHECK (destination_lon IS NULL OR destination_lon BETWEEN -180 AND 180), CONSTRAINT chk_routes_waypoints_json CHECK (jsonb_typeof(waypoints_json) = 'array') ); CREATE TABLE trips ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, trip_code TEXT NOT NULL, fleet_id UUID, truck_id UUID NOT NULL, container_id UUID NOT NULL, route_id UUID, origin_name TEXT NOT NULL, destination_name TEXT NOT NULL, planned_start_at TIMESTAMPTZ, planned_end_at TIMESTAMPTZ, actual_start_at TIMESTAMPTZ, actual_end_at TIMESTAMPTZ, status trip_status_enum NOT NULL DEFAULT 'PLANNED', metadata_json JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_trips_tenant_code UNIQUE (tenant_id, trip_code), CONSTRAINT uq_trips_id_tenant UNIQUE (id, tenant_id), CONSTRAINT fk_trips_fleet_tenant FOREIGN KEY (fleet_id, tenant_id) REFERENCES fleets(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_trips_truck_tenant FOREIGN KEY (truck_id, tenant_id) REFERENCES trucks(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_trips_container_tenant FOREIGN KEY (container_id, tenant_id) REFERENCES containers(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_trips_route_tenant FOREIGN KEY (route_id, tenant_id) REFERENCES routes(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT chk_trips_code_format CHECK (trip_code ~ '^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$'), CONSTRAINT chk_trips_planned_window CHECK ( planned_end_at IS NULL OR planned_start_at IS NULL OR planned_end_at >= planned_start_at ), CONSTRAINT chk_trips_actual_window CHECK ( actual_end_at IS NULL OR actual_start_at IS NULL OR actual_end_at >= actual_start_at ), CONSTRAINT chk_trips_completed_has_end CHECK ( status <> 'COMPLETED' OR actual_end_at IS NOT NULL ), CONSTRAINT chk_trips_metadata_json CHECK (jsonb_typeof(metadata_json) = 'object') ); -- ----------------------------------------------------------------------------- -- Device Domain -- ----------------------------------------------------------------------------- CREATE TABLE device_registry ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, fleet_id UUID, truck_id UUID, container_id UUID, device_type device_type_enum NOT NULL, device_code TEXT NOT NULL, mac_address TEXT, serial_number TEXT, firmware_version TEXT, last_seen_at TIMESTAMPTZ, active_flag BOOLEAN NOT NULL DEFAULT TRUE, metadata_json JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_device_registry_tenant_code UNIQUE (tenant_id, device_code), CONSTRAINT uq_device_registry_tenant_mac UNIQUE (tenant_id, mac_address), CONSTRAINT uq_device_registry_tenant_serial UNIQUE (tenant_id, serial_number), CONSTRAINT uq_device_registry_id_tenant UNIQUE (id, tenant_id), CONSTRAINT fk_device_registry_fleet_tenant FOREIGN KEY (fleet_id, tenant_id) REFERENCES fleets(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_device_registry_truck_tenant FOREIGN KEY (truck_id, tenant_id) REFERENCES trucks(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_device_registry_container_tenant FOREIGN KEY (container_id, tenant_id) REFERENCES containers(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT chk_device_registry_code_format CHECK (device_code ~ '^[A-Za-z0-9][A-Za-z0-9:_-]{1,63}$'), CONSTRAINT chk_device_registry_mac_format CHECK ( mac_address IS NULL OR mac_address ~* '^([0-9A-F]{2}:){5}[0-9A-F]{2}$' ), CONSTRAINT chk_device_registry_metadata_json CHECK (jsonb_typeof(metadata_json) = 'object') ); CREATE TABLE device_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, device_id UUID NOT NULL, mqtt_client_id TEXT, connected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), disconnected_at TIMESTAMPTZ, disconnect_reason TEXT, ip_address INET, active_flag BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT fk_device_sessions_device_tenant FOREIGN KEY (device_id, tenant_id) REFERENCES device_registry(id, tenant_id) ON DELETE CASCADE, CONSTRAINT chk_device_sessions_window CHECK ( disconnected_at IS NULL OR disconnected_at >= connected_at ), CONSTRAINT chk_device_sessions_active_consistency CHECK ( (active_flag AND disconnected_at IS NULL) OR ((NOT active_flag) AND disconnected_at IS NOT NULL) ) ); -- ----------------------------------------------------------------------------- -- Telemetry Domain -- ----------------------------------------------------------------------------- CREATE TABLE telemetry_latest ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, fleet_id UUID, truck_id UUID NOT NULL, container_id UUID NOT NULL, trip_id UUID, gateway_device_id UUID, sensor_device_id UUID, mqtt_topic TEXT NOT NULL, seq BIGINT NOT NULL, source_ts TIMESTAMPTZ NOT NULL, received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), gps_lat NUMERIC(9,6), gps_lon NUMERIC(9,6), speed_kph NUMERIC(8,2), temperature_c NUMERIC(7,2), humidity_pct NUMERIC(5,2), pressure_hpa NUMERIC(7,2), tilt_deg NUMERIC(7,2), shock BOOLEAN NOT NULL DEFAULT FALSE, gas_raw INTEGER, gas_alert BOOLEAN NOT NULL DEFAULT FALSE, sd_ok BOOLEAN, gps_fix BOOLEAN, uplink TEXT, raw_payload JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_telemetry_latest_scope UNIQUE (tenant_id, truck_id, container_id), CONSTRAINT uq_telemetry_latest_id_tenant UNIQUE (id, tenant_id), CONSTRAINT fk_telemetry_latest_fleet_tenant FOREIGN KEY (fleet_id, tenant_id) REFERENCES fleets(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_telemetry_latest_truck_tenant FOREIGN KEY (truck_id, tenant_id) REFERENCES trucks(id, tenant_id) ON DELETE CASCADE, CONSTRAINT fk_telemetry_latest_container_tenant FOREIGN KEY (container_id, tenant_id) REFERENCES containers(id, tenant_id) ON DELETE CASCADE, CONSTRAINT fk_telemetry_latest_trip_tenant FOREIGN KEY (trip_id, tenant_id) REFERENCES trips(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_telemetry_latest_gateway_tenant FOREIGN KEY (gateway_device_id, tenant_id) REFERENCES device_registry(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_telemetry_latest_sensor_tenant FOREIGN KEY (sensor_device_id, tenant_id) REFERENCES device_registry(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT chk_telemetry_latest_seq CHECK (seq >= 0), CONSTRAINT chk_telemetry_latest_gps_lat CHECK (gps_lat IS NULL OR gps_lat BETWEEN -90 AND 90), CONSTRAINT chk_telemetry_latest_gps_lon CHECK (gps_lon IS NULL OR gps_lon BETWEEN -180 AND 180), CONSTRAINT chk_telemetry_latest_speed CHECK (speed_kph IS NULL OR speed_kph >= 0), CONSTRAINT chk_telemetry_latest_humidity CHECK (humidity_pct IS NULL OR humidity_pct BETWEEN 0 AND 100), CONSTRAINT chk_telemetry_latest_pressure CHECK (pressure_hpa IS NULL OR pressure_hpa > 0), CONSTRAINT chk_telemetry_latest_gas CHECK (gas_raw IS NULL OR gas_raw BETWEEN 0 AND 4095), CONSTRAINT chk_telemetry_latest_payload CHECK (jsonb_typeof(raw_payload) = 'object') ); CREATE TABLE telemetry_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, fleet_id UUID, truck_id UUID NOT NULL, container_id UUID NOT NULL, trip_id UUID, gateway_device_id UUID, sensor_device_id UUID, mqtt_topic TEXT NOT NULL, seq BIGINT NOT NULL, occurred_at TIMESTAMPTZ NOT NULL, received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), gps_lat NUMERIC(9,6), gps_lon NUMERIC(9,6), speed_kph NUMERIC(8,2), temperature_c NUMERIC(7,2), humidity_pct NUMERIC(5,2), pressure_hpa NUMERIC(7,2), tilt_deg NUMERIC(7,2), shock BOOLEAN NOT NULL DEFAULT FALSE, gas_raw INTEGER, gas_alert BOOLEAN NOT NULL DEFAULT FALSE, sd_ok BOOLEAN, gps_fix BOOLEAN, uplink TEXT, raw_payload JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_telemetry_history_id_tenant UNIQUE (id, tenant_id), CONSTRAINT fk_telemetry_history_fleet_tenant FOREIGN KEY (fleet_id, tenant_id) REFERENCES fleets(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_telemetry_history_truck_tenant FOREIGN KEY (truck_id, tenant_id) REFERENCES trucks(id, tenant_id) ON DELETE CASCADE, CONSTRAINT fk_telemetry_history_container_tenant FOREIGN KEY (container_id, tenant_id) REFERENCES containers(id, tenant_id) ON DELETE CASCADE, CONSTRAINT fk_telemetry_history_trip_tenant FOREIGN KEY (trip_id, tenant_id) REFERENCES trips(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_telemetry_history_gateway_tenant FOREIGN KEY (gateway_device_id, tenant_id) REFERENCES device_registry(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_telemetry_history_sensor_tenant FOREIGN KEY (sensor_device_id, tenant_id) REFERENCES device_registry(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT chk_telemetry_history_seq CHECK (seq >= 0), CONSTRAINT chk_telemetry_history_gps_lat CHECK (gps_lat IS NULL OR gps_lat BETWEEN -90 AND 90), CONSTRAINT chk_telemetry_history_gps_lon CHECK (gps_lon IS NULL OR gps_lon BETWEEN -180 AND 180), CONSTRAINT chk_telemetry_history_speed CHECK (speed_kph IS NULL OR speed_kph >= 0), CONSTRAINT chk_telemetry_history_humidity CHECK (humidity_pct IS NULL OR humidity_pct BETWEEN 0 AND 100), CONSTRAINT chk_telemetry_history_pressure CHECK (pressure_hpa IS NULL OR pressure_hpa > 0), CONSTRAINT chk_telemetry_history_gas CHECK (gas_raw IS NULL OR gas_raw BETWEEN 0 AND 4095), CONSTRAINT chk_telemetry_history_payload CHECK (jsonb_typeof(raw_payload) = 'object') ); -- ----------------------------------------------------------------------------- -- Alerting Domain -- ----------------------------------------------------------------------------- CREATE TABLE alert_rules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, fleet_id UUID, rule_name TEXT NOT NULL, alert_type alert_type_enum NOT NULL, severity alert_severity_enum NOT NULL, threshold_numeric NUMERIC(12,2), threshold_boolean BOOLEAN, duration_seconds INTEGER NOT NULL DEFAULT 0, enabled BOOLEAN NOT NULL DEFAULT TRUE, config_json JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_alert_rules_tenant_name UNIQUE (tenant_id, rule_name), CONSTRAINT uq_alert_rules_id_tenant UNIQUE (id, tenant_id), CONSTRAINT fk_alert_rules_fleet_tenant FOREIGN KEY (fleet_id, tenant_id) REFERENCES fleets(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT chk_alert_rules_duration CHECK (duration_seconds >= 0), CONSTRAINT chk_alert_rules_threshold_present CHECK ( threshold_numeric IS NOT NULL OR threshold_boolean IS NOT NULL OR duration_seconds > 0 ), CONSTRAINT chk_alert_rules_config_json CHECK (jsonb_typeof(config_json) = 'object') ); CREATE TABLE alerts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, fleet_id UUID, truck_id UUID NOT NULL, container_id UUID NOT NULL, trip_id UUID, alert_rule_id UUID, alert_type alert_type_enum NOT NULL, severity alert_severity_enum NOT NULL, status alert_status_enum NOT NULL DEFAULT 'OPEN', title TEXT, message TEXT NOT NULL, opened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), acknowledged_at TIMESTAMPTZ, resolved_at TIMESTAMPTZ, last_event_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), latest_value_numeric NUMERIC(12,2), latest_value_boolean BOOLEAN, threshold_value_numeric NUMERIC(12,2), metadata_json JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_alerts_id_tenant UNIQUE (id, tenant_id), CONSTRAINT fk_alerts_fleet_tenant FOREIGN KEY (fleet_id, tenant_id) REFERENCES fleets(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_alerts_truck_tenant FOREIGN KEY (truck_id, tenant_id) REFERENCES trucks(id, tenant_id) ON DELETE CASCADE, CONSTRAINT fk_alerts_container_tenant FOREIGN KEY (container_id, tenant_id) REFERENCES containers(id, tenant_id) ON DELETE CASCADE, CONSTRAINT fk_alerts_trip_tenant FOREIGN KEY (trip_id, tenant_id) REFERENCES trips(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT fk_alerts_rule_tenant FOREIGN KEY (alert_rule_id, tenant_id) REFERENCES alert_rules(id, tenant_id) ON DELETE RESTRICT, CONSTRAINT chk_alerts_ack_after_open CHECK ( acknowledged_at IS NULL OR acknowledged_at >= opened_at ), CONSTRAINT chk_alerts_resolved_after_open CHECK ( resolved_at IS NULL OR resolved_at >= opened_at ), CONSTRAINT chk_alerts_last_event_after_open CHECK ( last_event_at >= opened_at ), CONSTRAINT chk_alerts_lifecycle_consistency CHECK ( (status = 'OPEN' AND acknowledged_at IS NULL AND resolved_at IS NULL) OR (status = 'ACKNOWLEDGED' AND acknowledged_at IS NOT NULL AND resolved_at IS NULL) OR (status = 'RESOLVED' AND resolved_at IS NOT NULL) ), CONSTRAINT chk_alerts_metadata_json CHECK (jsonb_typeof(metadata_json) = 'object') ); CREATE TABLE alert_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, alert_id UUID NOT NULL, event_type alert_event_type_enum NOT NULL, from_status alert_status_enum, to_status alert_status_enum NOT NULL, actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, event_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), message TEXT, metadata_json JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT fk_alert_events_alert_tenant FOREIGN KEY (alert_id, tenant_id) REFERENCES alerts(id, tenant_id) ON DELETE CASCADE, CONSTRAINT chk_alert_events_transition_consistency CHECK ( (event_type = 'OPENED' AND to_status = 'OPEN') OR (event_type = 'ACKNOWLEDGED' AND to_status = 'ACKNOWLEDGED') OR (event_type = 'RESOLVED' AND to_status = 'RESOLVED') OR (event_type = 'REOPENED' AND to_status = 'OPEN') OR (event_type = 'NOTE') ), CONSTRAINT chk_alert_events_metadata_json CHECK (jsonb_typeof(metadata_json) = 'object') ); -- ----------------------------------------------------------------------------- -- Audit Domain -- ----------------------------------------------------------------------------- CREATE TABLE audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL, actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, action TEXT NOT NULL, target_type TEXT NOT NULL, target_id UUID, metadata_json JSONB NOT NULL DEFAULT '{}'::JSONB, ip_address INET, user_agent TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_audit_logs_action_not_blank CHECK (length(trim(action)) > 0), CONSTRAINT chk_audit_logs_target_type_not_blank CHECK (length(trim(target_type)) > 0), CONSTRAINT chk_audit_logs_metadata_json CHECK (jsonb_typeof(metadata_json) = 'object') ); -- ----------------------------------------------------------------------------- -- Indexes -- ----------------------------------------------------------------------------- CREATE INDEX idx_users_tenant_status ON users(tenant_id, is_active, status); CREATE INDEX idx_users_last_login_at ON users(last_login_at DESC); CREATE INDEX idx_user_roles_tenant_user ON user_roles(tenant_id, user_id); CREATE INDEX idx_user_roles_role_id ON user_roles(role_id); CREATE INDEX idx_fleets_tenant_active ON fleets(tenant_id, is_active); CREATE INDEX idx_trucks_tenant_fleet ON trucks(tenant_id, fleet_id); CREATE INDEX idx_trucks_tenant_active ON trucks(tenant_id, is_active); CREATE INDEX idx_containers_tenant_active ON containers(tenant_id, is_active); CREATE INDEX idx_assignments_tenant_truck ON truck_container_assignments(tenant_id, truck_id, assigned_at DESC); CREATE INDEX idx_assignments_tenant_container ON truck_container_assignments(tenant_id, container_id, assigned_at DESC); CREATE UNIQUE INDEX uq_assignments_active_truck ON truck_container_assignments(tenant_id, truck_id) WHERE status = 'ACTIVE' AND unassigned_at IS NULL; CREATE UNIQUE INDEX uq_assignments_active_container ON truck_container_assignments(tenant_id, container_id) WHERE status = 'ACTIVE' AND unassigned_at IS NULL; CREATE INDEX idx_routes_tenant ON routes(tenant_id); CREATE INDEX idx_trips_tenant_status ON trips(tenant_id, status, planned_start_at DESC); CREATE INDEX idx_trips_tenant_asset ON trips(tenant_id, truck_id, container_id); CREATE INDEX idx_device_registry_tenant_type_active ON device_registry(tenant_id, device_type, active_flag); CREATE INDEX idx_device_registry_last_seen_at ON device_registry(last_seen_at DESC); CREATE INDEX idx_device_registry_tenant_asset ON device_registry(tenant_id, truck_id, container_id); CREATE INDEX idx_device_sessions_tenant_device_time ON device_sessions(tenant_id, device_id, connected_at DESC); CREATE INDEX idx_device_sessions_active ON device_sessions(tenant_id, connected_at DESC) WHERE active_flag = TRUE; CREATE INDEX idx_telemetry_latest_tenant_received ON telemetry_latest(tenant_id, received_at DESC); CREATE INDEX idx_telemetry_latest_tenant_fleet ON telemetry_latest(tenant_id, fleet_id); CREATE INDEX idx_telemetry_latest_status_flags ON telemetry_latest(tenant_id, gps_fix, gas_alert, shock); CREATE INDEX idx_telemetry_history_tenant_time ON telemetry_history(tenant_id, occurred_at DESC); CREATE INDEX idx_telemetry_history_tenant_asset_time ON telemetry_history(tenant_id, truck_id, container_id, occurred_at DESC); CREATE INDEX idx_telemetry_history_tenant_trip_time ON telemetry_history(tenant_id, trip_id, occurred_at DESC); CREATE INDEX idx_telemetry_history_payload_gin ON telemetry_history USING GIN(raw_payload); CREATE INDEX idx_alert_rules_tenant_enabled ON alert_rules(tenant_id, enabled, alert_type); CREATE INDEX idx_alerts_tenant_status_severity ON alerts(tenant_id, status, severity, opened_at DESC); CREATE INDEX idx_alerts_tenant_asset_status ON alerts(tenant_id, truck_id, container_id, status); CREATE INDEX idx_alerts_tenant_type_status ON alerts(tenant_id, alert_type, status); CREATE UNIQUE INDEX uq_alerts_active_per_source ON alerts(tenant_id, truck_id, container_id, alert_type) WHERE status IN ('OPEN', 'ACKNOWLEDGED'); CREATE INDEX idx_alert_events_alert_time ON alert_events(alert_id, event_at DESC); CREATE INDEX idx_alert_events_tenant_time ON alert_events(tenant_id, event_at DESC); CREATE INDEX idx_audit_logs_tenant_time ON audit_logs(tenant_id, created_at DESC); CREATE INDEX idx_audit_logs_actor_time ON audit_logs(actor_user_id, created_at DESC); CREATE INDEX idx_audit_logs_target_lookup ON audit_logs(target_type, target_id, created_at DESC); -- ----------------------------------------------------------------------------- -- Triggers -- ----------------------------------------------------------------------------- CREATE TRIGGER trg_tenants_set_updated_at BEFORE UPDATE ON tenants FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_users_set_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_roles_set_updated_at BEFORE UPDATE ON roles FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_fleets_set_updated_at BEFORE UPDATE ON fleets FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_trucks_set_updated_at BEFORE UPDATE ON trucks FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_containers_set_updated_at BEFORE UPDATE ON containers FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_assignments_set_updated_at BEFORE UPDATE ON truck_container_assignments FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_routes_set_updated_at BEFORE UPDATE ON routes FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_trips_set_updated_at BEFORE UPDATE ON trips FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_device_registry_set_updated_at BEFORE UPDATE ON device_registry FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_device_sessions_set_updated_at BEFORE UPDATE ON device_sessions FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_telemetry_latest_set_updated_at BEFORE UPDATE ON telemetry_latest FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_alert_rules_set_updated_at BEFORE UPDATE ON alert_rules FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_alerts_set_updated_at BEFORE UPDATE ON alerts FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER trg_telemetry_history_append_only BEFORE UPDATE OR DELETE ON telemetry_history FOR EACH ROW EXECUTE FUNCTION reject_update_delete_on_append_only(); CREATE TRIGGER trg_alert_events_append_only BEFORE UPDATE OR DELETE ON alert_events FOR EACH ROW EXECUTE FUNCTION reject_update_delete_on_append_only(); CREATE TRIGGER trg_audit_logs_append_only BEFORE UPDATE OR DELETE ON audit_logs FOR EACH ROW EXECUTE FUNCTION reject_update_delete_on_append_only(); COMMIT;