package main import ( "context" "os" "strings" "testing" "time" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) const a1CompanyName = "A1 Slovenija" // Integration smoke: A1 Slovenija must own processed products after migrate + seed-demo. // Skips when DATABASE_URL is unset (CI without Postgres). func TestLocalDemoCoHasProducts(t *testing.T) { dsn := strings.TrimSpace(os.Getenv("DATABASE_URL")) if dsn == "" { t.Skip("DATABASE_URL not set") } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() pg, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("postgres: %v", err) } defer pg.Close() var ( id uuid.UUID name string products int64 ) err = pg.QueryRow(ctx, ` SELECT c.id, c.name, (SELECT COUNT(*) FROM processed_products p WHERE p.company_id = c.id) FROM companies c WHERE c.name = $1 OR c.legacy_company_id = $2 ORDER BY (SELECT COUNT(*) FROM processed_products p WHERE p.company_id = c.id) DESC LIMIT 1`, a1CompanyName, billing.A1LegacyCompanyID).Scan(&id, &name, &products) if err != nil { t.Fatalf("query A1 Slovenija: %v (run migrate + seed-demo first)", err) } if products <= 0 { t.Fatalf("%s %s has %d products; want > 0", name, id, products) } t.Logf("%s id=%s products=%d", name, id, products) } // Integration smoke: A1 cohort keeps dump-faithful wallet (not fake demo 1M / Legacy rename). func TestLocalDemoCoLegacyCredits(t *testing.T) { dsn := strings.TrimSpace(os.Getenv("DATABASE_URL")) if dsn == "" { t.Skip("DATABASE_URL not set") } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() pg, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("postgres: %v", err) } defer pg.Close() var ( companyName string planName string monthly int maxProd *int isCustom bool isLegacy bool total int used int ) err = pg.QueryRow(ctx, ` SELECT c.name, p.name, p.monthly_credits, p.max_products, p.is_custom, COALESCE(p.is_legacy, false), cb.total_credits, cb.used_credits FROM companies c JOIN company_plans cp ON cp.company_id = c.id AND cp.is_active = true JOIN plans p ON p.id = cp.plan_id JOIN credit_balances cb ON cb.company_id = c.id WHERE c.name = $1 OR c.legacy_company_id = $2 ORDER BY (SELECT COUNT(*) FROM processed_products pp WHERE pp.company_id = c.id) DESC LIMIT 1`, a1CompanyName, billing.A1LegacyCompanyID). Scan(&companyName, &planName, &monthly, &maxProd, &isCustom, &isLegacy, &total, &used) if err != nil { t.Fatalf("query A1 plan/wallet: %v (run migrate + seed-demo first)", err) } if !strings.EqualFold(companyName, a1CompanyName) { t.Fatalf("company=%s want %s", companyName, a1CompanyName) } if !billing.IsLegacyPlan(planName, isLegacy) { t.Fatalf("plan=%s is_legacy=%v want A1/Legacy cohort", planName, isLegacy) } if total == 1_000_000 && used == 0 { t.Fatalf("wallet looks like fake demo pack total=%d used=%d; want dump credit_balances", total, used) } // Dump A1 was ~2500/216; allow local drift but reject empty or fake demo packs. if total < 1000 { t.Fatalf("total_credits=%d want >= 1000 (dump-shaped A1 wallet)", total) } if used < 0 { t.Fatalf("used_credits=%d want >= 0", used) } if monthly == 1_000_000 { t.Fatalf("plan monthly_credits inflated to demo 1M; dump A1 monthly was 0") } t.Logf("company=%s plan=%s monthly=%d total=%d used=%d remaining=%d is_legacy=%v is_custom=%v", companyName, planName, monthly, total, used, total-used, isLegacy, isCustom) } // Integration smoke: demo users belong only to Platform Demo (not A1). func TestDemoUsersIsolatedFromA1(t *testing.T) { dsn := strings.TrimSpace(os.Getenv("DATABASE_URL")) if dsn == "" { t.Skip("DATABASE_URL not set") } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() pg, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("postgres: %v", err) } defer pg.Close() rows, err := pg.Query(ctx, ` SELECT u.email, c.name, COALESCE(c.legacy_company_id, '') FROM users u JOIN memberships m ON m.user_id = u.id AND m.status = 'active' JOIN companies c ON c.id = m.company_id WHERE lower(u.email) IN ('demo@descrybe.local', 'demo@descrybe.test') ORDER BY u.email, c.name`) if err != nil { t.Fatalf("query demo memberships: %v (run seed-demo first)", err) } defer rows.Close() type mem struct { email, company, legacy string } var found []mem for rows.Next() { var m mem if err := rows.Scan(&m.email, &m.company, &m.legacy); err != nil { t.Fatalf("scan: %v", err) } found = append(found, m) if strings.EqualFold(m.legacy, billing.A1LegacyCompanyID) || strings.EqualFold(m.company, a1CompanyName) || strings.EqualFold(m.company, "Local Demo Co") { t.Fatalf("demo %s still member of A1 tenant %q (legacy=%s)", m.email, m.company, m.legacy) } if !strings.EqualFold(m.company, "Platform Demo") && !strings.EqualFold(m.company, "Demo") { t.Fatalf("demo %s company=%q want Platform Demo", m.email, m.company) } } if err := rows.Err(); err != nil { t.Fatalf("rows: %v", err) } if len(found) == 0 { t.Fatal("no demo memberships found — run go run ./cmd/seed-demo") } t.Logf("demo memberships ok: %+v", found) } // Integration smoke: Free plan definition stays at 0 AI credits after A1 seed. func TestFreePlanUnaffectedByEnterpriseSeed(t *testing.T) { dsn := strings.TrimSpace(os.Getenv("DATABASE_URL")) if dsn == "" { t.Skip("DATABASE_URL not set") } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() pg, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("postgres: %v", err) } defer pg.Close() var ( name string monthly int maxProd *int custom bool ) err = pg.QueryRow(ctx, ` SELECT name, monthly_credits, max_products, is_custom FROM plans WHERE lower(name) = 'free' ORDER BY id LIMIT 1`). Scan(&name, &monthly, &maxProd, &custom) if err != nil { t.Fatalf("query Free plan: %v", err) } if monthly != 0 { t.Fatalf("Free monthly_credits=%d want 0", monthly) } wantMax := billing.PlanMaxProducts("Free") if wantMax == nil || maxProd == nil || *maxProd != *wantMax { t.Fatalf("Free max_products=%v want %v", maxProd, wantMax) } t.Logf("Free plan ok name=%s monthly=%d max_products=%d", name, monthly, *maxProd) }