This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/billing-service/internal/services/subscription_service_test.go
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

327 lines
8.3 KiB
Go

package services
import (
"encoding/json"
"testing"
"github.com/breakpilot/billing-service/internal/models"
)
func TestSubscriptionStatus_Transitions(t *testing.T) {
// Test valid subscription status values
validStatuses := []models.SubscriptionStatus{
models.StatusTrialing,
models.StatusActive,
models.StatusPastDue,
models.StatusCanceled,
models.StatusExpired,
}
for _, status := range validStatuses {
if status == "" {
t.Errorf("Status should not be empty")
}
}
}
func TestPlanID_ValidValues(t *testing.T) {
validPlanIDs := []models.PlanID{
models.PlanBasic,
models.PlanStandard,
models.PlanPremium,
}
expected := []string{"basic", "standard", "premium"}
for i, planID := range validPlanIDs {
if string(planID) != expected[i] {
t.Errorf("PlanID should be '%s', got '%s'", expected[i], planID)
}
}
}
func TestPlanFeatures_JSONSerialization(t *testing.T) {
features := models.PlanFeatures{
MonthlyTaskAllowance: 100,
MaxTaskBalance: 500,
FeatureFlags: []string{"basic_ai", "templates"},
MaxTeamMembers: 3,
PrioritySupport: false,
CustomBranding: false,
BatchProcessing: true,
CustomTemplates: true,
FairUseMode: false,
}
// Test JSON serialization
data, err := json.Marshal(features)
if err != nil {
t.Fatalf("Failed to marshal PlanFeatures: %v", err)
}
// Test JSON deserialization
var decoded models.PlanFeatures
err = json.Unmarshal(data, &decoded)
if err != nil {
t.Fatalf("Failed to unmarshal PlanFeatures: %v", err)
}
// Verify fields
if decoded.MonthlyTaskAllowance != features.MonthlyTaskAllowance {
t.Errorf("MonthlyTaskAllowance mismatch: got %d, expected %d",
decoded.MonthlyTaskAllowance, features.MonthlyTaskAllowance)
}
if decoded.MaxTaskBalance != features.MaxTaskBalance {
t.Errorf("MaxTaskBalance mismatch: got %d, expected %d",
decoded.MaxTaskBalance, features.MaxTaskBalance)
}
if decoded.BatchProcessing != features.BatchProcessing {
t.Errorf("BatchProcessing mismatch: got %v, expected %v",
decoded.BatchProcessing, features.BatchProcessing)
}
}
func TestBillingPlan_DefaultPlansAreValid(t *testing.T) {
plans := models.GetDefaultPlans()
if len(plans) != 3 {
t.Fatalf("Expected 3 default plans, got %d", len(plans))
}
// Verify all plans have required fields
for _, plan := range plans {
if plan.ID == "" {
t.Errorf("Plan ID should not be empty")
}
if plan.Name == "" {
t.Errorf("Plan '%s' should have a name", plan.ID)
}
if plan.Description == "" {
t.Errorf("Plan '%s' should have a description", plan.ID)
}
if plan.PriceCents <= 0 {
t.Errorf("Plan '%s' should have a positive price, got %d", plan.ID, plan.PriceCents)
}
if plan.Currency != "eur" {
t.Errorf("Plan '%s' currency should be 'eur', got '%s'", plan.ID, plan.Currency)
}
if plan.Interval != "month" {
t.Errorf("Plan '%s' interval should be 'month', got '%s'", plan.ID, plan.Interval)
}
if !plan.IsActive {
t.Errorf("Plan '%s' should be active", plan.ID)
}
if plan.SortOrder <= 0 {
t.Errorf("Plan '%s' should have a positive sort order, got %d", plan.ID, plan.SortOrder)
}
}
}
func TestBillingPlan_TaskAllowanceProgression(t *testing.T) {
plans := models.GetDefaultPlans()
// Basic should have lowest allowance
basic := plans[0]
standard := plans[1]
premium := plans[2]
if basic.Features.MonthlyTaskAllowance >= standard.Features.MonthlyTaskAllowance {
t.Error("Standard plan should have more tasks than Basic")
}
if standard.Features.MonthlyTaskAllowance >= premium.Features.MonthlyTaskAllowance {
t.Error("Premium plan should have more tasks than Standard")
}
}
func TestBillingPlan_PriceProgression(t *testing.T) {
plans := models.GetDefaultPlans()
// Prices should increase with each tier
if plans[0].PriceCents >= plans[1].PriceCents {
t.Error("Standard should cost more than Basic")
}
if plans[1].PriceCents >= plans[2].PriceCents {
t.Error("Premium should cost more than Standard")
}
}
func TestBillingPlan_FairUseModeOnlyForPremium(t *testing.T) {
plans := models.GetDefaultPlans()
for _, plan := range plans {
if plan.ID == models.PlanPremium {
if !plan.Features.FairUseMode {
t.Error("Premium plan should have FairUseMode enabled")
}
} else {
if plan.Features.FairUseMode {
t.Errorf("Plan '%s' should not have FairUseMode enabled", plan.ID)
}
}
}
}
func TestBillingPlan_MaxTaskBalanceCalculation(t *testing.T) {
plans := models.GetDefaultPlans()
for _, plan := range plans {
expected := plan.Features.MonthlyTaskAllowance * models.CarryoverMonthsCap
if plan.Features.MaxTaskBalance != expected {
t.Errorf("Plan '%s' MaxTaskBalance should be %d (allowance * 5), got %d",
plan.ID, expected, plan.Features.MaxTaskBalance)
}
}
}
func TestAuditLogJSON_Marshaling(t *testing.T) {
// Test that audit log values can be properly serialized
oldValue := map[string]interface{}{
"plan_id": "basic",
"status": "active",
}
newValue := map[string]interface{}{
"plan_id": "standard",
"status": "active",
}
metadata := map[string]interface{}{
"reason": "upgrade",
}
// Marshal all values
oldJSON, err := json.Marshal(oldValue)
if err != nil {
t.Fatalf("Failed to marshal oldValue: %v", err)
}
newJSON, err := json.Marshal(newValue)
if err != nil {
t.Fatalf("Failed to marshal newValue: %v", err)
}
metaJSON, err := json.Marshal(metadata)
if err != nil {
t.Fatalf("Failed to marshal metadata: %v", err)
}
// Verify non-empty
if len(oldJSON) == 0 || len(newJSON) == 0 || len(metaJSON) == 0 {
t.Error("JSON outputs should not be empty")
}
}
func TestSubscriptionTrialCalculation(t *testing.T) {
// Test trial days calculation logic
trialDays := 7
if trialDays <= 0 {
t.Error("Trial days should be positive")
}
if trialDays > 30 {
t.Error("Trial days should not exceed 30")
}
}
func TestSubscriptionInfo_TrialingStatus(t *testing.T) {
info := models.SubscriptionInfo{
PlanID: models.PlanBasic,
PlanName: "Basic",
Status: models.StatusTrialing,
IsTrialing: true,
TrialDaysLeft: 5,
CancelAtPeriodEnd: false,
PriceCents: 990,
Currency: "eur",
}
if !info.IsTrialing {
t.Error("Should be trialing")
}
if info.Status != models.StatusTrialing {
t.Errorf("Status should be 'trialing', got '%s'", info.Status)
}
if info.TrialDaysLeft <= 0 {
t.Error("TrialDaysLeft should be positive during trial")
}
}
func TestSubscriptionInfo_ActiveStatus(t *testing.T) {
info := models.SubscriptionInfo{
PlanID: models.PlanStandard,
PlanName: "Standard",
Status: models.StatusActive,
IsTrialing: false,
TrialDaysLeft: 0,
CancelAtPeriodEnd: false,
PriceCents: 1990,
Currency: "eur",
}
if info.IsTrialing {
t.Error("Should not be trialing")
}
if info.Status != models.StatusActive {
t.Errorf("Status should be 'active', got '%s'", info.Status)
}
}
func TestSubscriptionInfo_CanceledStatus(t *testing.T) {
info := models.SubscriptionInfo{
PlanID: models.PlanStandard,
PlanName: "Standard",
Status: models.StatusActive,
IsTrialing: false,
CancelAtPeriodEnd: true, // Scheduled for cancellation
PriceCents: 1990,
Currency: "eur",
}
if !info.CancelAtPeriodEnd {
t.Error("CancelAtPeriodEnd should be true")
}
// Status remains active until period end
if info.Status != models.StatusActive {
t.Errorf("Status should still be 'active', got '%s'", info.Status)
}
}
func TestWebhookEventTypes(t *testing.T) {
// Test common Stripe webhook event types we handle
eventTypes := []string{
"checkout.session.completed",
"customer.subscription.created",
"customer.subscription.updated",
"customer.subscription.deleted",
"invoice.paid",
"invoice.payment_failed",
}
for _, eventType := range eventTypes {
if eventType == "" {
t.Error("Event type should not be empty")
}
}
}
func TestIdempotencyKey_Format(t *testing.T) {
// Test that we can handle Stripe event IDs
sampleEventIDs := []string{
"evt_1234567890abcdef",
"evt_test_abc123xyz789",
"evt_live_real_event_id",
}
for _, eventID := range sampleEventIDs {
if len(eventID) < 10 {
t.Errorf("Event ID '%s' seems too short", eventID)
}
// Stripe event IDs typically start with "evt_"
if eventID[:4] != "evt_" {
t.Errorf("Event ID '%s' should start with 'evt_'", eventID)
}
}
}