Files

173 lines
5.5 KiB
Go
Raw Permalink Normal View History

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)
}
}