feat(tenant/kol): let KOLs self-subscribe to their own published packages
Add /kol/manage/packages/:id/self-subscription POST/DELETE on the tenant side so a KOL can grant themselves access to their own published package without going through the marketplace approval flow. Reject self-subscribe through the marketplace with a clear error, expose owned_by_current_tenant on package responses, and surface a self_subscription block on KolPackage Response. The admin-web KOL workspace gets 订阅自己 / 取消订阅 entries with matching messaging in the marketplace detail view. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
@@ -54,6 +55,8 @@ type CreateAccessRowInput struct {
|
||||
|
||||
type KolSubscriptionRepository interface {
|
||||
Create(ctx context.Context, tenantID, packageID int64) (*KolSubscription, error)
|
||||
SelfSubscribe(ctx context.Context, tenantID, packageID int64) (*KolSubscription, error)
|
||||
CancelSelfSubscription(ctx context.Context, tenantID, packageID int64) (*KolSubscription, error)
|
||||
GetByID(ctx context.Context, tenantID, id int64) (*KolSubscription, error)
|
||||
GetByIDAny(ctx context.Context, id int64) (*KolSubscription, error)
|
||||
GetActiveForTenant(ctx context.Context, tenantID, packageID int64) (*KolSubscription, error)
|
||||
@@ -69,11 +72,12 @@ type KolSubscriptionRepository interface {
|
||||
}
|
||||
|
||||
type kolSubscriptionRepository struct {
|
||||
q generated.Querier
|
||||
db generated.DBTX
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewKolSubscriptionRepository(db generated.DBTX) KolSubscriptionRepository {
|
||||
return &kolSubscriptionRepository{q: newQuerier(db)}
|
||||
return &kolSubscriptionRepository{db: db, q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func mapKolSubscription(row generated.KolSubscription) KolSubscription {
|
||||
@@ -104,6 +108,218 @@ func (r *kolSubscriptionRepository) Create(ctx context.Context, tenantID, packag
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
func (r *kolSubscriptionRepository) SelfSubscribe(ctx context.Context, tenantID, packageID int64) (*KolSubscription, error) {
|
||||
tx, err := beginKolSubscriptionTx(ctx, r.db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tx != nil {
|
||||
defer tx.Rollback(ctx)
|
||||
}
|
||||
db := kolSubscriptionDB(r.db, tx)
|
||||
|
||||
sub, err := lockCurrentKolSubscription(ctx, db, tenantID, packageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sub == nil {
|
||||
sub, err = insertSelfKolSubscription(ctx, db, tenantID, packageID)
|
||||
} else if sub.Status != "active" {
|
||||
sub, err = activateSelfKolSubscription(ctx, db, sub.ID, tenantID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := grantSelfSubscriptionPrompts(ctx, db, tenantID, packageID, sub.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updated, err := getKolSubscriptionByTenantAndID(ctx, db, tenantID, sub.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tx != nil {
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (r *kolSubscriptionRepository) CancelSelfSubscription(ctx context.Context, tenantID, packageID int64) (*KolSubscription, error) {
|
||||
tx, err := beginKolSubscriptionTx(ctx, r.db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tx != nil {
|
||||
defer tx.Rollback(ctx)
|
||||
}
|
||||
db := kolSubscriptionDB(r.db, tx)
|
||||
|
||||
sub, err := lockCurrentKolSubscription(ctx, db, tenantID, packageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sub == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
updated, err := revokeKolSubscriptionByID(ctx, db, tenantID, sub.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := revokeKolSubscriptionAccessRows(ctx, db, tenantID, sub.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tx != nil {
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func beginKolSubscriptionTx(ctx context.Context, db generated.DBTX) (pgx.Tx, error) {
|
||||
if starter, ok := db.(interface {
|
||||
Begin(context.Context) (pgx.Tx, error)
|
||||
}); ok {
|
||||
return starter.Begin(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func kolSubscriptionDB(fallback generated.DBTX, tx pgx.Tx) generated.DBTX {
|
||||
if tx != nil {
|
||||
return tx
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func scanKolSubscriptionRaw(row interface{ Scan(...any) error }) (*KolSubscription, error) {
|
||||
var (
|
||||
id int64
|
||||
tenantID int64
|
||||
packageID int64
|
||||
status string
|
||||
approvedBy pgtype.Int8
|
||||
startAt pgtype.Timestamptz
|
||||
endAt pgtype.Timestamptz
|
||||
createdAt pgtype.Timestamptz
|
||||
updatedAt pgtype.Timestamptz
|
||||
)
|
||||
if err := row.Scan(
|
||||
&id,
|
||||
&tenantID,
|
||||
&packageID,
|
||||
&status,
|
||||
&approvedBy,
|
||||
&startAt,
|
||||
&endAt,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &KolSubscription{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
PackageID: packageID,
|
||||
Status: status,
|
||||
ApprovedBy: nullableInt64(approvedBy),
|
||||
StartAt: optionalTime(startAt),
|
||||
EndAt: optionalTime(endAt),
|
||||
CreatedAt: timeFromTimestamp(createdAt),
|
||||
UpdatedAt: timeFromTimestamp(updatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func lockCurrentKolSubscription(ctx context.Context, db generated.DBTX, tenantID, packageID int64) (*KolSubscription, error) {
|
||||
return scanKolSubscriptionRaw(db.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at
|
||||
FROM kol_subscriptions
|
||||
WHERE tenant_id = $1
|
||||
AND package_id = $2
|
||||
AND status IN ('pending', 'active')
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 1
|
||||
FOR UPDATE`, tenantID, packageID))
|
||||
}
|
||||
|
||||
func insertSelfKolSubscription(ctx context.Context, db generated.DBTX, tenantID, packageID int64) (*KolSubscription, error) {
|
||||
return scanKolSubscriptionRaw(db.QueryRow(ctx, `
|
||||
INSERT INTO kol_subscriptions (tenant_id, package_id, status, approved_by, start_at, end_at)
|
||||
VALUES ($1, $2, 'active', NULL, NOW(), NULL)
|
||||
RETURNING id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at`, tenantID, packageID))
|
||||
}
|
||||
|
||||
func activateSelfKolSubscription(ctx context.Context, db generated.DBTX, id, tenantID int64) (*KolSubscription, error) {
|
||||
return scanKolSubscriptionRaw(db.QueryRow(ctx, `
|
||||
UPDATE kol_subscriptions
|
||||
SET status = 'active',
|
||||
approved_by = NULL,
|
||||
start_at = NOW(),
|
||||
end_at = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
RETURNING id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at`, id, tenantID))
|
||||
}
|
||||
|
||||
func getKolSubscriptionByTenantAndID(ctx context.Context, db generated.DBTX, tenantID, id int64) (*KolSubscription, error) {
|
||||
return scanKolSubscriptionRaw(db.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at
|
||||
FROM kol_subscriptions
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2`, id, tenantID))
|
||||
}
|
||||
|
||||
func revokeKolSubscriptionByID(ctx context.Context, db generated.DBTX, tenantID, id int64) (*KolSubscription, error) {
|
||||
return scanKolSubscriptionRaw(db.QueryRow(ctx, `
|
||||
UPDATE kol_subscriptions
|
||||
SET status = 'revoked',
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND status IN ('pending', 'active')
|
||||
RETURNING id, tenant_id, package_id, status, approved_by, start_at, end_at, created_at, updated_at`, id, tenantID))
|
||||
}
|
||||
|
||||
func grantSelfSubscriptionPrompts(ctx context.Context, db generated.DBTX, tenantID, packageID, subscriptionID int64) error {
|
||||
_, err := db.Exec(ctx, `
|
||||
INSERT INTO kol_subscription_prompts (tenant_id, subscription_id, package_id, prompt_id, status)
|
||||
SELECT $1, $2, $3, p.id, 'active'
|
||||
FROM kol_prompts p
|
||||
WHERE p.tenant_id = $1
|
||||
AND p.package_id = $3
|
||||
AND p.status = 'active'
|
||||
AND p.published_revision_no IS NOT NULL
|
||||
AND p.deleted_at IS NULL
|
||||
ON CONFLICT (subscription_id, prompt_id) DO UPDATE
|
||||
SET status = 'active',
|
||||
revoked_at = NULL,
|
||||
updated_at = NOW()`, tenantID, subscriptionID, packageID)
|
||||
return err
|
||||
}
|
||||
|
||||
func revokeKolSubscriptionAccessRows(ctx context.Context, db generated.DBTX, tenantID, subscriptionID int64) error {
|
||||
_, err := db.Exec(ctx, `
|
||||
UPDATE kol_subscription_prompts
|
||||
SET status = 'revoked',
|
||||
revoked_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE tenant_id = $1
|
||||
AND subscription_id = $2
|
||||
AND status <> 'revoked'`, tenantID, subscriptionID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *kolSubscriptionRepository) GetByID(ctx context.Context, tenantID, id int64) (*KolSubscription, error) {
|
||||
row, err := r.q.GetKolSubscriptionByID(ctx, generated.GetKolSubscriptionByIDParams{
|
||||
ID: id,
|
||||
|
||||
Reference in New Issue
Block a user