Initial commit: breakpilot-core - Shared Infrastructure
Docker Compose with 24+ services: - PostgreSQL (PostGIS), Valkey, MinIO, Qdrant - Vault (PKI/TLS), Nginx (Reverse Proxy) - Backend Core API, Consent Service, Billing Service - RAG Service, Embedding Service - Gitea, Woodpecker CI/CD - Night Scheduler, Health Aggregator - Jitsi (Web/XMPP/JVB/Jicofo), Mailpit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
372
billing-service/internal/models/models.go
Normal file
372
billing-service/internal/models/models.go
Normal file
@@ -0,0 +1,372 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// SubscriptionStatus represents the status of a subscription
|
||||
type SubscriptionStatus string
|
||||
|
||||
const (
|
||||
StatusTrialing SubscriptionStatus = "trialing"
|
||||
StatusActive SubscriptionStatus = "active"
|
||||
StatusPastDue SubscriptionStatus = "past_due"
|
||||
StatusCanceled SubscriptionStatus = "canceled"
|
||||
StatusExpired SubscriptionStatus = "expired"
|
||||
)
|
||||
|
||||
// PlanID represents the available plan IDs
|
||||
type PlanID string
|
||||
|
||||
const (
|
||||
PlanBasic PlanID = "basic"
|
||||
PlanStandard PlanID = "standard"
|
||||
PlanPremium PlanID = "premium"
|
||||
)
|
||||
|
||||
// TaskType represents the type of task
|
||||
type TaskType string
|
||||
|
||||
const (
|
||||
TaskTypeCorrection TaskType = "correction"
|
||||
TaskTypeLetter TaskType = "letter"
|
||||
TaskTypeMeeting TaskType = "meeting"
|
||||
TaskTypeBatch TaskType = "batch"
|
||||
TaskTypeOther TaskType = "other"
|
||||
)
|
||||
|
||||
// CarryoverMonthsCap is the maximum number of months tasks can accumulate
|
||||
const CarryoverMonthsCap = 5
|
||||
|
||||
// Subscription represents a user's subscription
|
||||
type Subscription struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
StripeCustomerID string `json:"stripe_customer_id"`
|
||||
StripeSubscriptionID string `json:"stripe_subscription_id"`
|
||||
PlanID PlanID `json:"plan_id"`
|
||||
Status SubscriptionStatus `json:"status"`
|
||||
TrialEnd *time.Time `json:"trial_end,omitempty"`
|
||||
CurrentPeriodEnd *time.Time `json:"current_period_end,omitempty"`
|
||||
CancelAtPeriodEnd bool `json:"cancel_at_period_end"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BillingPlan represents a billing plan with its features and limits
|
||||
type BillingPlan struct {
|
||||
ID PlanID `json:"id"`
|
||||
StripePriceID string `json:"stripe_price_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
PriceCents int `json:"price_cents"` // Price in cents (990 = 9.90 EUR)
|
||||
Currency string `json:"currency"`
|
||||
Interval string `json:"interval"` // "month" or "year"
|
||||
Features PlanFeatures `json:"features"`
|
||||
IsActive bool `json:"is_active"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// PlanFeatures represents the features and limits of a plan
|
||||
type PlanFeatures struct {
|
||||
// Task-based limits (primary billing unit)
|
||||
MonthlyTaskAllowance int `json:"monthly_task_allowance"` // Tasks per month
|
||||
MaxTaskBalance int `json:"max_task_balance"` // Max accumulated tasks (allowance * CarryoverMonthsCap)
|
||||
|
||||
// Legacy fields for backward compatibility (deprecated, use task-based limits)
|
||||
AIRequestsLimit int `json:"ai_requests_limit,omitempty"`
|
||||
DocumentsLimit int `json:"documents_limit,omitempty"`
|
||||
|
||||
// Feature flags
|
||||
FeatureFlags []string `json:"feature_flags"`
|
||||
MaxTeamMembers int `json:"max_team_members,omitempty"`
|
||||
PrioritySupport bool `json:"priority_support"`
|
||||
CustomBranding bool `json:"custom_branding"`
|
||||
BatchProcessing bool `json:"batch_processing"`
|
||||
CustomTemplates bool `json:"custom_templates"`
|
||||
|
||||
// Premium: Fair Use (no visible limit)
|
||||
FairUseMode bool `json:"fair_use_mode"`
|
||||
}
|
||||
|
||||
// Task represents a single task that consumes 1 unit from the balance
|
||||
type Task struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
AccountID uuid.UUID `json:"account_id"`
|
||||
TaskType TaskType `json:"task_type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Consumed bool `json:"consumed"` // Always true when created
|
||||
// Internal metrics (not shown to user)
|
||||
PageCount int `json:"-"`
|
||||
TokenCount int `json:"-"`
|
||||
ProcessTime int `json:"-"` // in seconds
|
||||
}
|
||||
|
||||
// AccountUsage represents the task-based usage for an account
|
||||
type AccountUsage struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
AccountID uuid.UUID `json:"account_id"`
|
||||
PlanID PlanID `json:"plan"`
|
||||
MonthlyTaskAllowance int `json:"monthly_task_allowance"`
|
||||
CarryoverMonthsCap int `json:"carryover_months_cap"` // Always 5
|
||||
MaxTaskBalance int `json:"max_task_balance"` // allowance * cap
|
||||
TaskBalance int `json:"task_balance"` // Current available tasks
|
||||
LastRenewalAt time.Time `json:"last_renewal_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// UsageSummary tracks usage for a specific period (internal metrics)
|
||||
type UsageSummary struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
UsageType string `json:"usage_type"` // "task", "page", "token"
|
||||
PeriodStart time.Time `json:"period_start"`
|
||||
TotalCount int `json:"total_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// UserEntitlements represents cached entitlements for a user
|
||||
type UserEntitlements struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
PlanID PlanID `json:"plan_id"`
|
||||
TaskBalance int `json:"task_balance"`
|
||||
MaxBalance int `json:"max_balance"`
|
||||
Features PlanFeatures `json:"features"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// Legacy fields for backward compatibility with old entitlement service
|
||||
AIRequestsLimit int `json:"ai_requests_limit"`
|
||||
AIRequestsUsed int `json:"ai_requests_used"`
|
||||
DocumentsLimit int `json:"documents_limit"`
|
||||
DocumentsUsed int `json:"documents_used"`
|
||||
}
|
||||
|
||||
// StripeWebhookEvent tracks processed webhook events for idempotency
|
||||
type StripeWebhookEvent struct {
|
||||
StripeEventID string `json:"stripe_event_id"`
|
||||
EventType string `json:"event_type"`
|
||||
Processed bool `json:"processed"`
|
||||
ProcessedAt time.Time `json:"processed_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// BillingStatusResponse is the response for the billing status endpoint
|
||||
type BillingStatusResponse struct {
|
||||
HasSubscription bool `json:"has_subscription"`
|
||||
Subscription *SubscriptionInfo `json:"subscription,omitempty"`
|
||||
TaskUsage *TaskUsageInfo `json:"task_usage,omitempty"`
|
||||
Entitlements *EntitlementInfo `json:"entitlements,omitempty"`
|
||||
AvailablePlans []BillingPlan `json:"available_plans,omitempty"`
|
||||
}
|
||||
|
||||
// SubscriptionInfo contains subscription details for the response
|
||||
type SubscriptionInfo struct {
|
||||
PlanID PlanID `json:"plan_id"`
|
||||
PlanName string `json:"plan_name"`
|
||||
Status SubscriptionStatus `json:"status"`
|
||||
IsTrialing bool `json:"is_trialing"`
|
||||
TrialDaysLeft int `json:"trial_days_left,omitempty"`
|
||||
CurrentPeriodEnd *time.Time `json:"current_period_end,omitempty"`
|
||||
CancelAtPeriodEnd bool `json:"cancel_at_period_end"`
|
||||
PriceCents int `json:"price_cents"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// TaskUsageInfo contains current task usage information
|
||||
// This is the ONLY usage info shown to users
|
||||
type TaskUsageInfo struct {
|
||||
TasksAvailable int `json:"tasks_available"` // Current balance
|
||||
MaxTasks int `json:"max_tasks"` // Max possible balance
|
||||
InfoText string `json:"info_text"` // "Aufgaben verfuegbar: X von max. Y"
|
||||
TooltipText string `json:"tooltip_text"` // "Aufgaben koennen sich bis zu 5 Monate ansammeln."
|
||||
}
|
||||
|
||||
// EntitlementInfo contains feature entitlements
|
||||
type EntitlementInfo struct {
|
||||
Features []string `json:"features"`
|
||||
MaxTeamMembers int `json:"max_team_members,omitempty"`
|
||||
PrioritySupport bool `json:"priority_support"`
|
||||
CustomBranding bool `json:"custom_branding"`
|
||||
BatchProcessing bool `json:"batch_processing"`
|
||||
CustomTemplates bool `json:"custom_templates"`
|
||||
FairUseMode bool `json:"fair_use_mode"` // Premium only
|
||||
}
|
||||
|
||||
// StartTrialRequest is the request to start a trial
|
||||
type StartTrialRequest struct {
|
||||
PlanID PlanID `json:"plan_id" binding:"required"`
|
||||
}
|
||||
|
||||
// StartTrialResponse is the response after starting a trial
|
||||
type StartTrialResponse struct {
|
||||
CheckoutURL string `json:"checkout_url"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
// ChangePlanRequest is the request to change plans
|
||||
type ChangePlanRequest struct {
|
||||
NewPlanID PlanID `json:"new_plan_id" binding:"required"`
|
||||
}
|
||||
|
||||
// ChangePlanResponse is the response after changing plans
|
||||
type ChangePlanResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
EffectiveDate string `json:"effective_date,omitempty"`
|
||||
}
|
||||
|
||||
// CancelSubscriptionResponse is the response after canceling
|
||||
type CancelSubscriptionResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
CancelDate string `json:"cancel_date"`
|
||||
ActiveUntil string `json:"active_until"`
|
||||
}
|
||||
|
||||
// CustomerPortalResponse contains the portal URL
|
||||
type CustomerPortalResponse struct {
|
||||
PortalURL string `json:"portal_url"`
|
||||
}
|
||||
|
||||
// ConsumeTaskRequest is the request to consume a task (internal)
|
||||
type ConsumeTaskRequest struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
TaskType TaskType `json:"task_type" binding:"required"`
|
||||
}
|
||||
|
||||
// ConsumeTaskResponse is the response after consuming a task
|
||||
type ConsumeTaskResponse struct {
|
||||
Success bool `json:"success"`
|
||||
TaskID string `json:"task_id,omitempty"`
|
||||
TasksRemaining int `json:"tasks_remaining"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// CheckTaskAllowedResponse is the response for task limit checks
|
||||
type CheckTaskAllowedResponse struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
TasksAvailable int `json:"tasks_available"`
|
||||
MaxTasks int `json:"max_tasks"`
|
||||
PlanID PlanID `json:"plan_id"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// EntitlementCheckResponse is the response for entitlement checks (internal)
|
||||
type EntitlementCheckResponse struct {
|
||||
HasEntitlement bool `json:"has_entitlement"`
|
||||
PlanID PlanID `json:"plan_id,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// TaskLimitError represents the error when task limit is reached
|
||||
type TaskLimitError struct {
|
||||
Error string `json:"error"`
|
||||
CurrentBalance int `json:"current_balance"`
|
||||
Plan PlanID `json:"plan"`
|
||||
}
|
||||
|
||||
// UsageInfo represents current usage information (legacy, prefer TaskUsageInfo)
|
||||
type UsageInfo struct {
|
||||
AIRequestsUsed int `json:"ai_requests_used"`
|
||||
AIRequestsLimit int `json:"ai_requests_limit"`
|
||||
AIRequestsPercent float64 `json:"ai_requests_percent"`
|
||||
DocumentsUsed int `json:"documents_used"`
|
||||
DocumentsLimit int `json:"documents_limit"`
|
||||
DocumentsPercent float64 `json:"documents_percent"`
|
||||
PeriodStart string `json:"period_start"`
|
||||
PeriodEnd string `json:"period_end"`
|
||||
}
|
||||
|
||||
// CheckUsageResponse is the response for legacy usage checks
|
||||
type CheckUsageResponse struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
CurrentUsage int `json:"current_usage"`
|
||||
Limit int `json:"limit"`
|
||||
Remaining int `json:"remaining"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// TrackUsageRequest is the request to track usage (internal)
|
||||
type TrackUsageRequest struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
UsageType string `json:"usage_type" binding:"required"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
|
||||
// GetDefaultPlans returns the default billing plans with task-based limits
|
||||
func GetDefaultPlans() []BillingPlan {
|
||||
return []BillingPlan{
|
||||
{
|
||||
ID: PlanBasic,
|
||||
Name: "Basic",
|
||||
Description: "Perfekt fuer den Einstieg - Gelegentliche Nutzung",
|
||||
PriceCents: 990, // 9.90 EUR
|
||||
Currency: "eur",
|
||||
Interval: "month",
|
||||
Features: PlanFeatures{
|
||||
MonthlyTaskAllowance: 30, // 30 tasks/month
|
||||
MaxTaskBalance: 30 * CarryoverMonthsCap, // 150 max
|
||||
FeatureFlags: []string{"basic_ai", "basic_documents"},
|
||||
MaxTeamMembers: 1,
|
||||
PrioritySupport: false,
|
||||
CustomBranding: false,
|
||||
BatchProcessing: false,
|
||||
CustomTemplates: false,
|
||||
FairUseMode: false,
|
||||
},
|
||||
IsActive: true,
|
||||
SortOrder: 1,
|
||||
},
|
||||
{
|
||||
ID: PlanStandard,
|
||||
Name: "Standard",
|
||||
Description: "Fuer regelmaessige Nutzer - Mehrere Klassen und regelmaessige Korrekturen",
|
||||
PriceCents: 1990, // 19.90 EUR
|
||||
Currency: "eur",
|
||||
Interval: "month",
|
||||
Features: PlanFeatures{
|
||||
MonthlyTaskAllowance: 100, // 100 tasks/month
|
||||
MaxTaskBalance: 100 * CarryoverMonthsCap, // 500 max
|
||||
FeatureFlags: []string{"basic_ai", "basic_documents", "templates", "batch_processing"},
|
||||
MaxTeamMembers: 3,
|
||||
PrioritySupport: false,
|
||||
CustomBranding: false,
|
||||
BatchProcessing: true,
|
||||
CustomTemplates: true,
|
||||
FairUseMode: false,
|
||||
},
|
||||
IsActive: true,
|
||||
SortOrder: 2,
|
||||
},
|
||||
{
|
||||
ID: PlanPremium,
|
||||
Name: "Premium",
|
||||
Description: "Sorglos-Tarif - Vielnutzer, Teams, schulischer Kontext",
|
||||
PriceCents: 3990, // 39.90 EUR
|
||||
Currency: "eur",
|
||||
Interval: "month",
|
||||
Features: PlanFeatures{
|
||||
MonthlyTaskAllowance: 1000, // Very high (Fair Use)
|
||||
MaxTaskBalance: 1000 * CarryoverMonthsCap, // 5000 max (not shown to user)
|
||||
FeatureFlags: []string{"basic_ai", "basic_documents", "templates", "batch_processing", "team_features", "admin_panel", "audit_log", "api_access"},
|
||||
MaxTeamMembers: 10,
|
||||
PrioritySupport: true,
|
||||
CustomBranding: true,
|
||||
BatchProcessing: true,
|
||||
CustomTemplates: true,
|
||||
FairUseMode: true, // No visible limit
|
||||
},
|
||||
IsActive: true,
|
||||
SortOrder: 3,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateMaxTaskBalance calculates max task balance from monthly allowance
|
||||
func CalculateMaxTaskBalance(monthlyAllowance int) int {
|
||||
return monthlyAllowance * CarryoverMonthsCap
|
||||
}
|
||||
319
billing-service/internal/models/models_test.go
Normal file
319
billing-service/internal/models/models_test.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCarryoverMonthsCap(t *testing.T) {
|
||||
// Verify the constant is set correctly
|
||||
if CarryoverMonthsCap != 5 {
|
||||
t.Errorf("CarryoverMonthsCap should be 5, got %d", CarryoverMonthsCap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateMaxTaskBalance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
monthlyAllowance int
|
||||
expected int
|
||||
}{
|
||||
{"Basic plan", 30, 150},
|
||||
{"Standard plan", 100, 500},
|
||||
{"Premium plan", 1000, 5000},
|
||||
{"Zero allowance", 0, 0},
|
||||
{"Single task", 1, 5},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := CalculateMaxTaskBalance(tt.monthlyAllowance)
|
||||
if result != tt.expected {
|
||||
t.Errorf("CalculateMaxTaskBalance(%d) = %d, expected %d",
|
||||
tt.monthlyAllowance, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultPlans(t *testing.T) {
|
||||
plans := GetDefaultPlans()
|
||||
|
||||
if len(plans) != 3 {
|
||||
t.Fatalf("Expected 3 plans, got %d", len(plans))
|
||||
}
|
||||
|
||||
// Test Basic plan
|
||||
basic := plans[0]
|
||||
if basic.ID != PlanBasic {
|
||||
t.Errorf("First plan should be Basic, got %s", basic.ID)
|
||||
}
|
||||
if basic.PriceCents != 990 {
|
||||
t.Errorf("Basic price should be 990 cents, got %d", basic.PriceCents)
|
||||
}
|
||||
if basic.Features.MonthlyTaskAllowance != 30 {
|
||||
t.Errorf("Basic monthly allowance should be 30, got %d", basic.Features.MonthlyTaskAllowance)
|
||||
}
|
||||
if basic.Features.MaxTaskBalance != 150 {
|
||||
t.Errorf("Basic max balance should be 150, got %d", basic.Features.MaxTaskBalance)
|
||||
}
|
||||
if basic.Features.FairUseMode {
|
||||
t.Error("Basic should not have FairUseMode")
|
||||
}
|
||||
|
||||
// Test Standard plan
|
||||
standard := plans[1]
|
||||
if standard.ID != PlanStandard {
|
||||
t.Errorf("Second plan should be Standard, got %s", standard.ID)
|
||||
}
|
||||
if standard.PriceCents != 1990 {
|
||||
t.Errorf("Standard price should be 1990 cents, got %d", standard.PriceCents)
|
||||
}
|
||||
if standard.Features.MonthlyTaskAllowance != 100 {
|
||||
t.Errorf("Standard monthly allowance should be 100, got %d", standard.Features.MonthlyTaskAllowance)
|
||||
}
|
||||
if !standard.Features.BatchProcessing {
|
||||
t.Error("Standard should have BatchProcessing")
|
||||
}
|
||||
if !standard.Features.CustomTemplates {
|
||||
t.Error("Standard should have CustomTemplates")
|
||||
}
|
||||
|
||||
// Test Premium plan
|
||||
premium := plans[2]
|
||||
if premium.ID != PlanPremium {
|
||||
t.Errorf("Third plan should be Premium, got %s", premium.ID)
|
||||
}
|
||||
if premium.PriceCents != 3990 {
|
||||
t.Errorf("Premium price should be 3990 cents, got %d", premium.PriceCents)
|
||||
}
|
||||
if !premium.Features.FairUseMode {
|
||||
t.Error("Premium should have FairUseMode")
|
||||
}
|
||||
if !premium.Features.PrioritySupport {
|
||||
t.Error("Premium should have PrioritySupport")
|
||||
}
|
||||
if !premium.Features.CustomBranding {
|
||||
t.Error("Premium should have CustomBranding")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanIDConstants(t *testing.T) {
|
||||
if PlanBasic != "basic" {
|
||||
t.Errorf("PlanBasic should be 'basic', got '%s'", PlanBasic)
|
||||
}
|
||||
if PlanStandard != "standard" {
|
||||
t.Errorf("PlanStandard should be 'standard', got '%s'", PlanStandard)
|
||||
}
|
||||
if PlanPremium != "premium" {
|
||||
t.Errorf("PlanPremium should be 'premium', got '%s'", PlanPremium)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionStatusConstants(t *testing.T) {
|
||||
statuses := []struct {
|
||||
status SubscriptionStatus
|
||||
expected string
|
||||
}{
|
||||
{StatusTrialing, "trialing"},
|
||||
{StatusActive, "active"},
|
||||
{StatusPastDue, "past_due"},
|
||||
{StatusCanceled, "canceled"},
|
||||
{StatusExpired, "expired"},
|
||||
}
|
||||
|
||||
for _, tt := range statuses {
|
||||
if string(tt.status) != tt.expected {
|
||||
t.Errorf("Status %s should be '%s'", tt.status, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskTypeConstants(t *testing.T) {
|
||||
types := []struct {
|
||||
taskType TaskType
|
||||
expected string
|
||||
}{
|
||||
{TaskTypeCorrection, "correction"},
|
||||
{TaskTypeLetter, "letter"},
|
||||
{TaskTypeMeeting, "meeting"},
|
||||
{TaskTypeBatch, "batch"},
|
||||
{TaskTypeOther, "other"},
|
||||
}
|
||||
|
||||
for _, tt := range types {
|
||||
if string(tt.taskType) != tt.expected {
|
||||
t.Errorf("TaskType %s should be '%s'", tt.taskType, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanFeatures_CarryoverCalculation(t *testing.T) {
|
||||
plans := GetDefaultPlans()
|
||||
|
||||
for _, plan := range plans {
|
||||
expectedMax := plan.Features.MonthlyTaskAllowance * CarryoverMonthsCap
|
||||
if plan.Features.MaxTaskBalance != expectedMax {
|
||||
t.Errorf("Plan %s: MaxTaskBalance should be %d (allowance * 5), got %d",
|
||||
plan.ID, expectedMax, plan.Features.MaxTaskBalance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingPlan_AllPlansActive(t *testing.T) {
|
||||
plans := GetDefaultPlans()
|
||||
|
||||
for _, plan := range plans {
|
||||
if !plan.IsActive {
|
||||
t.Errorf("Plan %s should be active", plan.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingPlan_CurrencyIsEuro(t *testing.T) {
|
||||
plans := GetDefaultPlans()
|
||||
|
||||
for _, plan := range plans {
|
||||
if plan.Currency != "eur" {
|
||||
t.Errorf("Plan %s currency should be 'eur', got '%s'", plan.ID, plan.Currency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingPlan_IntervalIsMonth(t *testing.T) {
|
||||
plans := GetDefaultPlans()
|
||||
|
||||
for _, plan := range plans {
|
||||
if plan.Interval != "month" {
|
||||
t.Errorf("Plan %s interval should be 'month', got '%s'", plan.ID, plan.Interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingPlan_SortOrder(t *testing.T) {
|
||||
plans := GetDefaultPlans()
|
||||
|
||||
for i, plan := range plans {
|
||||
expectedOrder := i + 1
|
||||
if plan.SortOrder != expectedOrder {
|
||||
t.Errorf("Plan %s sort order should be %d, got %d",
|
||||
plan.ID, expectedOrder, plan.SortOrder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskUsageInfo_FormatStrings(t *testing.T) {
|
||||
usage := TaskUsageInfo{
|
||||
TasksAvailable: 45,
|
||||
MaxTasks: 150,
|
||||
InfoText: "Aufgaben verfuegbar: 45 von max. 150",
|
||||
TooltipText: "Aufgaben koennen sich bis zu 5 Monate ansammeln.",
|
||||
}
|
||||
|
||||
if usage.TasksAvailable != 45 {
|
||||
t.Errorf("TasksAvailable should be 45, got %d", usage.TasksAvailable)
|
||||
}
|
||||
if usage.MaxTasks != 150 {
|
||||
t.Errorf("MaxTasks should be 150, got %d", usage.MaxTasks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckTaskAllowedResponse_Allowed(t *testing.T) {
|
||||
response := CheckTaskAllowedResponse{
|
||||
Allowed: true,
|
||||
TasksAvailable: 50,
|
||||
MaxTasks: 150,
|
||||
PlanID: PlanBasic,
|
||||
}
|
||||
|
||||
if !response.Allowed {
|
||||
t.Error("Response should be allowed")
|
||||
}
|
||||
if response.Message != "" {
|
||||
t.Errorf("Message should be empty for allowed response, got '%s'", response.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckTaskAllowedResponse_NotAllowed(t *testing.T) {
|
||||
response := CheckTaskAllowedResponse{
|
||||
Allowed: false,
|
||||
TasksAvailable: 0,
|
||||
MaxTasks: 150,
|
||||
PlanID: PlanBasic,
|
||||
Message: "Dein Aufgaben-Kontingent ist aufgebraucht.",
|
||||
}
|
||||
|
||||
if response.Allowed {
|
||||
t.Error("Response should not be allowed")
|
||||
}
|
||||
if response.TasksAvailable != 0 {
|
||||
t.Errorf("TasksAvailable should be 0, got %d", response.TasksAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskLimitError(t *testing.T) {
|
||||
err := TaskLimitError{
|
||||
Error: "TASK_LIMIT_REACHED",
|
||||
CurrentBalance: 0,
|
||||
Plan: PlanBasic,
|
||||
}
|
||||
|
||||
if err.Error != "TASK_LIMIT_REACHED" {
|
||||
t.Errorf("Error should be 'TASK_LIMIT_REACHED', got '%s'", err.Error)
|
||||
}
|
||||
if err.CurrentBalance != 0 {
|
||||
t.Errorf("CurrentBalance should be 0, got %d", err.CurrentBalance)
|
||||
}
|
||||
if err.Plan != PlanBasic {
|
||||
t.Errorf("Plan should be basic, got '%s'", err.Plan)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumeTaskRequest(t *testing.T) {
|
||||
req := ConsumeTaskRequest{
|
||||
UserID: "550e8400-e29b-41d4-a716-446655440000",
|
||||
TaskType: TaskTypeCorrection,
|
||||
}
|
||||
|
||||
if req.UserID == "" {
|
||||
t.Error("UserID should not be empty")
|
||||
}
|
||||
if req.TaskType != TaskTypeCorrection {
|
||||
t.Errorf("TaskType should be correction, got '%s'", req.TaskType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumeTaskResponse_Success(t *testing.T) {
|
||||
resp := ConsumeTaskResponse{
|
||||
Success: true,
|
||||
TaskID: "task-123",
|
||||
TasksRemaining: 49,
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
t.Error("Response should be successful")
|
||||
}
|
||||
if resp.TasksRemaining != 49 {
|
||||
t.Errorf("TasksRemaining should be 49, got %d", resp.TasksRemaining)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitlementInfo_Premium(t *testing.T) {
|
||||
premium := GetDefaultPlans()[2]
|
||||
|
||||
info := EntitlementInfo{
|
||||
Features: premium.Features.FeatureFlags,
|
||||
MaxTeamMembers: premium.Features.MaxTeamMembers,
|
||||
PrioritySupport: premium.Features.PrioritySupport,
|
||||
CustomBranding: premium.Features.CustomBranding,
|
||||
BatchProcessing: premium.Features.BatchProcessing,
|
||||
CustomTemplates: premium.Features.CustomTemplates,
|
||||
FairUseMode: premium.Features.FairUseMode,
|
||||
}
|
||||
|
||||
if !info.FairUseMode {
|
||||
t.Error("Premium should have FairUseMode")
|
||||
}
|
||||
if info.MaxTeamMembers != 10 {
|
||||
t.Errorf("Premium MaxTeamMembers should be 10, got %d", info.MaxTeamMembers)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user