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:
@@ -16,6 +16,21 @@ MIGRATIONS = {
|
||||
'v1.1': 'v1.1_pos_tables.sql',
|
||||
'v1.3': 'v1.3_fleet.sql',
|
||||
'v1.4': 'v1.4_whatsapp.sql',
|
||||
'v1.5': 'v1.5_returns.sql',
|
||||
'v1.7': 'v1.7_plates.sql',
|
||||
'v1.8': 'v1.8_performance_indexes.sql',
|
||||
'v1.9': 'v1.9_redis_cache.sql',
|
||||
'v2.0': 'v2.0_multi_currency.sql',
|
||||
'v2.1': 'v2.1_suppliers.sql',
|
||||
'v2.2': 'v2.2_alerts_warranty.sql',
|
||||
'v2.3': 'v2.3_metabase.sql',
|
||||
'v2.4': 'v2.4_crm_enhanced.sql',
|
||||
'v2.5': 'v2.5_service_orders.sql',
|
||||
'v2.6': 'v2.6_bnpl_erp.sql',
|
||||
'v2.7': 'v2.7_notifications.sql',
|
||||
'v2.8': 'v2.8_savings.sql',
|
||||
'v2.9': 'v2.9_logistics.sql',
|
||||
'v3.0': 'v3.0_public_api.sql',
|
||||
}
|
||||
|
||||
|
||||
|
||||
101
pos/migrations/runner_master.py
Executable file
101
pos/migrations/runner_master.py
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
# /home/Autopartes/pos/migrations/runner_master.py
|
||||
"""Apply schema migrations to the master database (nexus_autoparts)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from tenant_db import get_master_conn
|
||||
|
||||
MIGRATIONS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Master DB migration registry: version -> filename
|
||||
MASTER_MIGRATIONS = {
|
||||
'v1.6': 'v1.6_marketplace.sql',
|
||||
}
|
||||
|
||||
|
||||
def get_current_master_version():
|
||||
"""Get current schema version of master DB."""
|
||||
conn = get_master_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS master_schema_version (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
version VARCHAR(20) NOT NULL DEFAULT 'v0.0',
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
cur.execute("""
|
||||
INSERT INTO master_schema_version (id, version)
|
||||
VALUES (1, 'v0.0')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""")
|
||||
conn.commit()
|
||||
cur.execute("SELECT version FROM master_schema_version WHERE id = 1")
|
||||
version = cur.fetchone()[0]
|
||||
cur.close()
|
||||
conn.close()
|
||||
return version
|
||||
|
||||
|
||||
def apply_master_migration(version):
|
||||
"""Apply a single migration to the master DB."""
|
||||
filename = MASTER_MIGRATIONS[version]
|
||||
filepath = os.path.join(MIGRATIONS_DIR, filename)
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
print(f" ERROR: Migration file not found: {filepath}")
|
||||
return False
|
||||
|
||||
conn = get_master_conn()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
with open(filepath) as f:
|
||||
cur.execute(f.read())
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f" ERROR: {e}")
|
||||
return False
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def run_master_migrations():
|
||||
"""Apply pending migrations to master DB."""
|
||||
current_version = get_current_master_version()
|
||||
sorted_versions = sorted(MASTER_MIGRATIONS.keys())
|
||||
|
||||
print(f"Master DB current version: {current_version}")
|
||||
print(f"Available migrations: {sorted_versions}")
|
||||
|
||||
for version in sorted_versions:
|
||||
if version <= current_version:
|
||||
continue
|
||||
|
||||
print(f" Applying {version}...", end=' ')
|
||||
if apply_master_migration(version):
|
||||
conn = get_master_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO master_schema_version (id, version)
|
||||
VALUES (1, %s)
|
||||
ON CONFLICT (id) DO UPDATE SET version = %s, updated_at = NOW()
|
||||
""", (version, version))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
print("OK")
|
||||
else:
|
||||
print("FAILED — stopping master migrations")
|
||||
break
|
||||
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_master_migrations()
|
||||
100
pos/migrations/v1.8_performance_indexes.sql
Normal file
100
pos/migrations/v1.8_performance_indexes.sql
Normal file
@@ -0,0 +1,100 @@
|
||||
-- v1.8 Performance indexes and FK fixes
|
||||
-- Applied to each tenant database
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- PERFORMANCE INDEXES
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Stock queries (used thousands of times per day)
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_ops_inventory_branch ON inventory_operations(inventory_id, branch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_ops_inventory_type_created ON inventory_operations(inventory_id, operation_type, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_ops_inventory_created_desc ON inventory_operations(inventory_id, created_at DESC);
|
||||
|
||||
-- Cash register lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_cash_movements_register ON cash_movements(register_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sales_register_status ON sales(register_id, status);
|
||||
|
||||
-- Inventory filtering
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_branch_active ON inventory(branch_id, is_active);
|
||||
|
||||
-- Transaction tables
|
||||
CREATE INDEX IF NOT EXISTS idx_sale_items_inventory ON sale_items(inventory_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sale_payments_sale ON sale_payments(sale_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sale_payments_register ON sale_payments(register_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_layaway_items_inventory ON layaway_items(inventory_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_layaway_items_layaway ON layaway_items(layaway_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_return_items_inventory ON return_items(inventory_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_return_items_return ON return_items(return_id);
|
||||
|
||||
-- Employees and permissions
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_email ON employees(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_employees_branch ON employees(branch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_employee_permissions_employee ON employee_permissions(employee_id);
|
||||
|
||||
-- Customers and fleet
|
||||
CREATE INDEX IF NOT EXISTS idx_customers_phone ON customers(phone);
|
||||
CREATE INDEX IF NOT EXISTS idx_fleet_vehicles_branch_vin ON fleet_vehicles(branch_id, vin);
|
||||
CREATE INDEX IF NOT EXISTS idx_fleet_maintenance_schedules_next_due ON fleet_maintenance_schedules(next_due_at);
|
||||
|
||||
-- WhatsApp and quotations
|
||||
CREATE INDEX IF NOT EXISTS idx_whatsapp_messages_status ON whatsapp_messages(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_whatsapp_messages_related ON whatsapp_messages(related_type, related_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_quotations_branch ON quotations(branch_id);
|
||||
|
||||
-- Accounting
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_parent ON accounts(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_date ON journal_entries(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_fiscal_periods_year_month ON fiscal_periods(year, month);
|
||||
|
||||
-- Physical counts
|
||||
CREATE INDEX IF NOT EXISTS idx_physical_counts_branch_status ON physical_counts(branch_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_physical_count_lines_inventory ON physical_count_lines(inventory_id);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- FOREIGN KEY FIXES
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Sale dependencies
|
||||
ALTER TABLE sale_items
|
||||
ADD CONSTRAINT fk_sale_items_sale FOREIGN KEY (sale_id) REFERENCES sales(id) ON DELETE CASCADE;
|
||||
ALTER TABLE sale_payments
|
||||
ADD CONSTRAINT fk_sale_payments_sale FOREIGN KEY (sale_id) REFERENCES sales(id) ON DELETE CASCADE;
|
||||
|
||||
-- Quotation dependencies
|
||||
ALTER TABLE quotation_items
|
||||
ADD CONSTRAINT fk_quotation_items_quotation FOREIGN KEY (quotation_id) REFERENCES quotations(id) ON DELETE CASCADE;
|
||||
|
||||
-- Layaway dependencies
|
||||
ALTER TABLE layaway_items
|
||||
ADD CONSTRAINT fk_layaway_items_layaway FOREIGN KEY (layaway_id) REFERENCES layaways(id) ON DELETE CASCADE;
|
||||
|
||||
-- Return dependencies
|
||||
ALTER TABLE return_items
|
||||
ADD CONSTRAINT fk_return_items_return FOREIGN KEY (return_id) REFERENCES returns(id) ON DELETE CASCADE;
|
||||
ALTER TABLE return_items
|
||||
ADD CONSTRAINT fk_return_items_sale_item FOREIGN KEY (sale_item_id) REFERENCES sale_items(id) ON DELETE SET NULL;
|
||||
|
||||
-- Cash movements
|
||||
ALTER TABLE cash_movements
|
||||
ADD CONSTRAINT fk_cash_movements_register FOREIGN KEY (register_id) REFERENCES cash_registers(id) ON DELETE CASCADE;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- DATA TYPE NORMALIZATION
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE physical_counts ALTER COLUMN created_at TYPE TIMESTAMPTZ;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- CONSTRAINT IMPROVEMENTS
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Ensure unique entry numbers per date (fiscal period approximation)
|
||||
-- Note: journal_entries uses 'date' column, not period_year/month
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_journal_entries_unique_number
|
||||
ON journal_entries(date, entry_number);
|
||||
|
||||
-- Add CHECK constraint for sales.status (idempotent for VARCHAR)
|
||||
-- Note: PostgreSQL does not support adding CHECK constraints via IF NOT EXISTS
|
||||
-- This is left as documentation; apply manually if needed:
|
||||
-- ALTER TABLE sales ADD CONSTRAINT chk_sales_status
|
||||
-- CHECK (status IN ('completed', 'cancelled', 'returned', 'partially_returned'));
|
||||
18
pos/migrations/v1.9_redis_cache.sql
Normal file
18
pos/migrations/v1.9_redis_cache.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- v1.9_redis_cache.sql
|
||||
-- Mejora #9: Caché de Stock con Redis
|
||||
--
|
||||
-- Adds Redis-backed stock caching for sub-millisecond lookups.
|
||||
-- No database schema changes required — caching is handled entirely
|
||||
-- in the application layer (pos/services/redis_stock_cache.py).
|
||||
--
|
||||
-- Invalidation strategy:
|
||||
-- - Every stock mutation (SALE, PURCHASE, RETURN, ADJUST, TRANSFER, INITIAL)
|
||||
-- invalidates the affected Redis keys immediately in Python code.
|
||||
-- - Cache TTL is 5 minutes (configurable via REDIS_STOCK_TTL env var).
|
||||
-- - On Redis miss, stock is computed from PostgreSQL SUM query and cached.
|
||||
--
|
||||
-- Prerequisites:
|
||||
-- - Redis server installed and running (default: localhost:6379)
|
||||
-- - redis-py library installed
|
||||
--
|
||||
SELECT 'v1.9 redis cache migration applied' as status;
|
||||
36
pos/migrations/v2.0_multi_currency.sql
Normal file
36
pos/migrations/v2.0_multi_currency.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- v2.0_multi_currency.sql
|
||||
-- Mejora #8: Soporte Multi-moneda
|
||||
--
|
||||
-- Adds currency and exchange_rate columns to sales, sale_items,
|
||||
-- quotations, quotation_items, and sale_payments.
|
||||
--
|
||||
-- Business rule: inventory prices are ALWAYS in MXN (base currency).
|
||||
-- Sales can be recorded in USD (or other currencies) with conversion
|
||||
-- at checkout time. Accounting and CFDI always use MXN.
|
||||
|
||||
-- sales
|
||||
ALTER TABLE sales
|
||||
ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'MXN',
|
||||
ADD COLUMN IF NOT EXISTS exchange_rate NUMERIC(12,6) DEFAULT 1.0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sales_currency ON sales(currency);
|
||||
|
||||
-- sale_items
|
||||
ALTER TABLE sale_items
|
||||
ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'MXN',
|
||||
ADD COLUMN IF NOT EXISTS exchange_rate NUMERIC(12,6) DEFAULT 1.0;
|
||||
|
||||
-- quotations
|
||||
ALTER TABLE quotations
|
||||
ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'MXN',
|
||||
ADD COLUMN IF NOT EXISTS exchange_rate NUMERIC(12,6) DEFAULT 1.0;
|
||||
|
||||
-- quotation_items
|
||||
ALTER TABLE quotation_items
|
||||
ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'MXN',
|
||||
ADD COLUMN IF NOT EXISTS exchange_rate NUMERIC(12,6) DEFAULT 1.0;
|
||||
|
||||
-- sale_payments
|
||||
ALTER TABLE sale_payments
|
||||
ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'MXN',
|
||||
ADD COLUMN IF NOT EXISTS exchange_rate NUMERIC(12,6) DEFAULT 1.0;
|
||||
81
pos/migrations/v2.1_suppliers.sql
Normal file
81
pos/migrations/v2.1_suppliers.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- v2.1_suppliers.sql
|
||||
-- Mejora #3: Proveedores y Órdenes de Compra
|
||||
--
|
||||
-- Adds supplier management and purchase order workflow to tenant databases.
|
||||
--
|
||||
-- Workflow:
|
||||
-- 1. Create supplier (suppliers table)
|
||||
-- 2. Create PO with items (purchase_orders + purchase_order_items)
|
||||
-- 3. Send PO to supplier (status = 'sent')
|
||||
-- 4. Receive partial or full delivery (status = 'partial' | 'received')
|
||||
-- → On receive: update stock via inventory_engine.record_purchase()
|
||||
-- → On receive: create accounting entry via record_purchase_entry()
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- SUPPLIERS
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
CREATE TABLE IF NOT EXISTS suppliers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
contact_name VARCHAR(200),
|
||||
phone VARCHAR(50),
|
||||
email VARCHAR(200),
|
||||
rfc VARCHAR(13),
|
||||
address TEXT,
|
||||
payment_terms VARCHAR(100), -- e.g., "30 dias", "contado"
|
||||
notes TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_suppliers_active ON suppliers(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_suppliers_name ON suppliers(name);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- PURCHASE ORDERS
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
CREATE TABLE IF NOT EXISTS purchase_orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
supplier_id INTEGER REFERENCES suppliers(id) ON DELETE SET NULL,
|
||||
branch_id INTEGER REFERENCES branches(id),
|
||||
employee_id INTEGER REFERENCES employees(id),
|
||||
status VARCHAR(20) DEFAULT 'draft' NOT NULL, -- draft, sent, partial, received, cancelled
|
||||
subtotal NUMERIC(12,2) DEFAULT 0 NOT NULL,
|
||||
tax_total NUMERIC(12,2) DEFAULT 0 NOT NULL,
|
||||
total NUMERIC(12,2) DEFAULT 0 NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'MXN',
|
||||
exchange_rate NUMERIC(12,6) DEFAULT 1.0,
|
||||
notes TEXT,
|
||||
supplier_invoice VARCHAR(100),
|
||||
expected_date DATE,
|
||||
sent_at TIMESTAMPTZ,
|
||||
received_at TIMESTAMPTZ,
|
||||
cancelled_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_po_supplier ON purchase_orders(supplier_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_po_status ON purchase_orders(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_po_branch ON purchase_orders(branch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_po_created ON purchase_orders(created_at DESC);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- PURCHASE ORDER ITEMS
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
CREATE TABLE IF NOT EXISTS purchase_order_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
po_id INTEGER NOT NULL REFERENCES purchase_orders(id) ON DELETE CASCADE,
|
||||
inventory_id INTEGER REFERENCES inventory(id) ON DELETE SET NULL,
|
||||
part_number VARCHAR(100),
|
||||
name VARCHAR(300),
|
||||
quantity INTEGER NOT NULL DEFAULT 1,
|
||||
received_qty INTEGER DEFAULT 0,
|
||||
unit_price NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
subtotal NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_poi_po ON purchase_order_items(po_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_poi_inventory ON purchase_order_items(inventory_id);
|
||||
82
pos/migrations/v2.2_alerts_warranty.sql
Normal file
82
pos/migrations/v2.2_alerts_warranty.sql
Normal file
@@ -0,0 +1,82 @@
|
||||
-- v2.2_alerts_warranty.sql
|
||||
-- Mejora #7: Alertas de Reorden mejoradas
|
||||
-- Mejora #10: Garantías / RMA
|
||||
-- Mejora #1: Multi-sucursal sync helpers
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- ALERTAS DE REORDER
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Add reorder columns to inventory
|
||||
ALTER TABLE inventory
|
||||
ADD COLUMN IF NOT EXISTS reorder_point INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS reorder_qty INTEGER;
|
||||
|
||||
-- Table to track generated reorder alerts (prevents duplicate notifications)
|
||||
CREATE TABLE IF NOT EXISTS reorder_alerts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
inventory_id INTEGER NOT NULL REFERENCES inventory(id) ON DELETE CASCADE,
|
||||
branch_id INTEGER REFERENCES branches(id),
|
||||
alert_type VARCHAR(20) NOT NULL, -- 'zero', 'low', 'over'
|
||||
stock_at_alert INTEGER NOT NULL,
|
||||
threshold INTEGER,
|
||||
status VARCHAR(20) DEFAULT 'open', -- open, acknowledged, resolved
|
||||
po_id INTEGER REFERENCES purchase_orders(id),
|
||||
employee_id INTEGER REFERENCES employees(id),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
resolved_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reorder_alerts_inventory ON reorder_alerts(inventory_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_reorder_alerts_status ON reorder_alerts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_reorder_alerts_created ON reorder_alerts(created_at DESC);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- GARANTÍAS / RMA
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Warranty registry attached to sale_items
|
||||
CREATE TABLE IF NOT EXISTS warranties (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sale_id INTEGER REFERENCES sales(id) ON DELETE SET NULL,
|
||||
sale_item_id INTEGER REFERENCES sale_items(id) ON DELETE SET NULL,
|
||||
inventory_id INTEGER REFERENCES inventory(id) ON DELETE SET NULL,
|
||||
customer_id INTEGER REFERENCES customers(id),
|
||||
supplier_id INTEGER REFERENCES suppliers(id),
|
||||
part_number VARCHAR(100),
|
||||
name VARCHAR(300),
|
||||
warranty_months INTEGER DEFAULT 0,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'active', -- active, claimed, expired, void
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_warranties_sale ON warranties(sale_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_warranties_customer ON warranties(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_warranties_status ON warranties(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_warranties_end_date ON warranties(end_date);
|
||||
|
||||
-- Warranty claims (RMA process)
|
||||
CREATE TABLE IF NOT EXISTS warranty_claims (
|
||||
id SERIAL PRIMARY KEY,
|
||||
warranty_id INTEGER NOT NULL REFERENCES warranties(id) ON DELETE CASCADE,
|
||||
claim_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
reason TEXT NOT NULL,
|
||||
diagnosis TEXT,
|
||||
resolution VARCHAR(20), -- approved, rejected, repaired, replaced, refunded
|
||||
replacement_inventory_id INTEGER REFERENCES inventory(id),
|
||||
refund_amount NUMERIC(12,2),
|
||||
labor_cost NUMERIC(12,2),
|
||||
status VARCHAR(20) DEFAULT 'open', -- open, in_review, resolved, closed
|
||||
employee_id INTEGER REFERENCES employees(id),
|
||||
supplier_rma_number VARCHAR(100),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
resolved_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_warranty_claims_warranty ON warranty_claims(warranty_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_warranty_claims_status ON warranty_claims(status);
|
||||
18
pos/migrations/v2.3_metabase.sql
Normal file
18
pos/migrations/v2.3_metabase.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- v2.3_metabase.sql
|
||||
-- Mejora #5: Metabase KPIs
|
||||
--
|
||||
-- No schema changes required in tenant DB.
|
||||
-- Metabase runs as a separate Docker container and connects
|
||||
-- to PostgreSQL as a read-only BI user.
|
||||
--
|
||||
-- Prerequisites:
|
||||
-- - Docker and docker-compose installed
|
||||
-- - docker-compose.metabase.yml deployed
|
||||
-- - scripts/setup_metabase.py executed
|
||||
--
|
||||
-- Post-deployment:
|
||||
-- 1. Start Metabase: docker compose -f docker-compose.metabase.yml up -d
|
||||
-- 2. Run setup: python3 scripts/setup_metabase.py
|
||||
-- 3. Access dashboard at http://<host>:3000
|
||||
--
|
||||
SELECT 'v2.3 metabase KPIs migration applied' as status;
|
||||
81
pos/migrations/v2.4_crm_enhanced.sql
Normal file
81
pos/migrations/v2.4_crm_enhanced.sql
Normal file
@@ -0,0 +1,81 @@
|
||||
-- v2.4 CRM Enhanced: activities, tags, loyalty
|
||||
|
||||
-- Customer activity timeline (notes, calls, visits, emails, whatsapp)
|
||||
CREATE TABLE IF NOT EXISTS customer_activities (
|
||||
id SERIAL PRIMARY KEY,
|
||||
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
activity_type VARCHAR(50) NOT NULL, -- 'note', 'call', 'visit', 'email', 'whatsapp', 'sale', 'payment', 'claim'
|
||||
title VARCHAR(200),
|
||||
description TEXT,
|
||||
metadata JSONB, -- e.g. {"call_duration": 120, "email_subject": "..."}
|
||||
employee_id INTEGER REFERENCES employees(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_cust_act_customer ON customer_activities(customer_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_cust_act_type ON customer_activities(activity_type);
|
||||
|
||||
-- Customer tags for segmentation
|
||||
CREATE TABLE IF NOT EXISTS customer_tags (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
color VARCHAR(7) DEFAULT '#6B7280', -- hex color
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, name)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_cust_tags_tenant ON customer_tags(tenant_id);
|
||||
|
||||
-- Many-to-many: customers <-> tags
|
||||
CREATE TABLE IF NOT EXISTS customer_tag_assignments (
|
||||
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES customer_tags(id) ON DELETE CASCADE,
|
||||
assigned_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
assigned_by INTEGER REFERENCES employees(id),
|
||||
PRIMARY KEY (customer_id, tag_id)
|
||||
);
|
||||
|
||||
-- Loyalty / rewards program
|
||||
CREATE TABLE IF NOT EXISTS loyalty_points (
|
||||
id SERIAL PRIMARY KEY,
|
||||
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
points INTEGER NOT NULL DEFAULT 0,
|
||||
points_type VARCHAR(20) NOT NULL DEFAULT 'earned', -- 'earned', 'redeemed', 'expired', 'bonus'
|
||||
source_type VARCHAR(50), -- 'sale', 'referral', 'promotion', 'manual', 'redemption'
|
||||
source_id INTEGER, -- sale_id or other reference
|
||||
description TEXT,
|
||||
expires_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_loyalty_customer ON loyalty_points(customer_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_loyalty_expires ON loyalty_points(expires_at) WHERE expires_at IS NOT NULL;
|
||||
|
||||
-- Loyalty redemptions (rewards catalog)
|
||||
CREATE TABLE IF NOT EXISTS loyalty_rewards (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
points_cost INTEGER NOT NULL,
|
||||
reward_type VARCHAR(50) DEFAULT 'discount', -- 'discount', 'free_product', 'service'
|
||||
reward_value NUMERIC(12,2), -- discount amount or product ID
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Loyalty redemption history
|
||||
CREATE TABLE IF NOT EXISTS loyalty_redemptions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
reward_id INTEGER REFERENCES loyalty_rewards(id),
|
||||
points_used INTEGER NOT NULL,
|
||||
reward_value NUMERIC(12,2),
|
||||
description TEXT,
|
||||
employee_id INTEGER REFERENCES employees(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_loyalty_redem_customer ON loyalty_redemptions(customer_id);
|
||||
|
||||
-- Add loyalty balance to customers (denormalized for quick lookup)
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS loyalty_points_balance INTEGER DEFAULT 0;
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS loyalty_tier VARCHAR(50) DEFAULT 'bronze';
|
||||
90
pos/migrations/v2.5_service_orders.sql
Normal file
90
pos/migrations/v2.5_service_orders.sql
Normal file
@@ -0,0 +1,90 @@
|
||||
-- v2.5 Service Orders (Kanban) for workshop management
|
||||
|
||||
CREATE TABLE IF NOT EXISTS service_orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
branch_id INTEGER REFERENCES branches(id),
|
||||
customer_id INTEGER REFERENCES customers(id),
|
||||
vehicle_id INTEGER REFERENCES fleet_vehicles(id), -- optional link to fleet
|
||||
order_number VARCHAR(50) UNIQUE, -- human-readable SO-2026-0001
|
||||
status VARCHAR(30) DEFAULT 'received', -- received, diagnosis, waiting_parts, repair, quality_check, ready, delivered, cancelled
|
||||
priority VARCHAR(20) DEFAULT 'normal', -- low, normal, high, urgent
|
||||
reception_notes TEXT,
|
||||
diagnosis_notes TEXT,
|
||||
repair_notes TEXT,
|
||||
delivery_notes TEXT,
|
||||
estimated_cost NUMERIC(12,2),
|
||||
final_cost NUMERIC(12,2),
|
||||
estimated_completion TIMESTAMPTZ,
|
||||
actual_completion TIMESTAMPTZ,
|
||||
delivered_at TIMESTAMPTZ,
|
||||
delivered_by INTEGER REFERENCES employees(id),
|
||||
employee_id INTEGER REFERENCES employees(id), -- assigned mechanic/technician
|
||||
mileage_in INTEGER,
|
||||
mileage_out INTEGER,
|
||||
fuel_level VARCHAR(20), -- empty, quarter, half, three_quarters, full
|
||||
created_by INTEGER REFERENCES employees(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_so_status ON service_orders(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_so_customer ON service_orders(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_so_branch ON service_orders(branch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_so_created ON service_orders(created_at DESC);
|
||||
|
||||
-- Service order items (parts needed/used)
|
||||
CREATE TABLE IF NOT EXISTS service_order_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
service_order_id INTEGER NOT NULL REFERENCES service_orders(id) ON DELETE CASCADE,
|
||||
inventory_id INTEGER REFERENCES inventory(id),
|
||||
part_number VARCHAR(50),
|
||||
name VARCHAR(300),
|
||||
quantity NUMERIC(10,2) DEFAULT 1,
|
||||
unit_cost NUMERIC(12,2),
|
||||
unit_price NUMERIC(12,2),
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending, ordered, received, installed, cancelled
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_soi_order ON service_order_items(service_order_id);
|
||||
|
||||
-- Service order labor / work items
|
||||
CREATE TABLE IF NOT EXISTS service_order_labor (
|
||||
id SERIAL PRIMARY KEY,
|
||||
service_order_id INTEGER NOT NULL REFERENCES service_orders(id) ON DELETE CASCADE,
|
||||
description TEXT NOT NULL,
|
||||
hours NUMERIC(6,2) DEFAULT 0,
|
||||
hourly_rate NUMERIC(12,2) DEFAULT 0,
|
||||
total_cost NUMERIC(12,2) DEFAULT 0,
|
||||
employee_id INTEGER REFERENCES employees(id), -- mechanic who did the work
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending, in_progress, completed
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sol_order ON service_order_labor(service_order_id);
|
||||
|
||||
-- Status history for audit trail
|
||||
CREATE TABLE IF NOT EXISTS service_order_status_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
service_order_id INTEGER NOT NULL REFERENCES service_orders(id) ON DELETE CASCADE,
|
||||
old_status VARCHAR(30),
|
||||
new_status VARCHAR(30) NOT NULL,
|
||||
changed_by INTEGER REFERENCES employees(id),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sosh_order ON service_order_status_history(service_order_id);
|
||||
|
||||
-- Trigger to auto-update updated_at
|
||||
CREATE OR REPLACE FUNCTION update_so_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_service_orders_updated_at ON service_orders;
|
||||
CREATE TRIGGER trg_service_orders_updated_at
|
||||
BEFORE UPDATE ON service_orders
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_so_updated_at();
|
||||
80
pos/migrations/v2.6_bnpl_erp.sql
Normal file
80
pos/migrations/v2.6_bnpl_erp.sql
Normal file
@@ -0,0 +1,80 @@
|
||||
-- v2.6 BNPL (APLAZO) and ERP Sync stubs
|
||||
|
||||
-- BNPL / External credit provider transactions
|
||||
CREATE TABLE IF NOT EXISTS bnpl_transactions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
customer_id INTEGER REFERENCES customers(id),
|
||||
sale_id INTEGER REFERENCES sales(id),
|
||||
provider VARCHAR(50) NOT NULL DEFAULT 'aplazo', -- 'aplazo', 'kueski', 'clip'
|
||||
provider_transaction_id VARCHAR(200),
|
||||
amount NUMERIC(12,2) NOT NULL,
|
||||
status VARCHAR(30) DEFAULT 'pending', -- pending, approved, rejected, funded, cancelled, refunded
|
||||
installment_count INTEGER DEFAULT 1,
|
||||
installment_amount NUMERIC(12,2),
|
||||
customer_fee NUMERIC(12,2) DEFAULT 0,
|
||||
merchant_fee NUMERIC(12,2) DEFAULT 0,
|
||||
provider_response JSONB,
|
||||
webhook_payload JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_bnpl_sale ON bnpl_transactions(sale_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bnpl_customer ON bnpl_transactions(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bnpl_status ON bnpl_transactions(status);
|
||||
|
||||
-- ERP Sync configurations per tenant
|
||||
CREATE TABLE IF NOT EXISTS erp_sync_configs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
erp_type VARCHAR(50) NOT NULL, -- 'aspel_sae', 'contpaqi', 'sap_b1', 'odoo'
|
||||
is_active BOOLEAN DEFAULT FALSE,
|
||||
api_endpoint VARCHAR(500),
|
||||
api_username VARCHAR(200),
|
||||
api_password_encrypted TEXT,
|
||||
database_name VARCHAR(200),
|
||||
company_code VARCHAR(50),
|
||||
sync_direction VARCHAR(20) DEFAULT 'bidirectional', -- 'to_erp', 'from_erp', 'bidirectional'
|
||||
sync_inventory BOOLEAN DEFAULT FALSE,
|
||||
sync_sales BOOLEAN DEFAULT FALSE,
|
||||
sync_customers BOOLEAN DEFAULT FALSE,
|
||||
sync_frequency_minutes INTEGER DEFAULT 60,
|
||||
last_sync_at TIMESTAMPTZ,
|
||||
last_sync_error TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_erp_config_tenant ON erp_sync_configs(tenant_id, erp_type);
|
||||
|
||||
-- ERP Sync logs (each sync run)
|
||||
CREATE TABLE IF NOT EXISTS erp_sync_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
config_id INTEGER REFERENCES erp_sync_configs(id),
|
||||
sync_type VARCHAR(50) NOT NULL, -- 'inventory', 'sales', 'customers', 'full'
|
||||
direction VARCHAR(20) NOT NULL, -- 'to_erp', 'from_erp'
|
||||
status VARCHAR(20) DEFAULT 'running', -- running, success, partial, failed
|
||||
records_processed INTEGER DEFAULT 0,
|
||||
records_failed INTEGER DEFAULT 0,
|
||||
error_message TEXT,
|
||||
details JSONB,
|
||||
started_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_erp_logs_config ON erp_sync_logs(config_id, started_at DESC);
|
||||
|
||||
-- ERP Sync queue (pending items to sync)
|
||||
CREATE TABLE IF NOT EXISTS erp_sync_queue (
|
||||
id SERIAL PRIMARY KEY,
|
||||
config_id INTEGER NOT NULL REFERENCES erp_sync_configs(id),
|
||||
entity_type VARCHAR(50) NOT NULL, -- 'inventory', 'sale', 'customer'
|
||||
entity_id INTEGER NOT NULL,
|
||||
action VARCHAR(20) NOT NULL, -- 'create', 'update', 'delete'
|
||||
priority INTEGER DEFAULT 5, -- 1=urgent, 10=low
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending, processing, completed, failed
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
last_error TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
processed_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_erp_queue_pending ON erp_sync_queue(config_id, status, priority, created_at)
|
||||
WHERE status IN ('pending', 'failed');
|
||||
67
pos/migrations/v2.7_notifications.sql
Normal file
67
pos/migrations/v2.7_notifications.sql
Normal file
@@ -0,0 +1,67 @@
|
||||
-- v2.7 Automatic Notifications Engine
|
||||
|
||||
-- Notification templates per tenant
|
||||
CREATE TABLE IF NOT EXISTS notification_templates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
event_type VARCHAR(50) NOT NULL, -- 'low_stock', 'order_ready', 'maintenance_due', 'new_sale', 'po_received', 'reorder_alert', 'warranty_expiring'
|
||||
channel VARCHAR(20) NOT NULL DEFAULT 'push', -- 'push', 'email', 'whatsapp', 'sms', 'in_app'
|
||||
name VARCHAR(200) NOT NULL,
|
||||
subject_template VARCHAR(500),
|
||||
body_template TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(tenant_id, event_type, channel)
|
||||
);
|
||||
|
||||
-- Notification delivery log
|
||||
CREATE TABLE IF NOT EXISTS notification_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
recipient_type VARCHAR(20) NOT NULL DEFAULT 'employee', -- 'employee', 'customer', 'owner', 'role'
|
||||
recipient_id INTEGER,
|
||||
event_type VARCHAR(50) NOT NULL,
|
||||
channel VARCHAR(20) NOT NULL,
|
||||
subject TEXT,
|
||||
body TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending, sent, delivered, failed, read
|
||||
error_message TEXT,
|
||||
metadata JSONB, -- {sale_id, po_id, inventory_id, etc.}
|
||||
sent_at TIMESTAMPTZ,
|
||||
read_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_notif_logs_recipient ON notification_logs(recipient_type, recipient_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_notif_logs_status ON notification_logs(status) WHERE status IN ('pending', 'failed');
|
||||
CREATE INDEX IF NOT EXISTS idx_notif_logs_event ON notification_logs(event_type, created_at DESC);
|
||||
|
||||
-- Notification preferences per employee
|
||||
CREATE TABLE IF NOT EXISTS notification_preferences (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id INTEGER NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
||||
event_type VARCHAR(50) NOT NULL,
|
||||
channel VARCHAR(20) NOT NULL,
|
||||
is_enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(employee_id, event_type, channel)
|
||||
);
|
||||
|
||||
-- Push subscriptions table (if not already created by push_service)
|
||||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
employee_id INTEGER NOT NULL UNIQUE,
|
||||
subscription_data TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Default templates
|
||||
INSERT INTO notification_templates (tenant_id, event_type, channel, name, subject_template, body_template) VALUES
|
||||
(1, 'low_stock', 'push', 'Stock Bajo', 'Stock bajo: {part_name}', 'El inventario de {part_name} ({part_number}) tiene solo {stock} unidades. Punto de reorden: {reorder_point}.'),
|
||||
(1, 'order_ready', 'push', 'Orden Lista', 'Orden #{order_number} lista', 'La orden de servicio #{order_number} está lista para entrega. Cliente: {customer_name}.'),
|
||||
(1, 'maintenance_due', 'push', 'Mantenimiento Vencido', 'Mantenimiento vencido', 'El vehículo {vehicle_plate} tiene mantenimiento de {maintenance_type} vencido. Kilometraje: {current_mileage}.'),
|
||||
(1, 'new_sale', 'push', 'Nueva Venta', 'Venta registrada', 'Venta #{sale_id} por ${total} registrada. Método: {payment_method}.'),
|
||||
(1, 'po_received', 'push', 'OC Recibida', 'Orden de compra recibida', 'La orden de compra #{po_id} fue recibida. Total: ${total}.'),
|
||||
(1, 'reorder_alert', 'push', 'Alerta de Reorden', 'Reorden requerida', '{part_name} ({part_number}) está por debajo del punto de reorden. Stock: {stock}, Reorden: {reorder_point}.'),
|
||||
(1, 'warranty_expiring', 'push', 'Garantía por vencer', 'Garantía por vencer', 'La garantía del item {part_name} vence el {expiry_date}. Cliente: {customer_name}.')
|
||||
ON CONFLICT DO NOTHING;
|
||||
31
pos/migrations/v2.8_savings.sql
Normal file
31
pos/migrations/v2.8_savings.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
-- v2.8 Savings Reports
|
||||
|
||||
-- Add retail_price (MSRP) to inventory for savings calculation
|
||||
ALTER TABLE inventory ADD COLUMN IF NOT EXISTS retail_price NUMERIC(12,2);
|
||||
ALTER TABLE inventory ADD COLUMN IF NOT EXISTS reference_price NUMERIC(12,2); -- competitor price or market price
|
||||
|
||||
-- Track savings per sale item
|
||||
ALTER TABLE sale_items ADD COLUMN IF NOT EXISTS retail_price NUMERIC(12,2);
|
||||
ALTER TABLE sale_items ADD COLUMN IF NOT EXISTS savings_amount NUMERIC(12,2) DEFAULT 0;
|
||||
|
||||
-- Savings summary per sale
|
||||
ALTER TABLE sales ADD COLUMN IF NOT EXISTS total_savings NUMERIC(12,2) DEFAULT 0;
|
||||
|
||||
-- Customer savings history (denormalized for quick lookup)
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS total_savings NUMERIC(12,2) DEFAULT 0;
|
||||
|
||||
-- Savings report view
|
||||
CREATE OR REPLACE VIEW v_customer_savings AS
|
||||
SELECT
|
||||
s.customer_id,
|
||||
c.name as customer_name,
|
||||
date_trunc('month', s.created_at) as month,
|
||||
COUNT(*) as orders_count,
|
||||
SUM(s.total) as total_spent,
|
||||
SUM(s.total_savings) as total_saved,
|
||||
AVG(s.total_savings / NULLIF(s.total, 0) * 100) as avg_savings_pct
|
||||
FROM sales s
|
||||
JOIN customers c ON s.customer_id = c.id
|
||||
WHERE s.status = 'completed' AND s.total_savings > 0
|
||||
GROUP BY s.customer_id, c.name, date_trunc('month', s.created_at)
|
||||
ORDER BY month DESC, total_saved DESC;
|
||||
82
pos/migrations/v2.9_logistics.sql
Normal file
82
pos/migrations/v2.9_logistics.sql
Normal file
@@ -0,0 +1,82 @@
|
||||
-- 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;
|
||||
47
pos/migrations/v3.0_public_api.sql
Normal file
47
pos/migrations/v3.0_public_api.sql
Normal file
@@ -0,0 +1,47 @@
|
||||
-- v3.0 Public API: API keys and rate limiting
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id INTEGER NOT NULL,
|
||||
name VARCHAR(200) NOT NULL, -- e.g. "Integration Acme Corp"
|
||||
key_hash VARCHAR(64) NOT NULL, -- SHA-256 hash of the API key
|
||||
key_prefix VARCHAR(8) NOT NULL, -- first 8 chars for display
|
||||
scopes JSONB DEFAULT '["read"]'::jsonb, -- ["read", "write", "admin"]
|
||||
rate_limit_rpm INTEGER DEFAULT 60, -- requests per minute
|
||||
rate_limit_rpd INTEGER DEFAULT 10000, -- requests per day
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
last_used_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ,
|
||||
created_by INTEGER REFERENCES employees(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant ON api_keys(tenant_id, is_active);
|
||||
|
||||
-- API request log for analytics and abuse detection
|
||||
CREATE TABLE IF NOT EXISTS api_request_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
api_key_id INTEGER REFERENCES api_keys(id),
|
||||
tenant_id INTEGER,
|
||||
method VARCHAR(10) NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
status_code INTEGER,
|
||||
response_time_ms INTEGER,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_logs_key ON api_request_logs(api_key_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_logs_tenant ON api_request_logs(tenant_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_logs_path ON api_request_logs(path, created_at DESC);
|
||||
|
||||
-- Rate limit counters (in-memory/redis preferred, but table as fallback)
|
||||
CREATE TABLE IF NOT EXISTS api_rate_limit_counters (
|
||||
id SERIAL PRIMARY KEY,
|
||||
api_key_id INTEGER NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
||||
window_start TIMESTAMPTZ NOT NULL,
|
||||
window_type VARCHAR(10) NOT NULL, -- 'minute', 'day'
|
||||
request_count INTEGER DEFAULT 0,
|
||||
UNIQUE(api_key_id, window_start, window_type)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_rate_limit_window ON api_rate_limit_counters(api_key_id, window_type, window_start);
|
||||
Reference in New Issue
Block a user