- 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
181 lines
6.0 KiB
Python
181 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Idempotent master-database schema for the B2B yonke marketplace.
|
|
|
|
Creates/ensures, in the master DB (nexus_autoparts):
|
|
- b2b_listings (used-part wholesale catalog published by sellers)
|
|
- marketplace_orders (cross-tenant order header; from v1.6_marketplace.sql)
|
|
- marketplace_order_items (order lines; + listing_id link column)
|
|
- tenants.is_seller (seller opt-in flag)
|
|
and activates is_seller=true for the demo sellers (47 Yonke Demo, 45 Capi Autopartes).
|
|
|
|
Safe to run repeatedly: every statement is CREATE ... IF NOT EXISTS / ADD COLUMN
|
|
IF NOT EXISTS / UPDATE (idempotent). It never drops or rewrites data.
|
|
|
|
Run:
|
|
cd /home/Autopartes && python3 scripts/b2b_marketplace_schema.py
|
|
# or point at another master DB:
|
|
MASTER_DB_URL=postgresql://user@host/nexus_autoparts python3 scripts/b2b_marketplace_schema.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
import psycopg2
|
|
|
|
MASTER_DB_URL = (
|
|
os.environ.get('MASTER_DB_URL')
|
|
or 'postgresql://postgres@localhost/nexus_autoparts'
|
|
)
|
|
|
|
# Demo sellers that must appear in the marketplace so it is never empty.
|
|
DEMO_SELLER_TENANT_IDS = (47, 45)
|
|
|
|
B2B_LISTINGS_DDL = """
|
|
CREATE TABLE IF NOT EXISTS b2b_listings (
|
|
id SERIAL PRIMARY KEY,
|
|
seller_tenant_id INTEGER NOT NULL,
|
|
seller_inventory_id INTEGER NOT NULL,
|
|
part_number VARCHAR(100),
|
|
name VARCHAR(300),
|
|
condition VARCHAR(20) DEFAULT 'usado',
|
|
condition_grade VARCHAR(2),
|
|
price_wholesale NUMERIC(12,2) NOT NULL DEFAULT 0,
|
|
donor_make VARCHAR(60),
|
|
donor_model VARCHAR(60),
|
|
donor_year INTEGER,
|
|
image_url VARCHAR(500),
|
|
status VARCHAR(20) NOT NULL DEFAULT 'available',
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE (seller_tenant_id, seller_inventory_id)
|
|
)
|
|
"""
|
|
|
|
# Mirror of pos/migrations/v1.6_marketplace.sql (master DB), kept here so this
|
|
# script can repair a master DB where the migration was skipped.
|
|
MARKETPLACE_ORDERS_DDL = """
|
|
CREATE TABLE IF NOT EXISTS marketplace_orders (
|
|
id SERIAL PRIMARY KEY,
|
|
buyer_tenant_id INTEGER NOT NULL,
|
|
seller_tenant_id INTEGER NOT NULL,
|
|
buyer_name VARCHAR(200),
|
|
seller_name VARCHAR(200),
|
|
total NUMERIC(12,2),
|
|
status VARCHAR(20) DEFAULT 'pending',
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
|
|
MARKETPLACE_ORDER_ITEMS_DDL = """
|
|
CREATE TABLE IF NOT EXISTS marketplace_order_items (
|
|
id SERIAL PRIMARY KEY,
|
|
order_id INTEGER REFERENCES marketplace_orders(id),
|
|
part_number VARCHAR(100),
|
|
part_name VARCHAR(300),
|
|
quantity INTEGER,
|
|
unit_price NUMERIC(12,2),
|
|
subtotal NUMERIC(12,2)
|
|
)
|
|
"""
|
|
|
|
STATEMENTS = [
|
|
('b2b_listings', B2B_LISTINGS_DDL),
|
|
('marketplace_orders', MARKETPLACE_ORDERS_DDL),
|
|
('marketplace_order_items', MARKETPLACE_ORDER_ITEMS_DDL),
|
|
|
|
# Extensions to tables that may already exist.
|
|
('marketplace_order_items.listing_id',
|
|
"ALTER TABLE marketplace_order_items ADD COLUMN IF NOT EXISTS listing_id INTEGER"),
|
|
('tenants.is_seller',
|
|
"ALTER TABLE tenants ADD COLUMN IF NOT EXISTS is_seller BOOLEAN DEFAULT FALSE"),
|
|
|
|
# Indexes.
|
|
('idx_b2b_listings_seller',
|
|
"CREATE INDEX IF NOT EXISTS idx_b2b_listings_seller ON b2b_listings(seller_tenant_id)"),
|
|
('idx_b2b_listings_status',
|
|
"CREATE INDEX IF NOT EXISTS idx_b2b_listings_status ON b2b_listings(status)"),
|
|
('idx_b2b_listings_catalog',
|
|
"CREATE INDEX IF NOT EXISTS idx_b2b_listings_catalog ON b2b_listings(status, condition_grade)"),
|
|
('idx_mp_orders_buyer',
|
|
"CREATE INDEX IF NOT EXISTS idx_mp_orders_buyer ON marketplace_orders(buyer_tenant_id)"),
|
|
('idx_mp_orders_seller',
|
|
"CREATE INDEX IF NOT EXISTS idx_mp_orders_seller ON marketplace_orders(seller_tenant_id)"),
|
|
('idx_mp_orders_status',
|
|
"CREATE INDEX IF NOT EXISTS idx_mp_orders_status ON marketplace_orders(status)"),
|
|
('idx_mp_order_items_order',
|
|
"CREATE INDEX IF NOT EXISTS idx_mp_order_items_order ON marketplace_order_items(order_id)"),
|
|
('idx_mp_order_items_listing',
|
|
"CREATE INDEX IF NOT EXISTS idx_mp_order_items_listing ON marketplace_order_items(listing_id)"),
|
|
]
|
|
|
|
|
|
def log(msg):
|
|
print(f" -> {msg}")
|
|
|
|
|
|
def ensure_schema(conn):
|
|
cur = conn.cursor()
|
|
for label, sql in STATEMENTS:
|
|
cur.execute(sql)
|
|
log(f"ok: {label}")
|
|
conn.commit()
|
|
cur.close()
|
|
|
|
|
|
def verify(conn):
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
SELECT to_regclass('b2b_listings'),
|
|
to_regclass('marketplace_orders'),
|
|
to_regclass('marketplace_order_items')
|
|
""")
|
|
row = cur.fetchone()
|
|
cur.close()
|
|
return row
|
|
|
|
|
|
def activate_demo_sellers(conn):
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT id, name, COALESCE(is_seller, false) FROM tenants WHERE id = ANY(%s) ORDER BY id",
|
|
(list(DEMO_SELLER_TENANT_IDS),),
|
|
)
|
|
found = cur.fetchall()
|
|
if not found:
|
|
log("no demo tenants found (47, 45) — skipping is_seller activation")
|
|
cur.close()
|
|
return
|
|
cur.execute(
|
|
"UPDATE tenants SET is_seller = TRUE WHERE id = ANY(%s)",
|
|
(list(DEMO_SELLER_TENANT_IDS),),
|
|
)
|
|
conn.commit()
|
|
for tenant_id, name, was_seller in found:
|
|
state = "already seller" if was_seller else "activated"
|
|
log(f"tenant {tenant_id} ({name}): {state}")
|
|
cur.close()
|
|
|
|
|
|
def main():
|
|
print(f"B2B marketplace master schema -> {MASTER_DB_URL}")
|
|
conn = psycopg2.connect(MASTER_DB_URL)
|
|
try:
|
|
ensure_schema(conn)
|
|
activate_demo_sellers(conn)
|
|
b2b, orders, items = verify(conn)
|
|
print("Verification:")
|
|
log(f"b2b_listings={b2b}")
|
|
log(f"marketplace_orders={orders}")
|
|
log(f"marketplace_order_items={items}")
|
|
finally:
|
|
conn.close()
|
|
print("Done.")
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|