FASE 4-5-6: Infraestructura, CRM, Service Orders, Notificaciones, Ahorro, Logistica, API Publica

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
This commit is contained in:
Nexus Dev
2026-04-27 05:23:30 +00:00
parent b70cb3042b
commit 9ff3dc4c8b
71 changed files with 10939 additions and 420 deletions

View File

@@ -0,0 +1,263 @@
-- Nexus Autoparts — Master Database Schema (PostgreSQL)
-- Adapted from SQLite vehicle_database/sql/schema.sql
-- NO TecDoc data included — tables are empty and ready for manual population.
-- =====================================================
-- VEHICLES
-- =====================================================
CREATE TABLE IF NOT EXISTS brands (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
country TEXT,
founded_year INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS engines (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
displacement_cc REAL,
cylinders INTEGER,
fuel_type TEXT CHECK(fuel_type IN ('gasoline', 'diesel', 'electric', 'hybrid', 'other')),
power_hp INTEGER,
torque_nm INTEGER,
engine_code TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS models (
id SERIAL PRIMARY KEY,
brand_id INTEGER NOT NULL REFERENCES brands(id),
name TEXT NOT NULL,
body_type TEXT CHECK(body_type IN ('sedan', 'hatchback', 'suv', 'truck', 'coupe', 'convertible', 'wagon', 'van', 'other')),
generation TEXT,
production_start_year INTEGER,
production_end_year INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(brand_id, name, generation)
);
CREATE TABLE IF NOT EXISTS years (
id SERIAL PRIMARY KEY,
year INTEGER NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS model_year_engine (
id SERIAL PRIMARY KEY,
model_id INTEGER NOT NULL REFERENCES models(id),
year_id INTEGER NOT NULL REFERENCES years(id),
engine_id INTEGER NOT NULL REFERENCES engines(id),
trim_level TEXT,
drivetrain TEXT CHECK(drivetrain IN ('FWD', 'RWD', 'AWD', '4WD', 'other')),
transmission TEXT CHECK(transmission IN ('manual', 'automatic', 'CVT', 'other')),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(model_id, year_id, engine_id, trim_level)
);
CREATE INDEX IF NOT EXISTS idx_models_brand ON models(brand_id);
CREATE INDEX IF NOT EXISTS idx_model_year_engine_model ON model_year_engine(model_id);
CREATE INDEX IF NOT EXISTS idx_model_year_engine_year ON model_year_engine(year_id);
CREATE INDEX IF NOT EXISTS idx_model_year_engine_engine ON model_year_engine(engine_id);
-- =====================================================
-- PARTS CATALOG
-- =====================================================
CREATE TABLE IF NOT EXISTS part_categories (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
name_es TEXT,
parent_id INTEGER REFERENCES part_categories(id),
slug TEXT UNIQUE,
icon_name TEXT,
display_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS part_groups (
id SERIAL PRIMARY KEY,
category_id INTEGER NOT NULL REFERENCES part_categories(id),
name TEXT NOT NULL,
name_es TEXT,
slug TEXT,
display_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS parts (
id SERIAL PRIMARY KEY,
oem_part_number TEXT NOT NULL,
name TEXT NOT NULL,
name_es TEXT,
group_id INTEGER REFERENCES part_groups(id),
description TEXT,
description_es TEXT,
weight_kg REAL,
material TEXT,
is_discontinued BOOLEAN DEFAULT FALSE,
superseded_by_id INTEGER REFERENCES parts(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS vehicle_parts (
id SERIAL PRIMARY KEY,
model_year_engine_id INTEGER NOT NULL REFERENCES model_year_engine(id),
part_id INTEGER NOT NULL REFERENCES parts(id),
quantity_required INTEGER DEFAULT 1,
position TEXT,
fitment_notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(model_year_engine_id, part_id, position)
);
CREATE INDEX IF NOT EXISTS idx_part_categories_parent ON part_categories(parent_id);
CREATE INDEX IF NOT EXISTS idx_part_categories_slug ON part_categories(slug);
CREATE INDEX IF NOT EXISTS idx_part_groups_category ON part_groups(category_id);
CREATE INDEX IF NOT EXISTS idx_parts_oem ON parts(oem_part_number);
CREATE INDEX IF NOT EXISTS idx_parts_group ON parts(group_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_parts_mye ON vehicle_parts(model_year_engine_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_parts_part ON vehicle_parts(part_id);
-- =====================================================
-- AFTERMARKET & CROSS-REFERENCES
-- =====================================================
CREATE TABLE IF NOT EXISTS manufacturers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
type TEXT CHECK(type IN ('oem', 'aftermarket', 'remanufactured')),
quality_tier TEXT CHECK(quality_tier IN ('economy', 'standard', 'premium', 'oem')),
country TEXT,
logo_url TEXT,
website TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS aftermarket_parts (
id SERIAL PRIMARY KEY,
oem_part_id INTEGER NOT NULL REFERENCES parts(id),
manufacturer_id INTEGER NOT NULL REFERENCES manufacturers(id),
part_number TEXT NOT NULL,
name TEXT,
name_es TEXT,
quality_tier TEXT CHECK(quality_tier IN ('economy', 'standard', 'premium')),
price_usd REAL,
warranty_months INTEGER,
in_stock BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS part_cross_references (
id SERIAL PRIMARY KEY,
part_id INTEGER NOT NULL REFERENCES parts(id),
cross_reference_number TEXT NOT NULL,
reference_type TEXT CHECK(reference_type IN ('oem_alternate', 'supersession', 'interchange', 'competitor')),
source TEXT,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_aftermarket_oem ON aftermarket_parts(oem_part_id);
CREATE INDEX IF NOT EXISTS idx_aftermarket_manufacturer ON aftermarket_parts(manufacturer_id);
CREATE INDEX IF NOT EXISTS idx_aftermarket_part_number ON aftermarket_parts(part_number);
CREATE INDEX IF NOT EXISTS idx_cross_ref_part ON part_cross_references(part_id);
CREATE INDEX IF NOT EXISTS idx_cross_ref_number ON part_cross_references(cross_reference_number);
-- =====================================================
-- DIAGRAMAS EXPLOSIONADOS
-- =====================================================
CREATE TABLE IF NOT EXISTS diagrams (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
name_es TEXT,
group_id INTEGER NOT NULL REFERENCES part_groups(id),
image_path TEXT NOT NULL,
thumbnail_path TEXT,
svg_content TEXT,
width INTEGER,
height INTEGER,
display_order INTEGER DEFAULT 0,
source TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS vehicle_diagrams (
id SERIAL PRIMARY KEY,
diagram_id INTEGER NOT NULL REFERENCES diagrams(id),
model_year_engine_id INTEGER NOT NULL REFERENCES model_year_engine(id),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(diagram_id, model_year_engine_id)
);
CREATE TABLE IF NOT EXISTS diagram_hotspots (
id SERIAL PRIMARY KEY,
diagram_id INTEGER NOT NULL REFERENCES diagrams(id),
part_id INTEGER REFERENCES parts(id),
callout_number INTEGER,
label TEXT,
shape TEXT DEFAULT 'rect' CHECK(shape IN ('rect', 'circle', 'poly')),
coords TEXT NOT NULL,
color TEXT DEFAULT '#e74c3c',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_diagrams_group ON diagrams(group_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_diagrams_diagram ON vehicle_diagrams(diagram_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_diagrams_mye ON vehicle_diagrams(model_year_engine_id);
CREATE INDEX IF NOT EXISTS idx_hotspots_diagram ON diagram_hotspots(diagram_id);
CREATE INDEX IF NOT EXISTS idx_hotspots_part ON diagram_hotspots(part_id);
-- =====================================================
-- FULL-TEXT SEARCH (PostgreSQL tsvector)
-- =====================================================
-- Add tsvector column to parts for full-text search
ALTER TABLE parts ADD COLUMN IF NOT EXISTS search_vector tsvector;
-- Create GIN index for fast full-text search
CREATE INDEX IF NOT EXISTS idx_parts_search ON parts USING GIN(search_vector);
-- Update function to populate search_vector
CREATE OR REPLACE FUNCTION parts_search_update() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('spanish', COALESCE(NEW.name, '')), 'A') ||
setweight(to_tsvector('spanish', COALESCE(NEW.name_es, '')), 'A') ||
setweight(to_tsvector('spanish', COALESCE(NEW.oem_part_number, '')), 'B') ||
setweight(to_tsvector('spanish', COALESCE(NEW.description, '')), 'C') ||
setweight(to_tsvector('spanish', COALESCE(NEW.description_es, '')), 'C');
RETURN NEW;
END
$$ LANGUAGE plpgsql;
-- Trigger to auto-update search_vector
DROP TRIGGER IF EXISTS parts_search_trigger ON parts;
CREATE TRIGGER parts_search_trigger
BEFORE INSERT OR UPDATE ON parts
FOR EACH ROW EXECUTE FUNCTION parts_search_update();
-- =====================================================
-- VIN CACHE
-- =====================================================
CREATE TABLE IF NOT EXISTS vin_cache (
id SERIAL PRIMARY KEY,
vin TEXT NOT NULL UNIQUE,
decoded_data TEXT NOT NULL,
make TEXT,
model TEXT,
year INTEGER,
engine_info TEXT,
body_class TEXT,
drive_type TEXT,
model_year_engine_id INTEGER REFERENCES model_year_engine(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_vin_cache_vin ON vin_cache(vin);
CREATE INDEX IF NOT EXISTS idx_vin_cache_make_model ON vin_cache(make, model, year);