package billing import ( "errors" "fmt" "strings" "testing" ) func TestGateErrorWrapping(t *testing.T) { creditErr := fmt.Errorf("%w: need at least %d credits (have %d)", ErrInsufficientCredits, 5, 2) if !errors.Is(creditErr, ErrInsufficientCredits) { t.Fatal("expected ErrInsufficientCredits") } if !strings.Contains(creditErr.Error(), "need at least 5") { t.Fatalf("unexpected message: %v", creditErr) } limitErr := fmt.Errorf("%w: plan allows up to %d products", ErrProductLimitExceeded, 100) if !errors.Is(limitErr, ErrProductLimitExceeded) { t.Fatal("expected ErrProductLimitExceeded") } if errors.Is(limitErr, ErrInsufficientCredits) { t.Fatal("should not match credits error") } aiErr := fmt.Errorf("%w — upgrade", ErrAIRequiresUpgrade) if !errors.Is(aiErr, ErrAIRequiresUpgrade) { t.Fatal("expected ErrAIRequiresUpgrade") } } func TestComputeEntitlements(t *testing.T) { free := ComputeEntitlements("Free", 0, 0, false) if free.CanUseAI || !free.CanUseEPREL || !free.IsFreePlan { t.Fatalf("free: %+v", free) } freeWithLeftover := ComputeEntitlements("Free", 0, 10, false) if !freeWithLeftover.CanUseAI { t.Fatal("leftover credits on Free should unlock AI") } growth := ComputeEntitlements("Growth", 2000, 0, false) if !growth.CanUseAI || !growth.CanUseEPREL || !growth.IsPaidPlan { t.Fatalf("growth: %+v", growth) } enterprise := ComputeEntitlements("Enterprise", EnterpriseUnlimitedCredits, EnterpriseUnlimitedCredits, false) if !enterprise.CanUseAI || !enterprise.CanUseEPREL || !enterprise.IsPaidPlan || enterprise.IsFreePlan { t.Fatalf("enterprise: %+v", enterprise) } if enterprise.MonthlyCredits != EnterpriseUnlimitedCredits || enterprise.RemainingCredits != EnterpriseUnlimitedCredits { t.Fatalf("enterprise credits: %+v", enterprise) } if ProcessingTypeRequiresAI("title") != true { t.Fatal("title requires AI") } if ProcessingTypeRequiresAI("full") { t.Fatal("full should auto-skip AI on Free, not hard-require") } if !ProcessingTypeRequiresEPREL("eprel_only") { t.Fatal("eprel_only requires EPREL") } } func TestDefaultPublicPlansEnterpriseUnlimited(t *testing.T) { plans := defaultPublicPlans() var ent *Plan for i := range plans { if strings.EqualFold(plans[i].Name, "Enterprise") { ent = &plans[i] break } } if ent == nil { t.Fatal("Enterprise missing from defaultPublicPlans") } if ent.MonthlyCredits != EnterpriseUnlimitedCredits { t.Fatalf("monthly_credits=%d want %d", ent.MonthlyCredits, EnterpriseUnlimitedCredits) } if ent.MaxProducts != nil { t.Fatalf("max_products should be nil (unlimited), got %v", *ent.MaxProducts) } if !ent.IsCustom { t.Fatal("Enterprise should be is_custom") } } func TestDefaultPublicPlansFreeZeroCredits(t *testing.T) { plans := defaultPublicPlans() var free *Plan for i := range plans { if strings.EqualFold(plans[i].Name, "Free") { free = &plans[i] break } } if free == nil { t.Fatal("Free missing from defaultPublicPlans") } if free.MonthlyCredits != 0 { t.Fatalf("Free monthly_credits=%d want 0 (Enterprise seed must not change this)", free.MonthlyCredits) } if free.MaxProducts == nil || *free.MaxProducts != 50 { t.Fatalf("Free max_products=%v want 50", free.MaxProducts) } if free.IsCustom { t.Fatal("Free must not be is_custom") } // Enterprise packaging must not leak into Free. if free.MonthlyCredits == EnterpriseUnlimitedCredits { t.Fatal("Free must not share Enterprise credit pack") } } func TestDefaultPublicPlansFiftyPercentCover(t *testing.T) { want := map[string]int{ "Starter": 100, "Plus": 400, "Growth": 1_200, "Business": 4_000, "Scale": 12_000, } pctWant := map[string]int{ "Starter": 50, "Plus": 50, "Growth": 50, "Business": 50, "Scale": 50, "Enterprise": 50, } maxWant := map[string]int{ "Free": 50, "Starter": 100, "Plus": 400, "Growth": 1_200, "Business": 4_000, "Scale": ScaleMaxProducts, } for name, pct := range pctWant { if got := PlanAICoverPercent(name); got != pct { t.Fatalf("PlanAICoverPercent(%s)=%d want %d", name, got, pct) } } for name, exp := range want { if got := MonthlyCreditsForPlan(name, 0); got != exp { t.Fatalf("MonthlyCreditsForPlan(%s)=%d want %d", name, got, exp) } } for _, p := range defaultPublicPlans() { if exp, ok := want[p.Name]; ok && p.MonthlyCredits != exp { t.Fatalf("%s monthly_credits=%d want %d", p.Name, p.MonthlyCredits, exp) } if p.Name == "Enterprise" { if p.MaxProducts != nil { t.Fatalf("Enterprise max_products should be nil, got %v", p.MaxProducts) } continue } wantMax, ok := maxWant[p.Name] if !ok { t.Fatalf("%s missing from maxWant", p.Name) } if p.MaxProducts == nil || *p.MaxProducts != wantMax { t.Fatalf("%s max_products=%v want %d", p.Name, p.MaxProducts, wantMax) } if p.Name != "Free" { base := CreditSKUBase(p.Name) if base <= 0 || base > wantMax { t.Fatalf("%s CreditSKUBase=%d must be in (0, MaxProducts=%d]", p.Name, base, wantMax) } } } // Starter included AI must stay tiny vs A1 (~€300) economics. if want["Starter"] > 150 { t.Fatalf("Starter monthly credits=%d too high vs A1 positioning", want["Starter"]) } if CreditSKUBase("Starter") != 100 || want["Starter"] != 100 { t.Fatalf("Starter base/credits: base=%d credits=%d", CreditSKUBase("Starter"), want["Starter"]) } if got := PlanMaxProducts("Scale"); got == nil || *got != ScaleMaxProducts || ScaleMaxProducts >= 1_000_000 { t.Fatalf("Scale max_products=%v ScaleMaxProducts=%d want %d (<1M)", got, ScaleMaxProducts, ScaleMaxProducts) } }