70725193eb
Addresses findings from consolidated Phase 1 code review: - Add composite unique indexes + composite FKs so subscription_prompts and packages cannot reference mismatched (tenant_id, package_id, prompt_id) tuples, enforcing ACL lineage at the schema level. - Add status CHECK constraints on all KOL tables as defense-in-depth. - ListActiveSubscribersForPackage: drop meaningless tenant_id IS NOT NULL, add end_at expiry filter so fan-out skips expired subscribers. - ApproveKolSubscription: add status='pending' guard to prevent double-approval. - ListSubscriptionPromptsByTenant: join subscription/prompt/package/profile lifecycle so hidden/archived content no longer leaks; surface kol_display_name for UI. - ListPublishedKolPackages: exclude expired subscribers from marketplace count. - KolDashboardTrend: rewrite as CTE with day-series, returning both daily usage and new subscription counts. - Remove cosmetic tenant_id IS NOT NULL from dashboard overview queries (the column is NOT NULL by schema). - check_tenant_scope.sh: require tenant_id = sqlc.(n?)arg filter, reject tenant_id IS NOT NULL as a fake scope, maintain explicit exemption list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
201 lines
5.8 KiB
Go
201 lines
5.8 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
|
)
|
|
|
|
type KolUsageLog struct {
|
|
ID int64
|
|
TenantID int64
|
|
GenerationTaskID *int64
|
|
Status string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
type KolUsageByTaskRecord struct {
|
|
ID int64
|
|
TenantID int64
|
|
PackageID int64
|
|
PromptID int64
|
|
Status string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
type CreateKolUsageLogInput struct {
|
|
TenantID int64
|
|
SubscriptionID *int64
|
|
SubscriptionPromptID *int64
|
|
PackageID int64
|
|
PromptID int64
|
|
PromptRevisionNo int
|
|
GenerationTaskID *int64
|
|
VariablesSnapshot []byte
|
|
SchemaSnapshot []byte
|
|
RenderedPromptHash *string
|
|
}
|
|
|
|
type KolDashboardOverview struct {
|
|
TotalSubs int64
|
|
MonthSubs int64
|
|
TotalUsage int64
|
|
MonthUsage int64
|
|
FailureCount int64
|
|
}
|
|
|
|
type KolDashboardTrendPoint struct {
|
|
Day time.Time
|
|
Usages int64
|
|
Subscriptions int64
|
|
}
|
|
|
|
type KolUsagePackageStats struct {
|
|
PackageID int64
|
|
Total int64
|
|
Completed int64
|
|
Failed int64
|
|
}
|
|
|
|
type KolUsageRepository interface {
|
|
Create(ctx context.Context, input CreateKolUsageLogInput) (*KolUsageLog, error)
|
|
MarkCompletedByTask(ctx context.Context, tenantID, taskID, articleID int64) error
|
|
MarkFailedByTask(ctx context.Context, tenantID, taskID int64, errorMsg string) error
|
|
GetByGenerationTask(ctx context.Context, tenantID, taskID int64) (*KolUsageByTaskRecord, error)
|
|
DashboardOverview(ctx context.Context, packageIDs []int64) (*KolDashboardOverview, error)
|
|
DashboardTrend(ctx context.Context, packageIDs []int64, periodDays int) ([]KolDashboardTrendPoint, error)
|
|
PerPackageStats(ctx context.Context, packageIDs []int64) ([]KolUsagePackageStats, error)
|
|
}
|
|
|
|
type kolUsageRepository struct {
|
|
q generated.Querier
|
|
}
|
|
|
|
func NewKolUsageRepository(db generated.DBTX) KolUsageRepository {
|
|
return &kolUsageRepository{q: newQuerier(db)}
|
|
}
|
|
|
|
func (r *kolUsageRepository) Create(ctx context.Context, input CreateKolUsageLogInput) (*KolUsageLog, error) {
|
|
row, err := r.q.CreateKolUsageLog(ctx, generated.CreateKolUsageLogParams{
|
|
TenantID: input.TenantID,
|
|
SubscriptionID: pgInt8(input.SubscriptionID),
|
|
SubscriptionPromptID: pgInt8(input.SubscriptionPromptID),
|
|
PackageID: input.PackageID,
|
|
PromptID: input.PromptID,
|
|
PromptRevisionNo: int32(input.PromptRevisionNo),
|
|
GenerationTaskID: pgInt8(input.GenerationTaskID),
|
|
VariablesSnapshot: input.VariablesSnapshot,
|
|
SchemaSnapshot: input.SchemaSnapshot,
|
|
RenderedPromptHash: pgText(input.RenderedPromptHash),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &KolUsageLog{
|
|
ID: row.ID,
|
|
TenantID: row.TenantID,
|
|
GenerationTaskID: nullableInt64(row.GenerationTaskID),
|
|
Status: row.Status,
|
|
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
|
}, nil
|
|
}
|
|
|
|
func (r *kolUsageRepository) MarkCompletedByTask(ctx context.Context, tenantID, taskID, articleID int64) error {
|
|
return r.q.MarkKolUsageCompletedByTask(ctx, generated.MarkKolUsageCompletedByTaskParams{
|
|
ArticleID: pgInt8(&articleID),
|
|
GenerationTaskID: pgInt8(&taskID),
|
|
TenantID: tenantID,
|
|
})
|
|
}
|
|
|
|
func (r *kolUsageRepository) MarkFailedByTask(ctx context.Context, tenantID, taskID int64, errorMsg string) error {
|
|
return r.q.MarkKolUsageFailedByTask(ctx, generated.MarkKolUsageFailedByTaskParams{
|
|
ErrorMessage: pgText(&errorMsg),
|
|
GenerationTaskID: pgInt8(&taskID),
|
|
TenantID: tenantID,
|
|
})
|
|
}
|
|
|
|
func (r *kolUsageRepository) GetByGenerationTask(ctx context.Context, tenantID, taskID int64) (*KolUsageByTaskRecord, error) {
|
|
row, err := r.q.GetKolUsageByGenerationTask(ctx, generated.GetKolUsageByGenerationTaskParams{
|
|
GenerationTaskID: pgInt8(&taskID),
|
|
TenantID: tenantID,
|
|
})
|
|
if err != nil {
|
|
// Callers use errors.Is(err, pgx.ErrNoRows) for "not found"; preserve err semantics.
|
|
return nil, err
|
|
}
|
|
return &KolUsageByTaskRecord{
|
|
ID: row.ID,
|
|
TenantID: row.TenantID,
|
|
PackageID: row.PackageID,
|
|
PromptID: row.PromptID,
|
|
Status: row.Status,
|
|
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
|
}, nil
|
|
}
|
|
|
|
func (r *kolUsageRepository) DashboardOverview(ctx context.Context, packageIDs []int64) (*KolDashboardOverview, error) {
|
|
if len(packageIDs) == 0 {
|
|
return &KolDashboardOverview{}, nil
|
|
}
|
|
row, err := r.q.KolDashboardOverview(ctx, packageIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &KolDashboardOverview{
|
|
TotalSubs: row.TotalSubs,
|
|
MonthSubs: row.MonthSubs,
|
|
TotalUsage: row.TotalUsage,
|
|
MonthUsage: row.MonthUsage,
|
|
FailureCount: row.FailureCount,
|
|
}, nil
|
|
}
|
|
|
|
func (r *kolUsageRepository) DashboardTrend(ctx context.Context, packageIDs []int64, periodDays int) ([]KolDashboardTrendPoint, error) {
|
|
if len(packageIDs) == 0 {
|
|
return []KolDashboardTrendPoint{}, nil
|
|
}
|
|
rows, err := r.q.KolDashboardTrend(ctx, generated.KolDashboardTrendParams{
|
|
PackageIds: packageIDs,
|
|
PeriodDays: int32(periodDays),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]KolDashboardTrendPoint, len(rows))
|
|
for i, row := range rows {
|
|
var day time.Time
|
|
if row.D.Valid {
|
|
day = row.D.Time
|
|
}
|
|
result[i] = KolDashboardTrendPoint{
|
|
Day: day,
|
|
Usages: row.Usages,
|
|
Subscriptions: row.Subscriptions,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (r *kolUsageRepository) PerPackageStats(ctx context.Context, packageIDs []int64) ([]KolUsagePackageStats, error) {
|
|
if len(packageIDs) == 0 {
|
|
return []KolUsagePackageStats{}, nil
|
|
}
|
|
rows, err := r.q.KolUsageCountByPackage(ctx, packageIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]KolUsagePackageStats, len(rows))
|
|
for i, row := range rows {
|
|
result[i] = KolUsagePackageStats{
|
|
PackageID: row.PackageID,
|
|
Total: row.Total,
|
|
Completed: row.Completed,
|
|
Failed: row.Failed,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|