package app import ( "context" "encoding/json" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/geo-platform/tenant-api/internal/shared/auditlog" sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache" "github.com/geo-platform/tenant-api/internal/shared/middleware" "github.com/geo-platform/tenant-api/internal/shared/response" "github.com/geo-platform/tenant-api/internal/tenant/repository" ) type ManualBindKolSubscriptionInput struct { TenantID int64 `json:"tenant_id" binding:"required"` PackageID int64 `json:"package_id" binding:"required"` EndAt *time.Time `json:"end_at"` } type KolSubscriptionAdminService struct { pool *pgxpool.Pool auditLogs *auditlog.AsyncWriter marketplaceRepo repository.KolMarketplaceRepository subRepo repository.KolSubscriptionRepository promptRepo repository.KolPromptRepository cache sharedcache.Cache } func NewKolSubscriptionAdminService( pool *pgxpool.Pool, auditLogs *auditlog.AsyncWriter, marketplaceRepo repository.KolMarketplaceRepository, subRepo repository.KolSubscriptionRepository, promptRepo repository.KolPromptRepository, ) *KolSubscriptionAdminService { return &KolSubscriptionAdminService{ pool: pool, auditLogs: auditLogs, marketplaceRepo: marketplaceRepo, subRepo: subRepo, promptRepo: promptRepo, } } func (s *KolSubscriptionAdminService) WithCache(c sharedcache.Cache) *KolSubscriptionAdminService { s.cache = c return s } func (s *KolSubscriptionAdminService) Approve(ctx context.Context, id int64, endAt *time.Time, operatorID int64) (*KolSubscriptionResponse, error) { tx, err := s.pool.Begin(ctx) if err != nil { return nil, response.ErrInternal(50096, "kol_subscription_approve_tx_failed", "failed to begin subscription approval transaction") } defer tx.Rollback(ctx) subTx := repository.NewKolSubscriptionRepository(tx) marketplaceTx := repository.NewKolMarketplaceRepository(tx) promptTx := repository.NewKolPromptRepository(tx) sub, err := subTx.GetByIDAny(ctx, id) if err != nil { return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error()) } if sub == nil { return nil, errKolSubscriptionNotFound } if sub.Status != "pending" { return nil, errKolSubscriptionNotPending } pkg, err := marketplaceTx.GetPublished(ctx, sub.PackageID) if err != nil { return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error()) } if pkg == nil { return nil, errKolMarketplacePackageNotFound } if err := subTx.Approve(ctx, id, sub.TenantID, operatorID, endAt); err != nil { return nil, response.ErrInternal(50097, "kol_subscription_approve_failed", err.Error()) } if err := grantActivePackagePrompts(ctx, subTx, promptTx, sub.TenantID, id, pkg.TenantID, pkg.ID); err != nil { return nil, err } updated, err := subTx.GetByIDAny(ctx, id) if err != nil { return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error()) } if err := tx.Commit(ctx); err != nil { return nil, response.ErrInternal(50098, "kol_subscription_approve_commit_failed", "failed to commit subscription approval") } InvalidateKolPromptCaches(ctx, s.cache) s.logAdminAction(ctx, "approve", operatorID, updated.TenantID, updated.ID, map[string]interface{}{ "id": updated.ID, "tenant_id": updated.TenantID, "package_id": updated.PackageID, "status": updated.Status, "end_at": timeStringPtr(endAt), }) return newKolSubscriptionResponse(updated), nil } func (s *KolSubscriptionAdminService) ManualBind(ctx context.Context, input ManualBindKolSubscriptionInput, operatorID int64) (*KolSubscriptionResponse, error) { if input.TenantID <= 0 { return nil, response.ErrBadRequest(40071, "kol_subscription_tenant_required", "tenant_id is required") } if input.PackageID <= 0 { return nil, response.ErrBadRequest(40072, "kol_subscription_package_required", "package_id is required") } tx, err := s.pool.Begin(ctx) if err != nil { return nil, response.ErrInternal(50099, "kol_subscription_bind_tx_failed", "failed to begin manual bind transaction") } defer tx.Rollback(ctx) subTx := repository.NewKolSubscriptionRepository(tx) marketplaceTx := repository.NewKolMarketplaceRepository(tx) promptTx := repository.NewKolPromptRepository(tx) pkg, err := marketplaceTx.GetPublished(ctx, input.PackageID) if err != nil { return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error()) } if pkg == nil { return nil, errKolMarketplacePackageNotFound } existing, err := subTx.GetActiveForTenant(ctx, input.TenantID, input.PackageID) if err != nil { return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error()) } if existing != nil { return nil, errKolSubscriptionExists } sub, err := subTx.Create(ctx, input.TenantID, input.PackageID) if err != nil { if isUniqueConstraintError(err, "uk_kol_subscription_active") { return nil, errKolSubscriptionExists } return nil, response.ErrInternal(50100, "kol_subscription_create_failed", err.Error()) } if err := subTx.Approve(ctx, sub.ID, input.TenantID, operatorID, input.EndAt); err != nil { return nil, response.ErrInternal(50097, "kol_subscription_approve_failed", err.Error()) } if err := grantActivePackagePrompts(ctx, subTx, promptTx, input.TenantID, sub.ID, pkg.TenantID, pkg.ID); err != nil { return nil, err } updated, err := subTx.GetByIDAny(ctx, sub.ID) if err != nil { return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error()) } if err := tx.Commit(ctx); err != nil { return nil, response.ErrInternal(50101, "kol_subscription_bind_commit_failed", "failed to commit manual bind") } InvalidateKolPromptCaches(ctx, s.cache) s.logAdminAction(ctx, "manual_bind", operatorID, updated.TenantID, updated.ID, map[string]interface{}{ "id": updated.ID, "tenant_id": updated.TenantID, "package_id": updated.PackageID, "status": updated.Status, "end_at": timeStringPtr(input.EndAt), }) return newKolSubscriptionResponse(updated), nil } func (s *KolSubscriptionAdminService) Revoke(ctx context.Context, id int64, operatorID int64) error { tx, err := s.pool.Begin(ctx) if err != nil { return response.ErrInternal(50102, "kol_subscription_revoke_tx_failed", "failed to begin revoke transaction") } defer tx.Rollback(ctx) subTx := repository.NewKolSubscriptionRepository(tx) sub, err := subTx.GetByIDAny(ctx, id) if err != nil { return response.ErrInternal(50093, "kol_subscription_query_failed", err.Error()) } if sub == nil { return errKolSubscriptionNotFound } if err := subTx.RevokeByTenant(ctx, id, sub.TenantID); err != nil { return response.ErrInternal(50103, "kol_subscription_revoke_failed", err.Error()) } if err := subTx.RevokeAccessRowsBySubscription(ctx, id, sub.TenantID); err != nil { return response.ErrInternal(50104, "kol_subscription_access_revoke_failed", err.Error()) } if err := tx.Commit(ctx); err != nil { return response.ErrInternal(50105, "kol_subscription_revoke_commit_failed", "failed to commit subscription revoke") } InvalidateKolPromptCaches(ctx, s.cache) s.logAdminAction(ctx, "revoke", operatorID, sub.TenantID, sub.ID, map[string]interface{}{ "id": sub.ID, "tenant_id": sub.TenantID, "package_id": sub.PackageID, "status": "revoked", }) return nil } func grantActivePackagePrompts( ctx context.Context, subRepo repository.KolSubscriptionRepository, promptRepo repository.KolPromptRepository, subscriberTenantID, subscriptionID, creatorTenantID, packageID int64, ) error { prompts, err := promptRepo.ListActiveByPackage(ctx, creatorTenantID, packageID) if err != nil { return response.ErrInternal(50092, "kol_marketplace_prompt_query_failed", err.Error()) } for _, prompt := range prompts { if err := subRepo.CreateAccessRow(ctx, repository.CreateAccessRowInput{ TenantID: subscriberTenantID, SubscriptionID: subscriptionID, PackageID: packageID, PromptID: prompt.ID, }); err != nil { return response.ErrInternal(50084, "kol_subscription_access_create_failed", err.Error()) } } return nil } func (s *KolSubscriptionAdminService) logAdminAction( ctx context.Context, action string, operatorID, targetTenantID, subscriptionID int64, after map[string]interface{}, ) { if s.auditLogs == nil { return } afterJSON, _ := json.Marshal(after) result := "success" resourceType := "kol_subscription" requestID := middleware.RequestIDFromContext(ctx) s.auditLogs.Log(auditlog.Entry{ OperatorID: operatorID, TenantID: &targetTenantID, Module: "kol_admin", Action: action, ResourceType: &resourceType, ResourceID: &subscriptionID, RequestID: nilIfEmptyString(requestID), AfterJSON: afterJSON, Result: &result, }) }