159 lines
4.4 KiB
Go
159 lines
4.4 KiB
Go
package main
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"log"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
|
||
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
|
)
|
||
|
|
|
||
|
|
// runCompaniesWithoutPlansRepair lists and/or assigns plans for companies that
|
||
|
|
// have no active company_plans row. Postgres-only; never deletes or overwrites
|
||
|
|
// an existing active plan. Live writes require confirm=true (no blind assigns).
|
||
|
|
func runCompaniesWithoutPlansRepair(postgresURL, planName string, listOnly, assign bool, dryRun, confirm bool) {
|
||
|
|
if postgresURL == "" {
|
||
|
|
log.Fatal("-postgres / DATABASE_URL is required for companies-without-plans tooling")
|
||
|
|
}
|
||
|
|
planName, err := normalizeAssignPlanName(planName, assign)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatal(err)
|
||
|
|
}
|
||
|
|
if !listOnly && !assign {
|
||
|
|
log.Fatal("pass -list-companies-without-plans and/or -assign-missing-plans")
|
||
|
|
}
|
||
|
|
if err := guardLiveMutation(assign, dryRun, confirm, "-assign-missing-plans"); err != nil {
|
||
|
|
log.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
pg, err := pgxpool.New(ctx, postgresURL)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatalf("postgres: %v", err)
|
||
|
|
}
|
||
|
|
defer pg.Close()
|
||
|
|
|
||
|
|
svc := &billing.Service{Pool: pg}
|
||
|
|
if err := svc.EnsureDefaultPlans(ctx); err != nil {
|
||
|
|
log.Fatalf("ensure default plans: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
total, err := svc.CountCompaniesWithoutActivePlan(ctx)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatalf("count companies without active plan: %v", err)
|
||
|
|
}
|
||
|
|
fmt.Printf("companies_without_active_plan: %d\n", total)
|
||
|
|
|
||
|
|
const page = 200
|
||
|
|
offset := 0
|
||
|
|
listed := 0
|
||
|
|
assigned := 0
|
||
|
|
skipped := 0
|
||
|
|
a1Skipped := 0
|
||
|
|
|
||
|
|
for {
|
||
|
|
rows, err := svc.ListCompaniesWithoutActivePlan(ctx, page, offset)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatalf("list companies without active plan: %v", err)
|
||
|
|
}
|
||
|
|
if len(rows) == 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
pageAssigned := 0
|
||
|
|
for _, c := range rows {
|
||
|
|
listed++
|
||
|
|
a1 := billing.IsA1CohortCompany(c.LegacyCompanyID, c.Name)
|
||
|
|
if listOnly || !assign {
|
||
|
|
fmt.Printf(" %s\t%s\t%s\ta1=%v\n", c.ID, c.Name, c.Language, a1)
|
||
|
|
}
|
||
|
|
mutate, skipReason := decideAssignMissingPlan(assign, c.LegacyCompanyID, c.Name)
|
||
|
|
if !mutate {
|
||
|
|
if assign && skipReason != "" {
|
||
|
|
fmt.Printf("skip\t%s\t%s\t%s\n", c.ID, c.Name, skipReason)
|
||
|
|
skipped++
|
||
|
|
a1Skipped++
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if dryRun {
|
||
|
|
fmt.Printf("dry-run: would assign plan %q to company %s (%s)\n", planName, c.ID, c.Name)
|
||
|
|
assigned++
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
ok, err := svc.AssignPlanByNameIfMissing(ctx, c.ID, planName)
|
||
|
|
if err != nil {
|
||
|
|
log.Printf("assign plan %q to company %s: %v", planName, c.ID, err)
|
||
|
|
skipped++
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !ok {
|
||
|
|
skipped++
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
fmt.Printf("assigned plan %q to company %s (%s)\n", planName, c.ID, c.Name)
|
||
|
|
assigned++
|
||
|
|
pageAssigned++
|
||
|
|
}
|
||
|
|
if len(rows) < page {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if assign && !dryRun {
|
||
|
|
// Live assigns shrink the result set, so restart from offset 0.
|
||
|
|
// If this page assigned nothing (e.g. all A1 skips), advance offset
|
||
|
|
// so we cannot spin forever on the same unassignable rows.
|
||
|
|
if pageAssigned == 0 {
|
||
|
|
offset += page
|
||
|
|
} else {
|
||
|
|
offset = 0
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
offset += page
|
||
|
|
}
|
||
|
|
|
||
|
|
if assign {
|
||
|
|
fmt.Printf("listed=%d assigned=%d skipped=%d a1_skipped=%d dry_run=%v plan=%q\n", listed, assigned, skipped, a1Skipped, dryRun, planName)
|
||
|
|
} else {
|
||
|
|
fmt.Printf("listed=%d\n", listed)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// decideAssignMissingPlan is the assign gate used by dry-run and -confirm.
|
||
|
|
// A1 cohort companies always skip (never get Free/other fallback), even when confirm=true.
|
||
|
|
func decideAssignMissingPlan(assign bool, legacyCompanyID, companyName string) (mutate bool, skipReason string) {
|
||
|
|
if !assign {
|
||
|
|
return false, ""
|
||
|
|
}
|
||
|
|
if billing.IsA1CohortCompany(legacyCompanyID, companyName) {
|
||
|
|
return false, "a1_cohort"
|
||
|
|
}
|
||
|
|
return true, ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeAssignPlanName(planName string, assign bool) (string, error) {
|
||
|
|
planName = strings.TrimSpace(planName)
|
||
|
|
if assign && planName == "" {
|
||
|
|
return "", fmt.Errorf("-plan-name is required with -assign-missing-plans (e.g. Free)")
|
||
|
|
}
|
||
|
|
return planName, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// guardLiveAssign is kept for tests; prefer guardLiveMutation for new call sites.
|
||
|
|
func guardLiveAssign(assign, dryRun, confirm bool) error {
|
||
|
|
return guardLiveMutation(assign, dryRun, confirm, "-assign-missing-plans")
|
||
|
|
}
|
||
|
|
|
||
|
|
func resolveFallbackPlanID(ctx context.Context, pg *pgxpool.Pool, planName string) (int64, error) {
|
||
|
|
planName = strings.TrimSpace(planName)
|
||
|
|
if planName == "" {
|
||
|
|
return 0, nil
|
||
|
|
}
|
||
|
|
svc := &billing.Service{Pool: pg}
|
||
|
|
if err := svc.EnsureDefaultPlans(ctx); err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
return svc.PlanIDByName(ctx, planName)
|
||
|
|
}
|