-- v2.9 Logistics & Shipment Tracking -- Couriers / carriers CREATE TABLE IF NOT EXISTS couriers ( id SERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL, name VARCHAR(100) NOT NULL, -- 'DHL', 'FedEx', 'Estafeta', '99minutos', 'Uber Direct' code VARCHAR(20) NOT NULL, -- internal code tracking_url_template VARCHAR(500), -- e.g. "https://www.dhl.com/track?trackingNumber={tracking_number}" api_endpoint VARCHAR(500), api_key_encrypted TEXT, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_couriers_code ON couriers(tenant_id, code); -- Shipments CREATE TABLE IF NOT EXISTS shipments ( id SERIAL PRIMARY KEY, tenant_id INTEGER NOT NULL, branch_id INTEGER REFERENCES branches(id), shipment_type VARCHAR(20) DEFAULT 'outbound', -- outbound, inbound, return related_type VARCHAR(50), -- 'sale', 'purchase_order', 'service_order', 'marketplace_order' related_id INTEGER, courier_id INTEGER REFERENCES couriers(id), tracking_number VARCHAR(100), tracking_url VARCHAR(500), status VARCHAR(30) DEFAULT 'pending', -- pending, label_created, picked_up, in_transit, out_for_delivery, delivered, failed, returned origin_address TEXT, destination_address TEXT, recipient_name VARCHAR(200), recipient_phone VARCHAR(50), estimated_delivery DATE, actual_delivery TIMESTAMPTZ, shipping_cost NUMERIC(12,2) DEFAULT 0, weight_kg NUMERIC(8,3), dimensions_cm VARCHAR(50), -- "30x20x15" notes TEXT, created_by INTEGER REFERENCES employees(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_shipments_tracking ON shipments(tracking_number); CREATE INDEX IF NOT EXISTS idx_shipments_status ON shipments(status); CREATE INDEX IF NOT EXISTS idx_shipments_related ON shipments(related_type, related_id); -- Shipment tracking history CREATE TABLE IF NOT EXISTS shipment_tracking ( id SERIAL PRIMARY KEY, shipment_id INTEGER NOT NULL REFERENCES shipments(id) ON DELETE CASCADE, status VARCHAR(30) NOT NULL, location VARCHAR(200), description TEXT, raw_response JSONB, tracked_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_shipment_tracking_shipment ON shipment_tracking(shipment_id, tracked_at DESC); -- Trigger for updated_at CREATE OR REPLACE FUNCTION update_shipment_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_shipments_updated_at ON shipments; CREATE TRIGGER trg_shipments_updated_at BEFORE UPDATE ON shipments FOR EACH ROW EXECUTE FUNCTION update_shipment_updated_at(); -- Insert default couriers INSERT INTO couriers (tenant_id, name, code, tracking_url_template, is_active) VALUES (1, 'DHL Express', 'dhl', 'https://www.dhl.com/mx/es/home/tracking/tracking-ecommerce.html?tracking-id={tracking_number}', true), (1, 'FedEx', 'fedex', 'https://www.fedex.com/apps/fedextrack/?tracknumbers={tracking_number}', true), (1, 'Estafeta', 'estafeta', 'https://www.estafeta.com/herramientas/rastreo?guias={tracking_number}', true), (1, '99 Minutos', '99minutos', 'https://99minutos.com/track/{tracking_number}', true), (1, 'Uber Direct', 'uber_direct', NULL, true), (1, 'Recolección en tienda', 'pickup', NULL, true) ON CONFLICT DO NOTHING;