feat(kol): marketplace + subscription admin services + publish fan-out
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
errKolMarketplacePackageNotFound = response.ErrNotFound(40471, "kol_marketplace_package_not_found", "package not found")
|
||||
errKolSubscriptionNotFound = response.ErrNotFound(40472, "kol_subscription_not_found", "subscription not found")
|
||||
errKolSubscriptionExists = response.ErrConflict(40971, "kol_subscription_exists", "subscription already exists")
|
||||
errKolSubscriptionNotPending = response.ErrConflict(40972, "kol_subscription_not_pending", "subscription is not pending")
|
||||
)
|
||||
|
||||
type MarketFilter struct {
|
||||
Industry *string
|
||||
Keyword *string
|
||||
Offset int
|
||||
Limit int
|
||||
}
|
||||
|
||||
type KolMarketplacePackageResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
KolProfileID int64 `json:"kol_profile_id"`
|
||||
KolDisplayName string `json:"kol_display_name"`
|
||||
KolAvatarURL *string `json:"kol_avatar_url"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
CoverURL *string `json:"cover_url"`
|
||||
Industry *string `json:"industry"`
|
||||
Tags []string `json:"tags"`
|
||||
PriceNote *string `json:"price_note"`
|
||||
Status string `json:"status"`
|
||||
PromptCount int `json:"prompt_count"`
|
||||
SubscriberCount int `json:"subscriber_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type KolMarketplacePromptResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PlatformHint *string `json:"platform_hint"`
|
||||
Status string `json:"status"`
|
||||
PublishedRevisionNo *int `json:"published_revision_no"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type KolMarketplacePackageDetailResponse struct {
|
||||
KolMarketplacePackageResponse
|
||||
KolBio *string `json:"kol_bio"`
|
||||
Prompts []KolMarketplacePromptResponse `json:"prompts"`
|
||||
Subscription *KolSubscriptionResponse `json:"subscription"`
|
||||
}
|
||||
|
||||
type KolSubscriptionResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
PackageID int64 `json:"package_id"`
|
||||
Status string `json:"status"`
|
||||
StartAt *string `json:"start_at"`
|
||||
EndAt *string `json:"end_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type KolSubscriptionPromptCardResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
PackageID int64 `json:"package_id"`
|
||||
PackageName string `json:"package_name"`
|
||||
PromptID int64 `json:"prompt_id"`
|
||||
PromptName string `json:"prompt_name"`
|
||||
PlatformHint *string `json:"platform_hint"`
|
||||
KolDisplayName string `json:"kol_display_name"`
|
||||
GrantedAt string `json:"granted_at"`
|
||||
}
|
||||
|
||||
type KolMarketplaceService struct {
|
||||
marketplaceRepo repository.KolMarketplaceRepository
|
||||
subRepo repository.KolSubscriptionRepository
|
||||
}
|
||||
|
||||
func NewKolMarketplaceService(
|
||||
marketplaceRepo repository.KolMarketplaceRepository,
|
||||
subRepo repository.KolSubscriptionRepository,
|
||||
) *KolMarketplaceService {
|
||||
return &KolMarketplaceService{
|
||||
marketplaceRepo: marketplaceRepo,
|
||||
subRepo: subRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *KolMarketplaceService) ListPackages(ctx context.Context, actor auth.Actor, filter MarketFilter) ([]KolMarketplacePackageResponse, error) {
|
||||
filter = normalizeMarketFilter(filter)
|
||||
|
||||
packages, err := s.marketplaceRepo.ListPublished(ctx, repository.ListPublishedFilter{
|
||||
Industry: filter.Industry,
|
||||
Keyword: filter.Keyword,
|
||||
Offset: filter.Offset,
|
||||
Limit: filter.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error())
|
||||
}
|
||||
|
||||
result := make([]KolMarketplacePackageResponse, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
item, err := newKolMarketplacePackageResponse(pkg)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50091, "kol_marketplace_decode_failed", err.Error())
|
||||
}
|
||||
result = append(result, *item)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *KolMarketplaceService) GetPackage(ctx context.Context, actor auth.Actor, id int64) (*KolMarketplacePackageDetailResponse, error) {
|
||||
pkg, err := s.marketplaceRepo.GetPublished(ctx, id)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error())
|
||||
}
|
||||
if pkg == nil {
|
||||
return nil, errKolMarketplacePackageNotFound
|
||||
}
|
||||
|
||||
prompts, err := s.marketplaceRepo.ListPublicPrompts(ctx, id)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50092, "kol_marketplace_prompt_query_failed", err.Error())
|
||||
}
|
||||
sub, err := s.subRepo.GetActiveForTenant(ctx, actor.TenantID, id)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
|
||||
}
|
||||
|
||||
pkgResp, err := newKolMarketplacePackageResponse(*pkg)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50091, "kol_marketplace_decode_failed", err.Error())
|
||||
}
|
||||
|
||||
resp := &KolMarketplacePackageDetailResponse{
|
||||
KolMarketplacePackageResponse: *pkgResp,
|
||||
KolBio: pkg.KolBio,
|
||||
Prompts: make([]KolMarketplacePromptResponse, 0, len(prompts)),
|
||||
Subscription: newKolSubscriptionResponse(sub),
|
||||
}
|
||||
for _, prompt := range prompts {
|
||||
resp.Prompts = append(resp.Prompts, KolMarketplacePromptResponse{
|
||||
ID: prompt.ID,
|
||||
Name: prompt.Name,
|
||||
PlatformHint: prompt.PlatformHint,
|
||||
Status: prompt.Status,
|
||||
PublishedRevisionNo: prompt.PublishedRevisionNo,
|
||||
CreatedAt: prompt.CreatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *KolMarketplaceService) Subscribe(ctx context.Context, actor auth.Actor, packageID int64) (*KolSubscriptionResponse, error) {
|
||||
pkg, err := s.marketplaceRepo.GetPublished(ctx, packageID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50090, "kol_marketplace_query_failed", err.Error())
|
||||
}
|
||||
if pkg == nil {
|
||||
return nil, errKolMarketplacePackageNotFound
|
||||
}
|
||||
|
||||
existing, err := s.subRepo.GetActiveForTenant(ctx, actor.TenantID, packageID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
|
||||
}
|
||||
if existing != nil {
|
||||
return nil, errKolSubscriptionExists
|
||||
}
|
||||
|
||||
sub, err := s.subRepo.Create(ctx, actor.TenantID, packageID)
|
||||
if err != nil {
|
||||
if isUniqueConstraintError(err, "uk_kol_subscription_active") {
|
||||
return nil, errKolSubscriptionExists
|
||||
}
|
||||
return nil, response.ErrInternal(50094, "kol_subscription_create_failed", err.Error())
|
||||
}
|
||||
return newKolSubscriptionResponse(sub), nil
|
||||
}
|
||||
|
||||
func (s *KolMarketplaceService) ListMySubscriptions(ctx context.Context, actor auth.Actor) ([]KolSubscriptionResponse, error) {
|
||||
subs, err := s.subRepo.ListByTenant(ctx, actor.TenantID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50093, "kol_subscription_query_failed", err.Error())
|
||||
}
|
||||
|
||||
result := make([]KolSubscriptionResponse, 0, len(subs))
|
||||
for _, sub := range subs {
|
||||
if resp := newKolSubscriptionResponse(&sub); resp != nil {
|
||||
result = append(result, *resp)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *KolMarketplaceService) ListMySubscriptionPrompts(ctx context.Context, actor auth.Actor) ([]KolSubscriptionPromptCardResponse, error) {
|
||||
rows, err := s.subRepo.ListSubscriptionPromptsByTenant(ctx, actor.TenantID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50095, "kol_subscription_prompt_query_failed", err.Error())
|
||||
}
|
||||
|
||||
result := make([]KolSubscriptionPromptCardResponse, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
grantedAt := ""
|
||||
if row.GrantedAt != nil {
|
||||
grantedAt = row.GrantedAt.Format(time.RFC3339)
|
||||
}
|
||||
result = append(result, KolSubscriptionPromptCardResponse{
|
||||
ID: row.ID,
|
||||
PackageID: row.PackageID,
|
||||
PackageName: row.PackageName,
|
||||
PromptID: row.PromptID,
|
||||
PromptName: row.PromptName,
|
||||
PlatformHint: row.PlatformHint,
|
||||
KolDisplayName: row.KolDisplayName,
|
||||
GrantedAt: grantedAt,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newKolMarketplacePackageResponse(pkg repository.PublishedKolPackage) (*KolMarketplacePackageResponse, error) {
|
||||
tags, err := decodeKolStringList(pkg.Tags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &KolMarketplacePackageResponse{
|
||||
ID: pkg.ID,
|
||||
KolProfileID: pkg.KolProfileID,
|
||||
KolDisplayName: pkg.KolDisplayName,
|
||||
KolAvatarURL: pkg.KolAvatarURL,
|
||||
Name: pkg.Name,
|
||||
Description: pkg.Description,
|
||||
CoverURL: pkg.CoverURL,
|
||||
Industry: pkg.Industry,
|
||||
Tags: tags,
|
||||
PriceNote: pkg.PriceNote,
|
||||
Status: pkg.Status,
|
||||
PromptCount: int(pkg.PromptCount),
|
||||
SubscriberCount: int(pkg.SubscriberCount),
|
||||
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newKolSubscriptionResponse(sub *repository.KolSubscription) *KolSubscriptionResponse {
|
||||
if sub == nil {
|
||||
return nil
|
||||
}
|
||||
return &KolSubscriptionResponse{
|
||||
ID: sub.ID,
|
||||
PackageID: sub.PackageID,
|
||||
Status: sub.Status,
|
||||
StartAt: timeStringPtr(sub.StartAt),
|
||||
EndAt: timeStringPtr(sub.EndAt),
|
||||
CreatedAt: sub.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeMarketFilter(filter MarketFilter) MarketFilter {
|
||||
if filter.Offset < 0 {
|
||||
filter.Offset = 0
|
||||
}
|
||||
if filter.Limit <= 0 {
|
||||
filter.Limit = 20
|
||||
}
|
||||
if filter.Limit > 100 {
|
||||
filter.Limit = 100
|
||||
}
|
||||
filter.Industry = normalizeKolOptionalString(filter.Industry)
|
||||
filter.Keyword = normalizeKolOptionalString(filter.Keyword)
|
||||
return filter
|
||||
}
|
||||
Reference in New Issue
Block a user