diff --git a/schemas/delivery.sql b/schemas/delivery.sql new file mode 100644 index 0000000..625ff21 --- /dev/null +++ b/schemas/delivery.sql @@ -0,0 +1,318 @@ +-- ============================================ +-- MCH-016: Entregas a Domicilio +-- Schema: delivery +-- Version: 1.0.0 +-- Created: 2026-01-18 +-- ============================================ + +-- Create schema if not exists +CREATE SCHEMA IF NOT EXISTS delivery; + +-- ============================================ +-- Table: delivery_zones +-- Delivery zone configuration (radius or polygon) +-- ============================================ + +CREATE TABLE IF NOT EXISTS delivery.delivery_zones ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + type VARCHAR(20) NOT NULL DEFAULT 'radius' CHECK (type IN ('radius', 'polygon')), + coordinates JSONB, + delivery_fee DECIMAL(10,2) NOT NULL DEFAULT 0, + min_order DECIMAL(10,2) NOT NULL DEFAULT 0, + estimated_time_minutes INTEGER NOT NULL DEFAULT 30, + active BOOLEAN NOT NULL DEFAULT true, + priority INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Indexes for delivery_zones +CREATE INDEX IF NOT EXISTS idx_delivery_zones_tenant_active + ON delivery.delivery_zones(tenant_id, active); + +CREATE INDEX IF NOT EXISTS idx_delivery_zones_priority + ON delivery.delivery_zones(tenant_id, priority); + +-- Comments +COMMENT ON TABLE delivery.delivery_zones IS 'Delivery zone configuration with coverage areas'; +COMMENT ON COLUMN delivery.delivery_zones.type IS 'Zone type: radius or polygon'; +COMMENT ON COLUMN delivery.delivery_zones.coordinates IS 'JSON: center+radiusKm for radius, polygon array for polygon, colonias list for colonia-based'; +COMMENT ON COLUMN delivery.delivery_zones.priority IS 'Lower number = higher priority for overlapping zones'; + +-- ============================================ +-- Table: deliveries +-- Delivery tracking for orders +-- ============================================ + +CREATE TABLE IF NOT EXISTS delivery.deliveries ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE, + order_id UUID NOT NULL REFERENCES orders.orders(id) ON DELETE CASCADE, + zone_id UUID REFERENCES delivery.delivery_zones(id) ON DELETE SET NULL, + + -- Assignment + assigned_to UUID, + assigned_to_name VARCHAR(100), + + -- Status + status VARCHAR(20) NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'assigned', 'picked_up', 'in_transit', 'arrived', 'delivered', 'failed', 'cancelled')), + + -- Delivery info + delivery_address TEXT NOT NULL, + delivery_coordinates JSONB, + delivery_notes TEXT, + customer_phone VARCHAR(20), + delivery_fee DECIMAL(10,2) NOT NULL DEFAULT 0, + estimated_minutes INTEGER, + + -- Timestamps for tracking + assigned_at TIMESTAMPTZ, + picked_up_at TIMESTAMPTZ, + in_transit_at TIMESTAMPTZ, + arrived_at TIMESTAMPTZ, + delivered_at TIMESTAMPTZ, + + -- Proof of delivery + proof_photo_url TEXT, + signature_url TEXT, + recipient_name VARCHAR(100), + + -- Failure handling + failure_reason TEXT, + retry_count INTEGER NOT NULL DEFAULT 0, + + -- Audit + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Indexes for deliveries +CREATE INDEX IF NOT EXISTS idx_deliveries_tenant_status + ON delivery.deliveries(tenant_id, status); + +CREATE INDEX IF NOT EXISTS idx_deliveries_order + ON delivery.deliveries(order_id); + +CREATE INDEX IF NOT EXISTS idx_deliveries_assigned + ON delivery.deliveries(assigned_to, status); + +CREATE INDEX IF NOT EXISTS idx_deliveries_created + ON delivery.deliveries(tenant_id, created_at DESC); + +-- Unique constraint: one delivery per order +CREATE UNIQUE INDEX IF NOT EXISTS idx_deliveries_order_unique + ON delivery.deliveries(order_id); + +-- Comments +COMMENT ON TABLE delivery.deliveries IS 'Delivery tracking for orders with status history'; +COMMENT ON COLUMN delivery.deliveries.status IS 'Delivery status: pending->assigned->picked_up->in_transit->arrived->delivered'; +COMMENT ON COLUMN delivery.deliveries.delivery_coordinates IS 'JSON with lat/lng for delivery location'; +COMMENT ON COLUMN delivery.deliveries.proof_photo_url IS 'Photo proof of delivery'; +COMMENT ON COLUMN delivery.deliveries.signature_url IS 'Digital signature from recipient'; + +-- ============================================ +-- Table: delivery_status_history +-- Audit trail for delivery status changes +-- ============================================ + +CREATE TABLE IF NOT EXISTS delivery.delivery_status_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + delivery_id UUID NOT NULL REFERENCES delivery.deliveries(id) ON DELETE CASCADE, + status VARCHAR(20) NOT NULL, + previous_status VARCHAR(20), + changed_by UUID, + changed_by_name VARCHAR(100), + notes TEXT, + location JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Indexes for status history +CREATE INDEX IF NOT EXISTS idx_delivery_status_history_delivery + ON delivery.delivery_status_history(delivery_id, created_at DESC); + +-- Comments +COMMENT ON TABLE delivery.delivery_status_history IS 'Audit trail for delivery status changes'; +COMMENT ON COLUMN delivery.delivery_status_history.location IS 'GPS coordinates when status changed'; + +-- ============================================ +-- Table: drivers +-- Driver registry for delivery assignments +-- ============================================ + +CREATE TABLE IF NOT EXISTS delivery.drivers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES core.tenants(id) ON DELETE CASCADE, + user_id UUID, + name VARCHAR(100) NOT NULL, + phone VARCHAR(20) NOT NULL, + vehicle_type VARCHAR(50), + vehicle_plate VARCHAR(20), + photo_url TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'available' + CHECK (status IN ('available', 'busy', 'offline', 'inactive')), + current_location JSONB, + last_seen_at TIMESTAMPTZ, + active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Indexes for drivers +CREATE INDEX IF NOT EXISTS idx_drivers_tenant_status + ON delivery.drivers(tenant_id, status); + +CREATE INDEX IF NOT EXISTS idx_drivers_tenant_active + ON delivery.drivers(tenant_id, active); + +-- Comments +COMMENT ON TABLE delivery.drivers IS 'Driver registry for delivery operations'; +COMMENT ON COLUMN delivery.drivers.vehicle_type IS 'Type of vehicle: moto, bici, auto, pie'; +COMMENT ON COLUMN delivery.drivers.current_location IS 'Last known GPS location'; + +-- ============================================ +-- Triggers for updated_at +-- ============================================ + +-- delivery_zones +CREATE OR REPLACE FUNCTION delivery.update_delivery_zones_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trigger_delivery_zones_updated_at ON delivery.delivery_zones; +CREATE TRIGGER trigger_delivery_zones_updated_at + BEFORE UPDATE ON delivery.delivery_zones + FOR EACH ROW EXECUTE FUNCTION delivery.update_delivery_zones_updated_at(); + +-- deliveries +CREATE OR REPLACE FUNCTION delivery.update_deliveries_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trigger_deliveries_updated_at ON delivery.deliveries; +CREATE TRIGGER trigger_deliveries_updated_at + BEFORE UPDATE ON delivery.deliveries + FOR EACH ROW EXECUTE FUNCTION delivery.update_deliveries_updated_at(); + +-- drivers +CREATE OR REPLACE FUNCTION delivery.update_drivers_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trigger_drivers_updated_at ON delivery.drivers; +CREATE TRIGGER trigger_drivers_updated_at + BEFORE UPDATE ON delivery.drivers + FOR EACH ROW EXECUTE FUNCTION delivery.update_drivers_updated_at(); + +-- ============================================ +-- Trigger for automatic status history +-- ============================================ + +CREATE OR REPLACE FUNCTION delivery.log_delivery_status_change() +RETURNS TRIGGER AS $$ +BEGIN + IF OLD.status IS DISTINCT FROM NEW.status THEN + INSERT INTO delivery.delivery_status_history ( + delivery_id, + status, + previous_status, + changed_by, + notes + ) VALUES ( + NEW.id, + NEW.status, + OLD.status, + NULL, -- Would need to get from context + CASE + WHEN NEW.status = 'failed' THEN NEW.failure_reason + ELSE NULL + END + ); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trigger_delivery_status_history ON delivery.deliveries; +CREATE TRIGGER trigger_delivery_status_history + AFTER UPDATE ON delivery.deliveries + FOR EACH ROW EXECUTE FUNCTION delivery.log_delivery_status_change(); + +-- ============================================ +-- Default zone templates for new tenants +-- ============================================ + +-- This function can be called to set up default zones for a tenant +CREATE OR REPLACE FUNCTION delivery.setup_default_zones(p_tenant_id UUID, p_center_lat DECIMAL, p_center_lng DECIMAL) +RETURNS void AS $$ +BEGIN + -- Zone 1: Centro (0-2 km) - Free delivery + INSERT INTO delivery.delivery_zones (tenant_id, name, type, coordinates, delivery_fee, min_order, estimated_time_minutes, priority) + VALUES ( + p_tenant_id, + 'Centro (0-2 km)', + 'radius', + jsonb_build_object('center', jsonb_build_object('lat', p_center_lat, 'lng', p_center_lng), 'radiusKm', 2), + 0, + 0, + 15, + 1 + ); + + -- Zone 2: Cercano (2-5 km) - $20 delivery + INSERT INTO delivery.delivery_zones (tenant_id, name, type, coordinates, delivery_fee, min_order, estimated_time_minutes, priority) + VALUES ( + p_tenant_id, + 'Cercano (2-5 km)', + 'radius', + jsonb_build_object('center', jsonb_build_object('lat', p_center_lat, 'lng', p_center_lng), 'radiusKm', 5), + 20, + 50, + 25, + 2 + ); + + -- Zone 3: Lejano (5-10 km) - $35 delivery + INSERT INTO delivery.delivery_zones (tenant_id, name, type, coordinates, delivery_fee, min_order, estimated_time_minutes, priority) + VALUES ( + p_tenant_id, + 'Lejano (5-10 km)', + 'radius', + jsonb_build_object('center', jsonb_build_object('lat', p_center_lat, 'lng', p_center_lng), 'radiusKm', 10), + 35, + 100, + 40, + 3 + ); +END; +$$ LANGUAGE plpgsql; + +-- ============================================ +-- Grant permissions +-- ============================================ + +GRANT USAGE ON SCHEMA delivery TO michangarrito_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA delivery TO michangarrito_app; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA delivery TO michangarrito_app; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA delivery TO michangarrito_app; + +-- ============================================ +-- Version tracking +-- ============================================ + +COMMENT ON SCHEMA delivery IS 'MCH-016: Delivery management - zones, tracking, drivers. Version 1.0.0';