173 lines
5.5 KiB
Go
173 lines
5.5 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|