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/usage_service.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

195 lines
5.4 KiB
Go

package services
import (
"context"
"fmt"
"time"
"github.com/breakpilot/billing-service/internal/database"
"github.com/breakpilot/billing-service/internal/models"
"github.com/google/uuid"
)
// UsageService handles usage tracking operations
type UsageService struct {
db *database.DB
entitlementService *EntitlementService
}
// NewUsageService creates a new UsageService
func NewUsageService(db *database.DB, entitlementService *EntitlementService) *UsageService {
return &UsageService{
db: db,
entitlementService: entitlementService,
}
}
// TrackUsage tracks usage for a user
func (s *UsageService) TrackUsage(ctx context.Context, userIDStr, usageType string, quantity int) error {
userID, err := uuid.Parse(userIDStr)
if err != nil {
return fmt.Errorf("invalid user ID: %w", err)
}
// Get current period start (beginning of current month)
now := time.Now()
periodStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
// Upsert usage summary
query := `
INSERT INTO usage_summary (user_id, usage_type, period_start, total_count)
VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id, usage_type, period_start) DO UPDATE SET
total_count = usage_summary.total_count + EXCLUDED.total_count,
updated_at = NOW()
`
_, err = s.db.Pool.Exec(ctx, query, userID, usageType, periodStart, quantity)
if err != nil {
return fmt.Errorf("failed to track usage: %w", err)
}
// Also update entitlements cache
return s.entitlementService.IncrementUsage(ctx, userID, usageType, quantity)
}
// GetUsageSummary returns usage summary for a user
func (s *UsageService) GetUsageSummary(ctx context.Context, userID uuid.UUID) (*models.UsageInfo, error) {
// Get entitlements (which include current usage)
ent, err := s.entitlementService.getUserEntitlements(ctx, userID)
if err != nil || ent == nil {
return nil, err
}
// Calculate percentages
aiPercent := 0.0
if ent.AIRequestsLimit > 0 {
aiPercent = float64(ent.AIRequestsUsed) / float64(ent.AIRequestsLimit) * 100
}
docPercent := 0.0
if ent.DocumentsLimit > 0 {
docPercent = float64(ent.DocumentsUsed) / float64(ent.DocumentsLimit) * 100
}
// Get period dates
now := time.Now()
periodStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
periodEnd := periodStart.AddDate(0, 1, 0).Add(-time.Second)
return &models.UsageInfo{
AIRequestsUsed: ent.AIRequestsUsed,
AIRequestsLimit: ent.AIRequestsLimit,
AIRequestsPercent: aiPercent,
DocumentsUsed: ent.DocumentsUsed,
DocumentsLimit: ent.DocumentsLimit,
DocumentsPercent: docPercent,
PeriodStart: periodStart.Format("2006-01-02"),
PeriodEnd: periodEnd.Format("2006-01-02"),
}, nil
}
// CheckUsageAllowed checks if a user is allowed to perform a usage action
func (s *UsageService) CheckUsageAllowed(ctx context.Context, userIDStr, usageType string) (*models.CheckUsageResponse, error) {
userID, err := uuid.Parse(userIDStr)
if err != nil {
return &models.CheckUsageResponse{
Allowed: false,
Message: "Invalid user ID",
}, nil
}
// Get entitlements
ent, err := s.entitlementService.getUserEntitlements(ctx, userID)
if err != nil {
return &models.CheckUsageResponse{
Allowed: false,
Message: "Failed to get entitlements",
}, nil
}
if ent == nil {
return &models.CheckUsageResponse{
Allowed: false,
Message: "No subscription found",
}, nil
}
var currentUsage, limit int
switch usageType {
case "ai_request":
currentUsage = ent.AIRequestsUsed
limit = ent.AIRequestsLimit
case "document_created":
currentUsage = ent.DocumentsUsed
limit = ent.DocumentsLimit
default:
return &models.CheckUsageResponse{
Allowed: true,
Message: "Unknown usage type - allowing",
}, nil
}
remaining := limit - currentUsage
allowed := remaining > 0
response := &models.CheckUsageResponse{
Allowed: allowed,
CurrentUsage: currentUsage,
Limit: limit,
Remaining: remaining,
}
if !allowed {
response.Message = fmt.Sprintf("Usage limit reached for %s (%d/%d)", usageType, currentUsage, limit)
}
return response, nil
}
// GetUsageHistory returns usage history for a user
func (s *UsageService) GetUsageHistory(ctx context.Context, userID uuid.UUID, months int) ([]models.UsageSummary, error) {
query := `
SELECT id, user_id, usage_type, period_start, total_count, created_at, updated_at
FROM usage_summary
WHERE user_id = $1
AND period_start >= $2
ORDER BY period_start DESC, usage_type
`
// Calculate start date
startDate := time.Now().AddDate(0, -months, 0)
startDate = time.Date(startDate.Year(), startDate.Month(), 1, 0, 0, 0, 0, time.UTC)
rows, err := s.db.Pool.Query(ctx, query, userID, startDate)
if err != nil {
return nil, err
}
defer rows.Close()
var summaries []models.UsageSummary
for rows.Next() {
var summary models.UsageSummary
err := rows.Scan(
&summary.ID, &summary.UserID, &summary.UsageType,
&summary.PeriodStart, &summary.TotalCount,
&summary.CreatedAt, &summary.UpdatedAt,
)
if err != nil {
return nil, err
}
summaries = append(summaries, summary)
}
return summaries, nil
}
// ResetPeriodUsage resets usage for a new billing period
func (s *UsageService) ResetPeriodUsage(ctx context.Context, userID uuid.UUID) error {
now := time.Now()
newPeriodStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
newPeriodEnd := newPeriodStart.AddDate(0, 1, 0).Add(-time.Second)
return s.entitlementService.ResetUsageCounters(ctx, userID, &newPeriodStart, &newPeriodEnd)
}