From c5e5f6ef7e8c841733f3e4aa3cc992c7d52cdb7a Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Wed, 18 Mar 2026 22:24:33 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20add=20SaaS=20schema=20migration=20?= =?UTF-8?q?=E2=80=94=20sessions,=20inventory,=20mappings=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creates sessions, warehouse_inventory, inventory_uploads, inventory_column_mappings tables. Extends users with business_name, is_active, last_login. Updates roles to ADMIN/OWNER/TALLER/BODEGA. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/migrate_saas_schema.py | 127 +++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 scripts/migrate_saas_schema.py diff --git a/scripts/migrate_saas_schema.py b/scripts/migrate_saas_schema.py new file mode 100644 index 0000000..3b6a9fd --- /dev/null +++ b/scripts/migrate_saas_schema.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +""" +Migration: SaaS schema changes +- Update roles (ADMIN, OWNER, TALLER, BODEGA) +- Extend users table with business_name, is_active, created_at, last_login +- Create sessions, warehouse_inventory, inventory_uploads, inventory_column_mappings tables +- Add indexes +""" + +import psycopg2 +import sys + +DB_URL = "postgresql://nexus:nexus_autoparts_2026@localhost/nexus_autoparts" + +STATEMENTS = [ + # ── Roles: UPSERT to desired values ── + """ + INSERT INTO roles (id_rol, name_rol) OVERRIDING SYSTEM VALUE VALUES (1, 'ADMIN') + ON CONFLICT (id_rol) DO UPDATE SET name_rol = EXCLUDED.name_rol; + """, + """ + INSERT INTO roles (id_rol, name_rol) OVERRIDING SYSTEM VALUE VALUES (2, 'OWNER') + ON CONFLICT (id_rol) DO UPDATE SET name_rol = EXCLUDED.name_rol; + """, + """ + INSERT INTO roles (id_rol, name_rol) OVERRIDING SYSTEM VALUE VALUES (3, 'TALLER') + ON CONFLICT (id_rol) DO UPDATE SET name_rol = EXCLUDED.name_rol; + """, + """ + INSERT INTO roles (id_rol, name_rol) OVERRIDING SYSTEM VALUE VALUES (4, 'BODEGA') + ON CONFLICT (id_rol) DO UPDATE SET name_rol = EXCLUDED.name_rol; + """, + + # ── Extend users table ── + "ALTER TABLE users ADD COLUMN IF NOT EXISTS business_name VARCHAR(200);", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT false;", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT now();", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login TIMESTAMP;", + + # ── Unique index on users(email) ── + """ + CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email); + """, + + # ── Sessions table ── + """ + CREATE TABLE IF NOT EXISTS sessions ( + id_session SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id_user) ON DELETE CASCADE, + refresh_token VARCHAR(500) UNIQUE NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT now() + ); + """, + + # ── Warehouse inventory table ── + """ + CREATE TABLE IF NOT EXISTS warehouse_inventory ( + id_inventory BIGSERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id_user), + part_id INTEGER NOT NULL REFERENCES parts(id_part), + price NUMERIC(12,2), + stock_quantity INTEGER DEFAULT 0, + min_order_quantity INTEGER DEFAULT 1, + warehouse_location VARCHAR(100) DEFAULT 'Principal', + updated_at TIMESTAMP DEFAULT now(), + UNIQUE(user_id, part_id, warehouse_location) + ); + """, + + # ── Inventory uploads table ── + """ + CREATE TABLE IF NOT EXISTS inventory_uploads ( + id_upload SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id_user), + filename VARCHAR(200), + status VARCHAR(20) DEFAULT 'pending', + rows_total INTEGER, + rows_imported INTEGER, + rows_errors INTEGER, + error_log TEXT, + created_at TIMESTAMP DEFAULT now(), + completed_at TIMESTAMP + ); + """, + + # ── Inventory column mappings table ── + """ + CREATE TABLE IF NOT EXISTS inventory_column_mappings ( + id_mapping SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL UNIQUE REFERENCES users(id_user), + mapping JSONB DEFAULT '{}'::jsonb + ); + """, + + # ── Indexes ── + "CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(refresh_token);", + "CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);", + "CREATE INDEX IF NOT EXISTS idx_wi_part ON warehouse_inventory(part_id);", + "CREATE INDEX IF NOT EXISTS idx_wi_user ON warehouse_inventory(user_id);", + + # ── Activate existing admin users ── + "UPDATE users SET is_active = true WHERE id_rol = 1;", +] + + +def main(): + conn = psycopg2.connect(DB_URL) + conn.autocommit = True + cur = conn.cursor() + + for i, sql in enumerate(STATEMENTS, 1): + label = sql.strip().split('\n')[0].strip()[:80] + try: + cur.execute(sql) + print(f" [{i:2d}/{len(STATEMENTS)}] OK {label}") + except Exception as e: + print(f" [{i:2d}/{len(STATEMENTS)}] ERR {label}\n {e}") + sys.exit(1) + + cur.close() + conn.close() + print(f"\nMigration complete — {len(STATEMENTS)} statements executed.") + + +if __name__ == "__main__": + main()