package usecase import ( "context" "fmt" "apps/backend/internal/module/usage/domain" "github.com/google/uuid" ) // KeyResolver decides platform vs byok for a meter. type KeyResolver interface { // Resolve returns key_mode and whether a usable key exists. Resolve(ctx context.Context, uid int64, meter string) (keyMode string, hasKey bool, err error) } // StaticResolver for tests / simple wiring. type StaticResolver struct { // ByUIDMeter: "uid:meter" -> keyMode (platform|byok); empty means no key Map map[string]string } func (s *StaticResolver) Resolve(_ context.Context, uid int64, meter string) (string, bool, error) { if s == nil || s.Map == nil { return "", false, nil } k := s.Map[fmt.Sprintf("%d:%s", uid, meter)] if k == "" { return "", false, nil } return k, true, nil } type Service struct { Repo domain.Repository Resolver KeyResolver } func New(repo domain.Repository, resolver KeyResolver) *Service { return &Service{Repo: repo, Resolver: resolver} } // RecordCall after successful external call (or for meter tests). func (s *Service) RecordCall(ctx context.Context, uid int64, meter, keyMode, label, source string) (*domain.Event, error) { if keyMode != domain.KeyModePlatform && keyMode != domain.KeyModeByok { return nil, fmt.Errorf("invalid key_mode") } credits := 0 if keyMode == domain.KeyModePlatform { credits = domain.DefaultCreditCost[meter] if credits == 0 { credits = 1 } prefs, _ := s.ensurePrefs(ctx, uid) if !prefs.Unlimited { sum, err := s.GetSummary(ctx, uid, domain.CurrentMonthKey()) if err != nil { return nil, err } if sum.Platform.CreditsRemaining < credits { return nil, domain.ErrQuotaExceeded } } } now := domain.NowNano() e := &domain.Event{ ID: uuid.NewString(), UID: uid, Meter: meter, Credits: credits, KeyMode: keyMode, Label: label, Source: source, MonthKey: domain.MonthKey(now), CreatedAt: now, } if err := s.Repo.InsertEvent(ctx, e); err != nil { return nil, err } return e, nil } // PrepareCall checks quota / key before external call; returns key_mode. func (s *Service) PrepareCall(ctx context.Context, uid int64, meter string) (keyMode string, err error) { if s.Resolver == nil { return "", domain.ErrNoKey } mode, has, err := s.Resolver.Resolve(ctx, uid, meter) if err != nil { return "", err } if !has { return "", domain.ErrNoKey } if mode == domain.KeyModePlatform { prefs, _ := s.ensurePrefs(ctx, uid) if !prefs.Unlimited { sum, err := s.GetSummary(ctx, uid, domain.CurrentMonthKey()) if err != nil { return "", err } cost := domain.DefaultCreditCost[meter] if cost == 0 { cost = 1 } if sum.Platform.CreditsRemaining < cost { return "", domain.ErrQuotaExceeded } } } return mode, nil } func (s *Service) ensurePrefs(ctx context.Context, uid int64) (*domain.MemberPrefs, error) { p, err := s.Repo.GetPrefs(ctx, uid) if err != nil || p == nil { p = &domain.MemberPrefs{UID: uid, PlanID: domain.PlanFree, UpdatedAt: domain.NowNano()} _ = s.Repo.SavePrefs(ctx, p) } if p.PlanID == "" { p.PlanID = domain.PlanFree } return p, nil } func (s *Service) GetSummary(ctx context.Context, uid int64, monthKey string) (*domain.MonthSummary, error) { if monthKey == "" { monthKey = domain.CurrentMonthKey() } prefs, _ := s.ensurePrefs(ctx, uid) plan := domain.Plans[prefs.PlanID] if plan.ID == "" { plan = domain.Plans[domain.PlanFree] } events, err := s.Repo.ListEvents(ctx, uid, monthKey, "all", 0) if err != nil { return nil, err } platBy := map[string]domain.MeterCount{} byokBy := map[string]domain.MeterCount{} platUsed, platCalls, byokCalls := 0, 0, 0 for _, e := range events { if e.KeyMode == domain.KeyModePlatform { platUsed += e.Credits platCalls++ m := platBy[e.Meter] m.Count++ m.Credits += e.Credits platBy[e.Meter] = m } else if e.KeyMode == domain.KeyModeByok { byokCalls++ m := byokBy[e.Meter] m.Count++ byokBy[e.Meter] = m } } remaining := plan.MonthlyCredits - platUsed if remaining < 0 { remaining = 0 } if prefs.Unlimited { remaining = plan.MonthlyCredits // display full; not enforced } return &domain.MonthSummary{ MonthKey: monthKey, UID: uid, PlanID: prefs.PlanID, Unlimited: prefs.Unlimited, Platform: domain.PlatformBlock{ CreditsUsed: platUsed, CreditsRemaining: remaining, CreditsTotal: plan.MonthlyCredits, CallCount: platCalls, ByMeter: platBy, }, Byok: domain.ByokBlock{CallCount: byokCalls, ByMeter: byokBy}, TotalCredits: plan.MonthlyCredits, RemainingCredits: remaining, }, nil } func (s *Service) ListEvents(ctx context.Context, uid int64, monthKey, keyMode string, limit int) ([]*domain.Event, error) { if limit <= 0 { limit = 100 } return s.Repo.ListEvents(ctx, uid, monthKey, keyMode, limit) } func (s *Service) GetPrefs(ctx context.Context, uid int64) (*domain.MemberPrefs, error) { return s.ensurePrefs(ctx, uid) } func (s *Service) SetPrefs(ctx context.Context, actorUID int64, targetUID int64, isAdmin bool, planID string, unlimited *bool) (*domain.MemberPrefs, error) { if !isAdmin && actorUID != targetUID { return nil, domain.ErrForbidden } if !isAdmin && unlimited != nil { // members cannot set unlimited return nil, domain.ErrForbidden } p, _ := s.ensurePrefs(ctx, targetUID) if planID != "" { if _, ok := domain.Plans[planID]; !ok { return nil, domain.ErrInvalidPlan } // member self can only change via purchase if !isAdmin && actorUID == targetUID { return nil, domain.ErrForbidden } p.PlanID = planID } if unlimited != nil && isAdmin { p.Unlimited = *unlimited } p.UpdatedAt = domain.NowNano() if err := s.Repo.SavePrefs(ctx, p); err != nil { return nil, err } return p, nil } func (s *Service) PurchasePlan(ctx context.Context, uid int64, planID, mockRef string) (*domain.Purchase, error) { if _, ok := domain.Plans[planID]; !ok { return nil, domain.ErrInvalidPlan } p, _ := s.ensurePrefs(ctx, uid) p.PlanID = planID p.UpdatedAt = domain.NowNano() _ = s.Repo.SavePrefs(ctx, p) pur := &domain.Purchase{ ID: uuid.NewString(), UID: uid, PlanID: planID, MockRef: mockRef, CreatedAt: domain.NowNano(), } if err := s.Repo.InsertPurchase(ctx, pur); err != nil { return nil, err } return pur, nil } func (s *Service) ListMyPurchases(ctx context.Context, uid int64, limit int) ([]*domain.Purchase, error) { if limit <= 0 { limit = 50 } return s.Repo.ListPurchases(ctx, uid, limit) } func (s *Service) TenantSummary(ctx context.Context, monthKey string) (*domain.TenantSummary, error) { if monthKey == "" { monthKey = domain.CurrentMonthKey() } events, err := s.Repo.ListEventsAll(ctx, monthKey, 0) if err != nil { return nil, err } type acc struct { plat int byok int } byUID := map[int64]*acc{} platTotal, byokTotal := 0, 0 for _, e := range events { a := byUID[e.UID] if a == nil { a = &acc{} byUID[e.UID] = a } if e.KeyMode == domain.KeyModePlatform { a.plat += e.Credits platTotal += e.Credits } else if e.KeyMode == domain.KeyModeByok { a.byok++ byokTotal++ } } var members []domain.TenantRow for uid, a := range byUID { prefs, _ := s.ensurePrefs(ctx, uid) members = append(members, domain.TenantRow{ UID: uid, PlanID: prefs.PlanID, Unlimited: prefs.Unlimited, PlatformCreditsUsed: a.plat, ByokCallCount: a.byok, }) } return &domain.TenantSummary{ MonthKey: monthKey, PlatformCreditsUsed: platTotal, ByokCallCount: byokTotal, Members: members, }, nil }