Files
geo/server/internal/tenant/app/kol_dashboard_service.go
T

192 lines
5.8 KiB
Go

package app
import (
"context"
"github.com/geo-platform/tenant-api/internal/shared/auth"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
type DashboardOverviewResponse struct {
TotalSubscriberTenants int64 `json:"total_subscriber_tenants"`
MonthNewSubscriptions int64 `json:"month_new_subscriptions"`
TotalUsage int64 `json:"total_usage"`
MonthUsage int64 `json:"month_usage"`
FailureCount int64 `json:"failure_count"`
FailureRate float64 `json:"failure_rate"`
}
type DashboardPackageStat struct {
PackageID int64 `json:"package_id"`
PackageName string `json:"package_name"`
SubscriberTenants int64 `json:"subscriber_tenants"`
UsageCount int64 `json:"usage_count"`
FailureRate float64 `json:"failure_rate"`
}
type DashboardTrendPoint struct {
Date string `json:"date"`
Subscriptions int64 `json:"subscriptions"`
Usages int64 `json:"usages"`
}
type KolDashboardService struct {
profileSvc *KolProfileService
pkgRepo repository.KolPackageRepository
subRepo repository.KolSubscriptionRepository
usageRepo repository.KolUsageRepository
}
func NewKolDashboardService(
profileSvc *KolProfileService,
pkgRepo repository.KolPackageRepository,
subRepo repository.KolSubscriptionRepository,
usageRepo repository.KolUsageRepository,
) *KolDashboardService {
return &KolDashboardService{
profileSvc: profileSvc,
pkgRepo: pkgRepo,
subRepo: subRepo,
usageRepo: usageRepo,
}
}
func (s *KolDashboardService) Overview(ctx context.Context, actor auth.Actor) (*DashboardOverviewResponse, error) {
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
if err != nil {
return nil, err
}
packageIDs, err := s.pkgRepo.ListPackageIDsByProfile(ctx, actor.TenantID, profile.ID)
if err != nil {
return nil, response.ErrInternal(50120, "kol_dashboard_package_query_failed", err.Error())
}
if len(packageIDs) == 0 {
return &DashboardOverviewResponse{}, nil
}
overview, err := s.usageRepo.DashboardOverview(ctx, packageIDs)
if err != nil {
return nil, response.ErrInternal(50121, "kol_dashboard_overview_query_failed", err.Error())
}
totalUsage := maxDashboardBase(overview.TotalUsage)
return &DashboardOverviewResponse{
TotalSubscriberTenants: overview.TotalSubs,
MonthNewSubscriptions: overview.MonthSubs,
TotalUsage: overview.TotalUsage,
MonthUsage: overview.MonthUsage,
FailureCount: overview.FailureCount,
FailureRate: float64(overview.FailureCount) / float64(totalUsage),
}, nil
}
func (s *KolDashboardService) PerPackage(ctx context.Context, actor auth.Actor) ([]DashboardPackageStat, error) {
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
if err != nil {
return nil, err
}
packages, err := s.pkgRepo.ListByProfile(ctx, actor.TenantID, profile.ID)
if err != nil {
return nil, response.ErrInternal(50120, "kol_dashboard_package_query_failed", err.Error())
}
if len(packages) == 0 {
return []DashboardPackageStat{}, nil
}
packageIDs := make([]int64, 0, len(packages))
for _, pkg := range packages {
packageIDs = append(packageIDs, pkg.ID)
}
usageStats, err := s.usageRepo.PerPackageStats(ctx, packageIDs)
if err != nil {
return nil, response.ErrInternal(50122, "kol_dashboard_package_stats_query_failed", err.Error())
}
usageByPackage := make(map[int64]repository.KolUsagePackageStats, len(usageStats))
for _, stat := range usageStats {
usageByPackage[stat.PackageID] = stat
}
result := make([]DashboardPackageStat, 0, len(packages))
for _, pkg := range packages {
subscribers, err := s.subRepo.ListActiveSubscribersForPackage(ctx, pkg.ID)
if err != nil {
return nil, response.ErrInternal(50123, "kol_dashboard_subscriber_query_failed", err.Error())
}
subscriberTenants := uniqueSubscriberTenants(subscribers)
usage := usageByPackage[pkg.ID]
result = append(result, DashboardPackageStat{
PackageID: pkg.ID,
PackageName: pkg.Name,
SubscriberTenants: subscriberTenants,
UsageCount: usage.Total,
FailureRate: float64(usage.Failed) / float64(maxDashboardBase(usage.Total)),
})
}
return result, nil
}
func (s *KolDashboardService) Trend(ctx context.Context, actor auth.Actor, periodDays int) ([]DashboardTrendPoint, error) {
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
if err != nil {
return nil, err
}
packageIDs, err := s.pkgRepo.ListPackageIDsByProfile(ctx, actor.TenantID, profile.ID)
if err != nil {
return nil, response.ErrInternal(50120, "kol_dashboard_package_query_failed", err.Error())
}
if len(packageIDs) == 0 {
return []DashboardTrendPoint{}, nil
}
points, err := s.usageRepo.DashboardTrend(ctx, packageIDs, periodDays)
if err != nil {
return nil, response.ErrInternal(50124, "kol_dashboard_trend_query_failed", err.Error())
}
result := make([]DashboardTrendPoint, 0, len(points))
for _, point := range points {
result = append(result, DashboardTrendPoint{
Date: point.Day.Format("2006-01-02"),
Subscriptions: point.Subscriptions,
Usages: point.Usages,
})
}
return result, nil
}
func maxDashboardBase(total int64) int64 {
if total <= 0 {
return 1
}
return total
}
func uniqueSubscriberTenants(subscribers []repository.KolSubscription) int64 {
if len(subscribers) == 0 {
return 0
}
seen := make(map[int64]struct{}, len(subscribers))
for _, subscriber := range subscribers {
seen[subscriber.TenantID] = struct{}{}
}
return int64(len(seen))
}
func ValidateKolDashboardPeriodDays(periodDays int) error {
switch periodDays {
case 7, 30, 90:
return nil
default:
return response.ErrBadRequest(40073, "kol_dashboard_period_invalid", "period_days must be one of 7, 30, 90")
}
}