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>
373 lines
14 KiB
Go
373 lines
14 KiB
Go
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
|
|
}
|