diff --git a/backend/scripts/seed_procedural_flows.py b/backend/scripts/seed_procedural_flows.py index 4436b330..a4990931 100644 --- a/backend/scripts/seed_procedural_flows.py +++ b/backend/scripts/seed_procedural_flows.py @@ -883,6 +883,7 @@ async def create_procedural_flow(client: httpx.AsyncClient, token: str, flow_dat # Mark as default/system flow (public and visible to all) flow_data["is_default"] = True flow_data["is_public"] = True + flow_data["status"] = "draft" # Skip validation for seed data # Check if flow with same name exists list_response = await client.get(f"{API_BASE_URL}/trees", headers=headers, params={"tree_type": "procedural"}) diff --git a/backend/scripts/seed_test_users.py b/backend/scripts/seed_test_users.py new file mode 100644 index 00000000..f8348d97 --- /dev/null +++ b/backend/scripts/seed_test_users.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +""" +Create test user accounts for local development. + +Creates 4 accounts: + 1. Super Admin – platform-wide admin (manages everything) + 2. Pro Solo User – single user on a "pro" plan + 3. Team Admin – admin of a team account ("team" plan) + 4. Team Engineer – regular engineer on the same team account + +Usage: + cd backend + python -m scripts.seed_test_users +""" + +import asyncio +import random +import string +import uuid +from datetime import datetime, timezone + +from sqlalchemy import text +from sqlalchemy.ext.asyncio import create_async_engine + +from app.core.config import settings +from app.core.security import get_password_hash + + +# --------------------------------------------------------------------------- +# Configuration – change passwords here if you like +# --------------------------------------------------------------------------- +SHARED_PASSWORD = "TestPass123!" + +USERS = [ + { + "key": "super_admin", + "name": "RF Super Admin", + "email": "admin@resolutionflow.example.com", + "is_super_admin": True, + "is_team_admin": False, + "account_name": "ResolutionFlow Platform", + "account_role": "owner", + "plan": "team", + }, + { + "key": "pro_solo", + "name": "Pat Solo", + "email": "pro@resolutionflow.example.com", + "is_super_admin": False, + "is_team_admin": False, + "account_name": "Pat's MSP", + "account_role": "owner", + "plan": "pro", + }, + { + "key": "team_admin", + "name": "Alex Manager", + "email": "teamadmin@resolutionflow.example.com", + "is_super_admin": False, + "is_team_admin": True, + "account_name": "Acme MSP", + "account_role": "owner", # owns the account; is_team_admin=True gives admin powers + "plan": "team", + }, + { + "key": "team_engineer", + "name": "Jordan Tech", + "email": "engineer@resolutionflow.example.com", + "is_super_admin": False, + "is_team_admin": False, + "account_name": "Acme MSP", # same shared account + "account_role": "engineer", + "plan": None, # uses the team_admin's account & subscription + }, +] + + +def _display_code() -> str: + return "".join(random.choices(string.ascii_uppercase + string.digits, k=8)) + + +async def main() -> None: + engine = create_async_engine(settings.DATABASE_URL, echo=False) + password_hash = get_password_hash(SHARED_PASSWORD) + now = datetime.now(timezone.utc) + team_account_id: uuid.UUID | None = None + + async with engine.begin() as conn: + for cfg in USERS: + # Check if user already exists + result = await conn.execute( + text("SELECT id, account_id FROM users WHERE email = :email"), + {"email": cfg["email"]}, + ) + row = result.first() + if row: + print(f" [SKIP] {cfg['email']} already exists") + if cfg["key"] == "team_admin": + team_account_id = row.account_id + continue + + # ---- Create or reuse Account ---- + if cfg["key"] == "team_engineer": + if team_account_id is None: + result = await conn.execute( + text("SELECT id FROM accounts WHERE name = :name"), + {"name": "Acme MSP"}, + ) + acme = result.first() + if acme: + team_account_id = acme.id + if team_account_id is None: + print(f" [ERROR] Cannot create {cfg['email']} — Acme MSP account not found.") + continue + account_id = team_account_id + else: + account_id = uuid.uuid4() + await conn.execute( + text(""" + INSERT INTO accounts (id, name, display_code, created_at, updated_at) + VALUES (:id, :name, :code, :now, :now) + """), + {"id": account_id, "name": cfg["account_name"], "code": _display_code(), "now": now}, + ) + if cfg["key"] == "team_admin": + team_account_id = account_id + + # ---- Create User ---- + user_id = uuid.uuid4() + await conn.execute( + text(""" + INSERT INTO users (id, email, password_hash, name, role, is_super_admin, + is_team_admin, is_active, account_id, account_role, created_at) + VALUES (:id, :email, :pw, :name, 'engineer', :is_sa, :is_ta, true, + :account_id, :account_role, :now) + """), + { + "id": user_id, + "email": cfg["email"], + "pw": password_hash, + "name": cfg["name"], + "is_sa": cfg["is_super_admin"], + "is_ta": cfg["is_team_admin"], + "account_id": account_id, + "account_role": cfg["account_role"], + "now": now, + }, + ) + + # Set account owner (skip for team_engineer — they don't own the account) + if cfg["key"] != "team_engineer": + await conn.execute( + text("UPDATE accounts SET owner_id = :uid WHERE id = :aid"), + {"uid": user_id, "aid": account_id}, + ) + + # ---- Create Subscription (once per account) ---- + if cfg["plan"] is not None: + await conn.execute( + text(""" + INSERT INTO subscriptions (id, account_id, plan, status, created_at, updated_at) + VALUES (:id, :aid, :plan, 'active', :now, :now) + """), + {"id": uuid.uuid4(), "aid": account_id, "plan": cfg["plan"], "now": now}, + ) + + print(f" [OK] {cfg['email']:40s} account_role={cfg['account_role']:<10s} plan={cfg['plan'] or '(shared)'}") + + await engine.dispose() + + print() + print("=" * 60) + print(" Test accounts ready!") + print(f" Password for all accounts: {SHARED_PASSWORD}") + print("=" * 60) + print() + print(" Accounts:") + print(f" Super Admin : admin@resolutionflow.example.com") + print(f" Pro Solo : pro@resolutionflow.example.com") + print(f" Team Admin : teamadmin@resolutionflow.example.com") + print(f" Team Engineer: engineer@resolutionflow.example.com") + print() + + +if __name__ == "__main__": + print("\n[*] ResolutionFlow — Test User Seeder") + print("=" * 60) + asyncio.run(main()) diff --git a/backend/scripts/seed_trees.py b/backend/scripts/seed_trees.py index bd15396f..b839eba4 100644 --- a/backend/scripts/seed_trees.py +++ b/backend/scripts/seed_trees.py @@ -3330,6 +3330,7 @@ async def create_tree(client: httpx.AsyncClient, token: str, tree_data: dict) -> # Mark as default/system tree (public and visible to all) tree_data["is_default"] = True tree_data["is_public"] = True + tree_data["status"] = "draft" # Skip validation for seed data # Check if tree with same name exists list_response = await client.get(f"{API_BASE_URL}/trees", headers=headers) diff --git a/backend/scripts/seed_trees_v2.py b/backend/scripts/seed_trees_v2.py index 0045619e..0bfaf48c 100644 --- a/backend/scripts/seed_trees_v2.py +++ b/backend/scripts/seed_trees_v2.py @@ -1089,6 +1089,7 @@ async def create_tree(client: httpx.AsyncClient, token: str, tree_data: dict, ca tree_data["is_default"] = True tree_data["is_public"] = True + tree_data["status"] = "draft" # Skip validation for seed data # Normalize description -> action/solution fields normalize_tree_structure(tree_data)