package billing import ( "context" "errors" "os" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) func TestPlanIDByNameRequiresName(t *testing.T) { t.Parallel() svc := &Service{} _, err := svc.PlanIDByName(context.Background(), " ") if !errors.Is(err, ErrPlanNameRequired) { t.Fatalf("got %v, want ErrPlanNameRequired", err) } } func TestAssignPlanIfMissingSkipsExisting(t *testing.T) { dsn := os.Getenv("DATABASE_URL") if dsn == "" { t.Skip("DATABASE_URL not set") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() pg, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatal(err) } defer pg.Close() svc := &Service{Pool: pg} if err := svc.EnsureDefaultPlans(ctx); err != nil { t.Fatal(err) } freeID, err := svc.PlanIDByName(ctx, "Free") if err != nil { t.Fatal(err) } starterID, err := svc.PlanIDByName(ctx, "Starter") if err != nil { t.Fatal(err) } companyID := uuid.New() _, err = pg.Exec(ctx, `INSERT INTO companies (id, name) VALUES ($1, $2)`, companyID, "missing-plans-"+companyID.String()[:8]) if err != nil { t.Fatal(err) } t.Cleanup(func() { cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second) defer cleanupCancel() _, _ = pg.Exec(cleanupCtx, `DELETE FROM companies WHERE id = $1`, companyID) }) assigned, err := svc.AssignPlanIfMissing(ctx, companyID, freeID) if err != nil { t.Fatal(err) } if !assigned { t.Fatal("expected first assign to succeed") } assigned, err = svc.AssignPlanIfMissing(ctx, companyID, starterID) if err != nil { t.Fatal(err) } if assigned { t.Fatal("must not overwrite an existing active plan") } has, err := svc.HasActivePlan(ctx, companyID) if err != nil || !has { t.Fatalf("has active plan: has=%v err=%v", has, err) } var planID int64 err = pg.QueryRow(ctx, `SELECT plan_id FROM company_plans WHERE company_id = $1 AND is_active = true`, companyID).Scan(&planID) if err != nil { t.Fatal(err) } if planID != freeID { t.Fatalf("active plan_id=%d, want Free id=%d", planID, freeID) } missing, err := svc.ListCompaniesWithoutActivePlan(ctx, 500, 0) if err != nil { t.Fatal(err) } for _, c := range missing { if c.ID == companyID { t.Fatal("company with active plan must not appear in without-plan list") } } } func TestListCompaniesWithoutActivePlanIncludesBareCompany(t *testing.T) { dsn := os.Getenv("DATABASE_URL") if dsn == "" { t.Skip("DATABASE_URL not set") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() pg, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatal(err) } defer pg.Close() svc := &Service{Pool: pg} companyID := uuid.New() _, err = pg.Exec(ctx, `INSERT INTO companies (id, name) VALUES ($1, $2)`, companyID, "no-plan-"+companyID.String()[:8]) if err != nil { t.Fatal(err) } t.Cleanup(func() { cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second) defer cleanupCancel() _, _ = pg.Exec(cleanupCtx, `DELETE FROM companies WHERE id = $1`, companyID) }) found := false rows, err := svc.ListCompaniesWithoutActivePlan(ctx, 500, 0) if err != nil { t.Fatal(err) } for _, c := range rows { if c.ID == companyID { found = true break } } if !found { t.Fatal("bare company must appear in without-plan list") } }