#!/usr/bin/env python3 """ Marketplace seed script — populates the DB with realistic test data so you can exercise the marketplace UI end-to-end from a browser. What it creates: - 3 bodegas in different cities (all pre-verified) - ~60 warehouse_inventory rows spread across the 3 bodegas - 1 new employee in tenant_refaccionaria_demo configured as seller for bodega #1 (PIN: 9999) - 3 purchase orders in different states (submitted, confirmed, delivered) After running, you can log into the POS: - As buyer: Ivan Alcaraz (existing owner) — to test Explorar + Mis Pedidos - As seller: "Bodega Test Seller" (new) PIN 9999 — to test Inbox + Inventario Run: cd /home/Autopartes/pos && python3 ../scripts/seed_marketplace.py Safe to re-run: the script cleans up prior seed data before re-inserting. """ import os import sys import bcrypt # Ensure we can import from the POS package sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'pos')) from tenant_db import get_master_conn, get_tenant_conn from services import marketplace_service as mkt # ═══════════════════════════════════════════════════════════════════════════ # CONFIG — tweak these if you want different seed data # ═══════════════════════════════════════════════════════════════════════════ TENANT_ID = 11 # tenant_refaccionaria_demo SELLER_PIN = '9999' # login PIN for the seed seller employee SELLER_NAME = 'Bodega Test Seller' SELLER_EMAIL = 'seller@bodegatest.mx' BODEGAS = [ { 'name': 'Bodega Central Tijuana', 'owner_name': 'Juan Perez', 'whatsapp_phone': '5216641230001', 'email': 'juan@bodegacentral.mx', 'city': 'Tijuana', 'state': 'BC', 'address': 'Blvd. Industrial 1234, Col. Otay, Tijuana BC', }, { 'name': 'Refacciones del Norte GDL', 'owner_name': 'Maria Gonzalez', 'whatsapp_phone': '5213311230002', 'email': 'maria@refnorte.mx', 'city': 'Guadalajara', 'state': 'JAL', 'address': 'Av. Vallarta 5678, Zapopan JAL', }, { 'name': 'AutoPartes del Bajio CDMX', 'owner_name': 'Carlos Torres', 'whatsapp_phone': '5215511230003', 'email': 'carlos@autopartesbajio.mx', 'city': 'Ciudad de Mexico', 'state': 'CDMX', 'address': 'Eje 5 Norte 910, Azcapotzalco CDMX', }, ] # ═══════════════════════════════════════════════════════════════════════════ # HELPERS # ═══════════════════════════════════════════════════════════════════════════ def log(msg): print(f' → {msg}') def hash_pin(pin: str) -> str: return bcrypt.hashpw(pin.encode(), bcrypt.gensalt()).decode() # ═══════════════════════════════════════════════════════════════════════════ # CLEANUP — remove previous seed data before re-inserting # ═══════════════════════════════════════════════════════════════════════════ def cleanup(master_conn, tenant_conn): """Remove previous seed data so re-running the script is idempotent.""" log('Cleaning up previous seed data...') # Tenant side: remove the seed seller employee cur = tenant_conn.cursor() cur.execute("DELETE FROM employees WHERE email = %s", (SELLER_EMAIL,)) tenant_conn.commit() cur.close() # Master side: remove any bodegas with the seed names cur = master_conn.cursor() names = tuple(b['name'] for b in BODEGAS) # Cascade will clean up purchase_orders, po_items, po_status_history, # and warehouse_inventory rows referencing these bodegas. # warehouse_inventory's FK to bodegas is not CASCADE — handle manually. cur.execute(""" DELETE FROM warehouse_inventory WHERE bodega_id IN (SELECT id_bodega FROM bodegas WHERE name = ANY(%s)) """, (list(names),)) cur.execute(""" DELETE FROM purchase_orders WHERE bodega_id IN (SELECT id_bodega FROM bodegas WHERE name = ANY(%s)) """, (list(names),)) cur.execute("DELETE FROM bodegas WHERE name = ANY(%s)", (list(names),)) master_conn.commit() cur.close() log('Cleanup complete.') # ═══════════════════════════════════════════════════════════════════════════ # STEP 1 — Create bodegas (pre-verified) # ═══════════════════════════════════════════════════════════════════════════ def create_bodegas(master_conn) -> list[int]: bodega_ids = [] for b in BODEGAS: bid = mkt.create_bodega( master_conn, name=b['name'], owner_name=b['owner_name'], whatsapp_phone=b['whatsapp_phone'], email=b['email'], city=b['city'], state=b['state'], address=b['address'], ) mkt.verify_bodega(master_conn, bid) bodega_ids.append(bid) log(f'Created+verified bodega #{bid}: {b["name"]}') master_conn.commit() return bodega_ids # ═══════════════════════════════════════════════════════════════════════════ # STEP 2 — Populate warehouse_inventory # ═══════════════════════════════════════════════════════════════════════════ def populate_inventory(master_conn, bodega_ids: list[int]): """Pick ~60 real OEM parts from the catalog, split across the 3 bodegas with pseudo-realistic price and stock levels. warehouse_inventory has an FK on user_id → users(id_user). We use a single master user as the legacy owner AND differentiate bodegas via a composite (user_id, part_id, warehouse_location) key where warehouse_location becomes "Bodega#{bodega_id}" so different bodegas can hold the same part without violating the UNIQUE constraint. """ cur = master_conn.cursor() # Resolve a valid user_id for the FK (doesn't matter which — we use it # as a plumbing placeholder only; the real ownership is via bodega_id). cur.execute("SELECT id_user FROM users ORDER BY id_user LIMIT 1") row = cur.fetchone() if not row: cur.close() log('WARN: no users in master DB — cannot seed inventory') return owner_user_id = row[0] # Grab 60 popular parts (prefer those with aftermarket equivalents so the # buyer gets meaningful data when they open the detail view). cur.execute(""" SELECT p.id_part, p.oem_part_number, p.name_part FROM parts p JOIN aftermarket_parts ap ON ap.oem_part_id = p.id_part WHERE p.oem_part_number IS NOT NULL GROUP BY p.id_part, p.oem_part_number, p.name_part ORDER BY COUNT(ap.id_aftermarket_parts) DESC LIMIT 60 """) parts = cur.fetchall() cur.close() import random random.seed(42) rows_inserted = 0 for i, (part_id, oem, name) in enumerate(parts): bodega_count = random.choice([1, 1, 2, 2, 3]) bodegas_for_this_part = random.sample(bodega_ids, bodega_count) for bid in bodegas_for_this_part: price = round(random.uniform(50, 3500), 2) stock = random.randint(1, 20) location = f'Bodega#{bid}' # unique per bodega → no conflict # Each insert gets its own cursor + savepoint so one failure # doesn't abort the whole transaction. try: cur2 = master_conn.cursor() cur2.execute(""" INSERT INTO warehouse_inventory (user_id, part_id, price, stock_quantity, min_order_quantity, warehouse_location, bodega_id, currency, updated_at) VALUES (%s, %s, %s, %s, 1, %s, %s, 'MXN', NOW()) ON CONFLICT (user_id, part_id, warehouse_location) DO UPDATE SET price = EXCLUDED.price, stock_quantity = EXCLUDED.stock_quantity, bodega_id = EXCLUDED.bodega_id, updated_at = NOW() """, (owner_user_id, part_id, price, stock, location, bid)) master_conn.commit() cur2.close() rows_inserted += 1 except Exception as e: master_conn.rollback() if rows_inserted < 3: # only log the first few to avoid spam print(f' inventory insert failed for part {part_id}: {str(e)[:120]}') log(f'Populated {rows_inserted} warehouse_inventory rows across {len(bodega_ids)} bodegas') # ═══════════════════════════════════════════════════════════════════════════ # STEP 3 — Create a seller employee # ═══════════════════════════════════════════════════════════════════════════ def create_seller_employee(tenant_conn, bodega_id: int) -> int: cur = tenant_conn.cursor() pin_hash = hash_pin(SELLER_PIN) cur.execute(""" INSERT INTO employees (name, email, role, marketplace_role, bodega_id, is_active, pin, password_hash) VALUES (%s, %s, 'employee', 'seller', %s, TRUE, %s, %s) RETURNING id """, (SELLER_NAME, SELLER_EMAIL, bodega_id, pin_hash, pin_hash)) emp_id = cur.fetchone()[0] tenant_conn.commit() cur.close() log(f'Created seller employee #{emp_id} "{SELLER_NAME}" for bodega #{bodega_id} (PIN: {SELLER_PIN})') return emp_id # ═══════════════════════════════════════════════════════════════════════════ # STEP 4 — Create sample POs in different states # ═══════════════════════════════════════════════════════════════════════════ def create_sample_pos(master_conn, bodega_ids: list[int]): """Create 3 POs in states: submitted, confirmed, delivered.""" cur = master_conn.cursor() cur.execute(""" SELECT p.id_part FROM parts p JOIN warehouse_inventory wi ON wi.part_id = p.id_part WHERE wi.bodega_id = %s LIMIT 3 """, (bodega_ids[0],)) sample_parts = [row[0] for row in cur.fetchall()] cur.close() if not sample_parts: log('WARN: no sample parts for PO seed, skipping') return # PO 1 — submitted (waiting for seller to confirm) po1 = mkt.create_po_draft( master_conn, buyer_tenant_id=TENANT_ID, buyer_user_id=1, buyer_display_name='Ivan Alcaraz', buyer_phone='5216649998877', buyer_email='ivan@demo.mx', bodega_id=bodega_ids[0], items=[ {'part_id': sample_parts[0], 'quantity': 2, 'unit_price': 250.00}, {'part_id': sample_parts[1], 'quantity': 1, 'unit_price': 850.00}, ], delivery_method='pickup', buyer_notes='Urgente — cliente esperando', ) mkt.transition_po(master_conn, po_id=po1, new_status='submitted', actor_user_id=1, actor_kind='buyer', note='Enviado') log(f'Created PO #{po1} in state: submitted') # PO 2 — confirmed (seller accepted, preparing) po2 = mkt.create_po_draft( master_conn, buyer_tenant_id=TENANT_ID, buyer_user_id=1, buyer_display_name='Ivan Alcaraz', buyer_phone='5216649998877', buyer_email='ivan@demo.mx', bodega_id=bodega_ids[0], items=[ {'part_id': sample_parts[2], 'quantity': 3, 'unit_price': 420.00}, ], delivery_method='delivery', delivery_address='Refaccionaria Demo, Av. Revolucion 100, Tijuana', buyer_notes='Entregar en la tarde', ) mkt.transition_po(master_conn, po_id=po2, new_status='submitted', actor_user_id=1, actor_kind='buyer') mkt.transition_po(master_conn, po_id=po2, new_status='confirmed', actor_user_id=99, actor_kind='seller', note='Confirmado — preparando pedido') log(f'Created PO #{po2} in state: confirmed') # PO 3 — delivered (full happy path, closed) po3 = mkt.create_po_draft( master_conn, buyer_tenant_id=TENANT_ID, buyer_user_id=1, buyer_display_name='Ivan Alcaraz', buyer_phone='5216649998877', buyer_email='ivan@demo.mx', bodega_id=bodega_ids[1], # different bodega items=[ {'part_id': sample_parts[0], 'quantity': 1, 'unit_price': 175.50}, ], delivery_method='pickup', ) for new_state, kind in [('submitted', 'buyer'), ('confirmed', 'seller'), ('ready', 'seller'), ('delivered', 'buyer')]: mkt.transition_po(master_conn, po_id=po3, new_status=new_state, actor_user_id=1 if kind == 'buyer' else 99, actor_kind=kind) log(f'Created PO #{po3} in state: delivered') # ═══════════════════════════════════════════════════════════════════════════ # MAIN # ═══════════════════════════════════════════════════════════════════════════ def main(): print('╔══════════════════════════════════════════════╗') print('║ Nexus Marketplace — Seed Script ║') print('╚══════════════════════════════════════════════╝') print() master_conn = get_master_conn() tenant_conn = get_tenant_conn(TENANT_ID) try: cleanup(master_conn, tenant_conn) print() print('STEP 1 — Create bodegas') bodega_ids = create_bodegas(master_conn) print() print('STEP 2 — Populate warehouse_inventory') populate_inventory(master_conn, bodega_ids) print() print('STEP 3 — Create seller employee') seller_id = create_seller_employee(tenant_conn, bodega_ids[0]) print() print('STEP 4 — Create sample POs') create_sample_pos(master_conn, bodega_ids) print() print('╔══════════════════════════════════════════════╗') print('║ DONE ║') print('╚══════════════════════════════════════════════╝') print() print('Log in at http://localhost:5001/pos/login with:') print() print(f' BUYER (Ivan Alcaraz — existing owner)') print(f' PIN: (your existing PIN)') print(f' Test at: http://localhost:5001/pos/marketplace') print(f' Should see: Explorar + Mis Pedidos tabs') print() print(f' SELLER ({SELLER_NAME})') print(f' Email: {SELLER_EMAIL}') print(f' PIN: {SELLER_PIN}') print(f' Test at: http://localhost:5001/pos/marketplace') print(f' Should see: Pedidos Recibidos + Mi Inventario + Explorar') print() print(f'Bodegas created: #{bodega_ids[0]} (Tijuana), ' f'#{bodega_ids[1]} (Guadalajara), #{bodega_ids[2]} (CDMX)') finally: master_conn.close() tenant_conn.close() if __name__ == '__main__': main()