-- Migration 181: FedEx shipments foundation (case-linked) CREATE TABLE IF NOT EXISTS fedex_shipments ( id SERIAL PRIMARY KEY, booking_ref VARCHAR(64) NOT NULL UNIQUE, case_id INTEGER NOT NULL REFERENCES sag_sager(id) ON DELETE CASCADE, customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL, contact_id INTEGER REFERENCES contacts(id) ON DELETE SET NULL, service_type VARCHAR(32) NOT NULL, shipment_status VARCHAR(32) NOT NULL DEFAULT 'draft', pickup_window_start TIMESTAMP NOT NULL, pickup_window_end TIMESTAMP NOT NULL, recipient_name VARCHAR(150) NOT NULL, company_name VARCHAR(150), address_line1 VARCHAR(200) NOT NULL, address_line2 VARCHAR(200), postal_code VARCHAR(20) NOT NULL, city VARCHAR(120) NOT NULL, country_code VARCHAR(2) NOT NULL, phone VARCHAR(50), email VARCHAR(150), tracking_number VARCHAR(64), label_url TEXT, cancel_reason TEXT, dry_run BOOLEAN NOT NULL DEFAULT FALSE, submitted_at TIMESTAMP, api_payload JSONB, api_response JSONB, created_by_user_id INTEGER REFERENCES users(user_id) ON DELETE SET NULL, updated_by_user_id INTEGER REFERENCES users(user_id) ON DELETE SET NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, CONSTRAINT fedex_shipments_service_type_check CHECK (service_type IN ('PRIORITY', 'ECONOMY')), CONSTRAINT fedex_shipments_status_check CHECK ( shipment_status IN ('draft', 'submitted', 'booked', 'in_transit', 'delivered', 'cancelled', 'failed') ), CONSTRAINT fedex_shipments_pickup_window_check CHECK (pickup_window_end > pickup_window_start) ); CREATE TABLE IF NOT EXISTS fedex_shipment_packages ( id SERIAL PRIMARY KEY, shipment_id INTEGER NOT NULL REFERENCES fedex_shipments(id) ON DELETE CASCADE, weight_kg NUMERIC(10,3) NOT NULL, length_cm NUMERIC(10,2) NOT NULL, width_cm NUMERIC(10,2) NOT NULL, height_cm NUMERIC(10,2) NOT NULL, description VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fedex_package_weight_positive CHECK (weight_kg > 0), CONSTRAINT fedex_package_length_positive CHECK (length_cm > 0), CONSTRAINT fedex_package_width_positive CHECK (width_cm > 0), CONSTRAINT fedex_package_height_positive CHECK (height_cm > 0) ); CREATE TABLE IF NOT EXISTS fedex_tracking_events ( id SERIAL PRIMARY KEY, shipment_id INTEGER NOT NULL REFERENCES fedex_shipments(id) ON DELETE CASCADE, status VARCHAR(64) NOT NULL, description TEXT, event_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, location_city VARCHAR(120), location_country VARCHAR(2), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_fedex_shipments_case_id ON fedex_shipments(case_id); CREATE INDEX IF NOT EXISTS idx_fedex_shipments_status ON fedex_shipments(shipment_status); CREATE INDEX IF NOT EXISTS idx_fedex_shipments_tracking_number ON fedex_shipments(tracking_number); CREATE INDEX IF NOT EXISTS idx_fedex_shipments_created_at ON fedex_shipments(created_at DESC); CREATE INDEX IF NOT EXISTS idx_fedex_packages_shipment_id ON fedex_shipment_packages(shipment_id); CREATE INDEX IF NOT EXISTS idx_fedex_tracking_shipment_id ON fedex_tracking_events(shipment_id); CREATE INDEX IF NOT EXISTS idx_fedex_tracking_event_timestamp ON fedex_tracking_events(event_timestamp DESC); CREATE OR REPLACE FUNCTION update_fedex_shipments_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS fedex_shipments_updated_at_trigger ON fedex_shipments; CREATE TRIGGER fedex_shipments_updated_at_trigger BEFORE UPDATE ON fedex_shipments FOR EACH ROW EXECUTE FUNCTION update_fedex_shipments_updated_at();