Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos - F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado - F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment - F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings) - F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant - Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas - Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant) - Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant - Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
This commit is contained in:
@@ -70,6 +70,11 @@ MIGRATIONS = {
|
||||
"v4.21": "v4.21_cashier_workshop_permissions.sql",
|
||||
"v4.22": "v4.22_cashier_invoicing_permissions.sql",
|
||||
"v4.23": "v4.23_service_order_mechanic_name.sql",
|
||||
"v4.24": "v4.24_trgm_search_indexes.sql",
|
||||
"v4.25": "v4.25_transfer_documents.sql",
|
||||
"v4.26": "v4.26_sales_client_uuid.sql",
|
||||
"v4.27": "v4.27_yonke_m1.sql",
|
||||
"v4.28": "v4.28_meli_commerce.sql",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
-- : SKIP
|
||||
-- v1.2: Add subdomain column to tenants table (nexus_master)
|
||||
-- This migration runs against the MASTER database, not tenant databases.
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
-- : SKIP
|
||||
-- Marketplace orders (cross-tenant)
|
||||
-- These tables go in nexus_autoparts (master DB), not tenant DBs
|
||||
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
-- Applies to: master DB (nexus_autoparts) and all tenant DBs
|
||||
|
||||
-- ─── MASTER DB ─────────────────────────────────
|
||||
-- These indexes only exist in the master DB (nexus_autoparts). On tenant DBs
|
||||
-- (the `parts`/`warehouse_inventory` OEM tables live only in master) they are
|
||||
-- skipped via existence guards so this migration can run against tenants too.
|
||||
|
||||
-- Index for parts name lookups (used by get_part_types, shop supplies)
|
||||
CREATE INDEX IF NOT EXISTS idx_parts_name_part ON parts(name_part);
|
||||
CREATE INDEX IF NOT EXISTS idx_parts_name_part_pattern ON parts(name_part text_pattern_ops);
|
||||
|
||||
-- Partial index for warehouse_inventory stock lookups (already created in Fase 1,
|
||||
-- kept here for completeness on fresh installs)
|
||||
CREATE INDEX IF NOT EXISTS idx_wi_part_stock_positive
|
||||
ON warehouse_inventory(part_id) WHERE stock_quantity > 0;
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('public.parts') IS NOT NULL THEN
|
||||
EXECUTE 'CREATE INDEX IF NOT EXISTS idx_parts_name_part ON parts(name_part)';
|
||||
EXECUTE 'CREATE INDEX IF NOT EXISTS idx_parts_name_part_pattern ON parts(name_part text_pattern_ops)';
|
||||
END IF;
|
||||
IF to_regclass('public.warehouse_inventory') IS NOT NULL THEN
|
||||
EXECUTE 'CREATE INDEX IF NOT EXISTS idx_wi_part_stock_positive ON warehouse_inventory(part_id) WHERE stock_quantity > 0';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ─── TENANT DB (run on each tenant) ────────────
|
||||
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
-- To apply manually on a tenant with WhatsApp tables:
|
||||
-- psql <tenant_db> -f pos/migrations/v3.5_whatsapp_state_machine.sql
|
||||
-- (Remove the ': SKIP' line above before manual execution.)
|
||||
--
|
||||
-- 2026-10-11: fixed nested dollar-quoting (the outer DO used $$ and the inner
|
||||
-- trigger function also used $$, which broke the parser). Now $do$ / $fn$.
|
||||
-- ============================================================
|
||||
|
||||
DO $$
|
||||
DO $do$
|
||||
BEGIN
|
||||
-- 1. Extender whatsapp_sessions con estado y contexto
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'whatsapp_sessions') THEN
|
||||
@@ -41,12 +44,12 @@ BEGIN
|
||||
CREATE INDEX IF NOT EXISTS idx_wa_cust_link_customer ON wa_customer_links(customer_id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_wa_link_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
RETURNS TRIGGER AS $fn$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
$fn$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_wa_link_updated ON wa_customer_links;
|
||||
CREATE TRIGGER trg_wa_link_updated
|
||||
@@ -115,4 +118,4 @@ BEGIN
|
||||
FROM wa_customer_links wcl
|
||||
WHERE ws.phone = wcl.phone AND ws.customer_id IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
END $do$;
|
||||
|
||||
18
pos/migrations/v4.24_trgm_search_indexes.sql
Normal file
18
pos/migrations/v4.24_trgm_search_indexes.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- v4.24: pg_trgm search indexes for fast fuzzy inventory search.
|
||||
-- The POS list endpoint uses ILIKE '%q%' on part_number/name/barcode/aliases;
|
||||
-- btree indexes cannot serve those. Trigram GIN indexes make them fast even
|
||||
-- with 150k+ SKU catalogs.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_part_trgm
|
||||
ON inventory USING gin (part_number gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_name_trgm
|
||||
ON inventory USING gin (name gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_alias_trgm
|
||||
ON inventory_sku_aliases USING gin (sku gin_trgm_ops);
|
||||
|
||||
ANALYZE inventory;
|
||||
ANALYZE inventory_sku_aliases;
|
||||
28
pos/migrations/v4.25_transfer_documents.sql
Normal file
28
pos/migrations/v4.25_transfer_documents.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- v4.25: transfer documents with in-transit state.
|
||||
-- Transfers become documents: draft -> sent -> received (or cancelled).
|
||||
-- Actual stock movement happens on 'receive' (origin -, destination +),
|
||||
-- so goods in transit are tracked and discrepancies are explicit.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inventory_transfers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
from_branch_id INTEGER NOT NULL REFERENCES branches(id),
|
||||
to_branch_id INTEGER NOT NULL REFERENCES branches(id),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft | sent | received | cancelled
|
||||
notes TEXT,
|
||||
employee_id INTEGER,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
sent_at TIMESTAMPTZ,
|
||||
received_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inventory_transfer_lines (
|
||||
id SERIAL PRIMARY KEY,
|
||||
transfer_id INTEGER NOT NULL REFERENCES inventory_transfers(id) ON DELETE CASCADE,
|
||||
inventory_id INTEGER NOT NULL REFERENCES inventory(id),
|
||||
quantity INTEGER NOT NULL,
|
||||
qty_received INTEGER,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_transfers_status ON inventory_transfers(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_transfer_lines_transfer ON inventory_transfer_lines(transfer_id);
|
||||
4
pos/migrations/v4.26_sales_client_uuid.sql
Normal file
4
pos/migrations/v4.26_sales_client_uuid.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- v4.26: idempotencia para venta offline (client_uuid generado por el POS).
|
||||
ALTER TABLE sales ADD COLUMN IF NOT EXISTS client_uuid VARCHAR(64);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_sales_client_uuid
|
||||
ON sales (client_uuid) WHERE client_uuid IS NOT NULL;
|
||||
113
pos/migrations/v4.27_yonke_m1.sql
Normal file
113
pos/migrations/v4.27_yonke_m1.sql
Normal file
@@ -0,0 +1,113 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- v4.27 — Yonke M1: used-parts (parts-usadas) data model
|
||||
-- Additive + idempotent. Runs against tenant DBs.
|
||||
--
|
||||
-- Enables businesses that sell ONLY used parts (yonkes/deshuesaderos):
|
||||
-- * condition (nuevo/usado/reconstruido) + grade (A/B/C/D)
|
||||
-- * unique 1-of-1 pieces (is_unique_piece) with a donor vehicle
|
||||
-- * multiple photos per item (inventory_images)
|
||||
-- * ownership (propio/consigna) + consignment tables
|
||||
-- * warranty expressed in days (for used parts)
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ─── 1. inventory: condition / donor / ownership / unique-piece columns ─────
|
||||
ALTER TABLE inventory
|
||||
ADD COLUMN IF NOT EXISTS condition VARCHAR(20) DEFAULT 'nuevo',
|
||||
ADD COLUMN IF NOT EXISTS condition_grade VARCHAR(2),
|
||||
ADD COLUMN IF NOT EXISTS is_unique_piece BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS serial_number VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS donor_vin VARCHAR(20),
|
||||
ADD COLUMN IF NOT EXISTS donor_make VARCHAR(60),
|
||||
ADD COLUMN IF NOT EXISTS donor_model VARCHAR(60),
|
||||
ADD COLUMN IF NOT EXISTS donor_year INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS donor_mileage_km INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS ownership VARCHAR(20) DEFAULT 'propio',
|
||||
ADD COLUMN IF NOT EXISTS acquisition_cost NUMERIC(12,2);
|
||||
|
||||
-- CHECK constraints (guarded so re-runs are safe)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'inventory_condition_chk') THEN
|
||||
ALTER TABLE inventory ADD CONSTRAINT inventory_condition_chk
|
||||
CHECK (condition IS NULL OR condition IN ('nuevo','usado','reconstruido'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'inventory_ownership_chk') THEN
|
||||
ALTER TABLE inventory ADD CONSTRAINT inventory_ownership_chk
|
||||
CHECK (ownership IS NULL OR ownership IN ('propio','consigna'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'inventory_grade_chk') THEN
|
||||
ALTER TABLE inventory ADD CONSTRAINT inventory_grade_chk
|
||||
CHECK (condition_grade IS NULL OR condition_grade IN ('A','B','C','D'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_condition
|
||||
ON inventory(condition);
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_donor_vin
|
||||
ON inventory(donor_vin) WHERE donor_vin IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_donor_vehicle
|
||||
ON inventory(donor_make, donor_model, donor_year) WHERE is_unique_piece;
|
||||
|
||||
-- ─── 2. Relax part_number NOT NULL so codeless unique pieces can exist ──────
|
||||
-- A standard UNIQUE index in PostgreSQL already treats NULLs as distinct, so
|
||||
-- the existing full unique index (idx_inventory_part_unique) keeps enforcing
|
||||
-- uniqueness on non-null codes while allowing many NULLs. We deliberately do
|
||||
-- NOT turn it into a partial index: that would break `ON CONFLICT (part_number)`
|
||||
-- used by the importers.
|
||||
ALTER TABLE inventory ALTER COLUMN part_number DROP NOT NULL;
|
||||
|
||||
-- ─── 3. inventory_images: multiple photos per item ─────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS inventory_images (
|
||||
id SERIAL PRIMARY KEY,
|
||||
inventory_id INTEGER NOT NULL REFERENCES inventory(id) ON DELETE CASCADE,
|
||||
url VARCHAR(500) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_images_inv
|
||||
ON inventory_images(inventory_id, sort_order);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_inventory_images_primary
|
||||
ON inventory_images(inventory_id) WHERE is_primary;
|
||||
|
||||
-- Migrate the existing single image_url into the new table (first photo).
|
||||
INSERT INTO inventory_images (inventory_id, url, sort_order, is_primary)
|
||||
SELECT i.id, i.image_url, 0, TRUE
|
||||
FROM inventory i
|
||||
WHERE i.image_url IS NOT NULL AND i.image_url <> ''
|
||||
AND NOT EXISTS (SELECT 1 FROM inventory_images ii WHERE ii.inventory_id = i.id);
|
||||
|
||||
-- ─── 4. warranties: express warranty in days (used parts) ──────────────────
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('public.warranties') IS NOT NULL THEN
|
||||
EXECUTE 'ALTER TABLE warranties ADD COLUMN IF NOT EXISTS warranty_days INTEGER';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ─── 5. Consignments (third-party parts held for sale) ─────────────────────
|
||||
CREATE TABLE IF NOT EXISTS consignments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
consignor_name VARCHAR(200) NOT NULL,
|
||||
consignor_rfc VARCHAR(13),
|
||||
consignor_phone VARCHAR(30),
|
||||
commission_pct NUMERIC(5,2) DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'open', -- open | closed | settled
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS consignment_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
consignment_id INTEGER NOT NULL REFERENCES consignments(id) ON DELETE CASCADE,
|
||||
inventory_id INTEGER REFERENCES inventory(id) ON DELETE SET NULL,
|
||||
agreed_price NUMERIC(12,2),
|
||||
commission_amount NUMERIC(12,2),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'in_stock', -- in_stock | sold | returned | settled
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_consignment_items_cons
|
||||
ON consignment_items(consignment_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_consignment_items_inv
|
||||
ON consignment_items(inventory_id);
|
||||
56
pos/migrations/v4.28_meli_commerce.sql
Normal file
56
pos/migrations/v4.28_meli_commerce.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- v4.28 — MeLi commerce: listing economics, inventory-match metadata
|
||||
-- Additive + idempotent. Runs against tenant DBs.
|
||||
--
|
||||
-- Goal: make MercadoLibre listings sellable end-to-end by persisting, per
|
||||
-- listing, the data that today is only computed transiently:
|
||||
-- * how the ML item was linked to local inventory (backfill audit)
|
||||
-- * the ML commission and the resulting net price / margin
|
||||
-- * the cost at the moment of publishing (so margin stays comparable)
|
||||
-- * the listing type and the description actually published
|
||||
--
|
||||
-- Also normalizes marketplace_questions.status to lowercase: ML returns
|
||||
-- "UNANSWERED"/"ANSWERED" and the auto-answer/SLA queries filter lowercase.
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ─── 1. marketplace_listings: match metadata + economics ────────────────────
|
||||
ALTER TABLE marketplace_listings
|
||||
ADD COLUMN IF NOT EXISTS listing_type_id VARCHAR(30),
|
||||
ADD COLUMN IF NOT EXISTS inventory_match_source VARCHAR(40),
|
||||
ADD COLUMN IF NOT EXISTS inventory_matched_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS meli_fee NUMERIC(12,2),
|
||||
ADD COLUMN IF NOT EXISTS meli_fee_pct NUMERIC(6,2),
|
||||
ADD COLUMN IF NOT EXISTS meli_net_price NUMERIC(12,2),
|
||||
ADD COLUMN IF NOT EXISTS meli_fee_source VARCHAR(20),
|
||||
ADD COLUMN IF NOT EXISTS cost_at_publish NUMERIC(12,2),
|
||||
ADD COLUMN IF NOT EXISTS margin_pct NUMERIC(8,2),
|
||||
ADD COLUMN IF NOT EXISTS meli_description TEXT,
|
||||
ADD COLUMN IF NOT EXISTS economics_updated_at TIMESTAMPTZ;
|
||||
|
||||
-- Fast lookup of the listings that still need an inventory match (backfill).
|
||||
CREATE INDEX IF NOT EXISTS idx_marketplace_listings_unmapped
|
||||
ON marketplace_listings(channel)
|
||||
WHERE inventory_id IS NULL;
|
||||
|
||||
-- ─── 2. marketplace_questions: status normalization ─────────────────────────
|
||||
-- ML's API returns "UNANSWERED" / "ANSWERED"; the service compares lowercase.
|
||||
-- Normalize historical rows so auto-answer and the SLA job see them.
|
||||
UPDATE marketplace_questions
|
||||
SET status = LOWER(status), updated_at = NOW()
|
||||
WHERE status IS NOT NULL AND status <> LOWER(status);
|
||||
|
||||
-- Index for the SLA sweep: oldest unanswered questions first.
|
||||
CREATE INDEX IF NOT EXISTS idx_marketplace_questions_status_date
|
||||
ON marketplace_questions(status, question_date);
|
||||
|
||||
-- ─── 3. Allow several active ML listings per local product ──────────────────
|
||||
-- The original v3.4 unique index assumed one ML listing per inventory item.
|
||||
-- Real MeLi catalogs routinely publish several listings (different fitments /
|
||||
-- titles) that share the same internal part number; the inventory backfill
|
||||
-- must be able to link them all, and order conversion maps each order item by
|
||||
-- external_item_id. Replace the uniqueness with a plain lookup index — the
|
||||
-- publish path now upserts by (inventory_id, channel) explicitly.
|
||||
DROP INDEX IF EXISTS idx_marketplace_listings_unique;
|
||||
CREATE INDEX IF NOT EXISTS idx_marketplace_listings_inv_channel
|
||||
ON marketplace_listings(inventory_id, channel) WHERE is_active = true;
|
||||
|
||||
Reference in New Issue
Block a user