- 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>
319 lines
11 KiB
PL/PgSQL
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';
|