Files
resolutionflow/backend/scripts/seed_test_users.py
Michael Chihlas 49c6c8fd00 fix(seed): include cancel_at_period_end in test-user subscription INSERT
Discovered during Phase 9 QA: seed_test_users.py was missing the
cancel_at_period_end column in its subscriptions INSERT, but the
column is NOT NULL (added in 016_add_subscription_tables.py).
Result: seed crashed with NotNullViolationError before any users
were created, blocking auth in fresh dev environments.

Pre-existing on main; not introduced by the FlowPilot migration
branch. Default value: false.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 23:36:04 -04:00

192 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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:
# Must use ADMIN_DATABASE_URL (BYPASSRLS) — Phase 4 enabled RLS on users.
# The app-role connection has no tenant context at seed time and would see 0 rows.
admin_url = getattr(settings, "ADMIN_DATABASE_URL", None) or settings.DATABASE_URL
engine = create_async_engine(admin_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, cancel_at_period_end, created_at, updated_at)
VALUES (:id, :aid, :plan, 'active', false, :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())