package main import ( "context" "fmt" "strings" "github.com/descrybe/descrybe-v2/apps/api/internal/auth" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) const defaultMigratorDemoCompany = "Platform Demo" // ensureDemoUser upserts a local demo account (no Clerk), makes them platform admin, // and binds them only to a standalone Platform Demo company (never A1 / every tenant). func ensureDemoUser( ctx context.Context, pg *pgxpool.Pool, email, password, displayName, localDemoName string, dryRun bool, report map[string]int, ) (*DemoReport, error) { emailNorm := strings.ToLower(strings.TrimSpace(email)) if emailNorm == "" || password == "" { return nil, fmt.Errorf("demo email and password required") } if localDemoName == "" { localDemoName = defaultMigratorDemoCompany } if displayName == "" { displayName = "Demo User" } out := &DemoReport{ Email: emailNorm, PasswordSet: true, PlatformAdmin: true, Note: "Password documented in docs/portable-mysql-pg-migration.md (not written to report JSON).", } if dryRun { report["demo_user"] = 1 out.Note = "dry-run: demo user not written" out.PasswordSet = false return out, nil } hash, err := auth.HashPassword(password) if err != nil { return nil, err } tx, err := pg.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) var userID uuid.UUID err = tx.QueryRow(ctx, ` INSERT INTO users ( email, name, password_hash, must_set_password, is_platform_admin, is_active, updated_at ) VALUES ($1, $2, $3, false, true, true, now()) ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name, password_hash = EXCLUDED.password_hash, must_set_password = false, is_platform_admin = true, is_active = true, updated_at = now() RETURNING id`, emailNorm, displayName, hash).Scan(&userID) if err != nil { return nil, fmt.Errorf("upsert demo user: %w", err) } out.UserID = userID.String() demoCompanyID, demoName, err := ensureMigratorDemoCompany(ctx, tx, localDemoName) if err != nil { return nil, err } out.PrimaryCompany = demoCompanyID.String() out.PrimaryName = demoName ct, err := tx.Exec(ctx, ` INSERT INTO memberships (company_id, user_id, role, status) VALUES ($1, $2, 'admin', 'active') ON CONFLICT (company_id, user_id) DO UPDATE SET role = 'admin', status = 'active', updated_at = now()`, demoCompanyID, userID) if err != nil { return nil, fmt.Errorf("demo membership: %w", err) } if _, err := tx.Exec(ctx, ` DELETE FROM memberships WHERE user_id = $1 AND company_id <> $2`, userID, demoCompanyID); err != nil { return nil, fmt.Errorf("remove non-demo memberships: %w", err) } out.Memberships = ct.RowsAffected() if err := tx.Commit(ctx); err != nil { return nil, err } report["demo_user"] = 1 report["demo_memberships"] = int(out.Memberships) return out, nil } func ensureMigratorDemoCompany(ctx context.Context, tx pgx.Tx, name string) (uuid.UUID, string, error) { name = strings.TrimSpace(name) if name == "" { name = defaultMigratorDemoCompany } if strings.EqualFold(name, "A1 Slovenija") || strings.EqualFold(name, "A1") || strings.EqualFold(name, "Local Demo Co") { return uuid.Nil, "", fmt.Errorf("demo company name %q collides with A1 tenant — use %q", name, defaultMigratorDemoCompany) } var id uuid.UUID err := tx.QueryRow(ctx, ` SELECT c.id FROM companies c WHERE c.name = $1 AND COALESCE(c.legacy_company_id, '') <> $2 ORDER BY c.created_at ASC LIMIT 1`, name, billing.A1LegacyCompanyID).Scan(&id) if err == nil { if _, err := tx.Exec(ctx, `INSERT INTO company_settings (company_id) VALUES ($1) ON CONFLICT DO NOTHING`, id); err != nil { return uuid.Nil, "", err } if _, err := tx.Exec(ctx, `INSERT INTO credit_balances (company_id) VALUES ($1) ON CONFLICT DO NOTHING`, id); err != nil { return uuid.Nil, "", err } return id, name, nil } if err != pgx.ErrNoRows { return uuid.Nil, "", err } err = tx.QueryRow(ctx, `INSERT INTO companies (name) VALUES ($1) RETURNING id`, name).Scan(&id) if err != nil { return uuid.Nil, "", err } if _, err := tx.Exec(ctx, `INSERT INTO company_settings (company_id) VALUES ($1) ON CONFLICT DO NOTHING`, id); err != nil { return uuid.Nil, "", err } if _, err := tx.Exec(ctx, `INSERT INTO credit_balances (company_id) VALUES ($1) ON CONFLICT DO NOTHING`, id); err != nil { return uuid.Nil, "", err } return id, name, nil }