FASE 4: - Redis cache de stock con fallback graceful - Multi-moneda (MXN/USD) con contabilidad en MXN - Proveedores y ordenes de compra completo - Meilisearch 1.5M+ partes indexadas - Metabase KPIs con dashboard auto-generado FASE 5: - CRM mejorado: activities, tags, loyalty program, analytics - Imagenes de partes: upload, resize, thumbnails WebP - Ordenes de servicio Kanban: received->diagnosis->repair->ready->delivered - Garantias/RMA, alertas de reorden, multi-sucursal - Stubs BNPL (APLAZO) y ERP Sync (Aspel/Contpaqi) FASE 6: - Notificaciones automaticas: push/WhatsApp/email/in-app - Reportes de ahorro vs retail_price - Logistica + tracking: DHL, FedEx, Estafeta, 99min, Uber - API Publica: API keys, rate limiting, catalog search Migraciones: v1.9-v3.0 Tests: 93/93 pasando Backup: nexus_backup_20260427_045859.tar.gz
83 lines
3.3 KiB
PL/PgSQL
83 lines
3.3 KiB
PL/PgSQL
-- 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;
|