michangarrito-database-v2/schemas/delivery.sql
rckrdmrd 362e488ae7 feat(MCH-016): Add delivery schema DDL
- Create delivery schema
- Add delivery_zones table with radius/polygon support
- Add deliveries table with status tracking and proof of delivery
- Add delivery_status_history for audit trail
- Add drivers table for delivery personnel
- Create updated_at triggers for all tables
- Add automatic status history logging trigger
- Create setup_default_zones() function for new tenants

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 03:42:57 -06:00

319 lines
11 KiB
PL/PgSQL

-- ============================================
-- 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';