feat(membership): enforce tenant subscription plans with blocked UI
Add configurable membership plans (free/plus/pro) synced to DB on boot, a subscription guard middleware that blocks tenant endpoints on expired or missing plans, and a MembershipBlockedView that surfaces the reason so the admin can contact the tenant owner. Quota and brand-library reads now honor the active plan's policy JSON and expired subscriptions.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"golang.org/x/sync/singleflight"
|
||||
@@ -18,13 +19,23 @@ import (
|
||||
type WorkspaceService struct {
|
||||
repo repository.WorkspaceRepository
|
||||
templates repository.TemplateRepository
|
||||
quota repository.QuotaRepository
|
||||
supportedPlatformCount int
|
||||
cache sharedcache.Cache
|
||||
cacheGroup singleflight.Group
|
||||
}
|
||||
|
||||
func NewWorkspaceService(repo repository.WorkspaceRepository, templates repository.TemplateRepository) *WorkspaceService {
|
||||
return &WorkspaceService{repo: repo, templates: templates, supportedPlatformCount: 5}
|
||||
func NewWorkspaceService(
|
||||
repo repository.WorkspaceRepository,
|
||||
templates repository.TemplateRepository,
|
||||
quota repository.QuotaRepository,
|
||||
) *WorkspaceService {
|
||||
return &WorkspaceService{
|
||||
repo: repo,
|
||||
templates: templates,
|
||||
quota: quota,
|
||||
supportedPlatformCount: 5,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WorkspaceService) WithCache(c sharedcache.Cache) *WorkspaceService {
|
||||
@@ -87,29 +98,21 @@ func (s *WorkspaceService) RecentArticles(ctx context.Context) ([]domain.RecentA
|
||||
func (s *WorkspaceService) QuotaSummary(ctx context.Context) (*domain.QuotaSummary, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
return sharedcache.LoadJSON(ctx, s.cache, &s.cacheGroup, workspaceQuotaSummaryCacheKey(actor.TenantID), defaultCacheTTL(), func(loadCtx context.Context) (*domain.QuotaSummary, error) {
|
||||
balance, err := s.repo.GetQuotaSummary(loadCtx, actor.TenantID)
|
||||
status, err := s.quota.GetArticleQuotaStatus(loadCtx, actor.TenantID, time.Now().UTC())
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &domain.QuotaSummary{Balance: 0}, nil
|
||||
}
|
||||
return nil, response.ErrInternal(50010, "query_failed", "failed to get quota balance")
|
||||
}
|
||||
|
||||
plan, err := s.repo.GetActivePlanForTenant(loadCtx, actor.TenantID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &domain.QuotaSummary{Balance: balance}, nil
|
||||
}
|
||||
return nil, response.ErrInternal(50010, "query_failed", "failed to get active plan")
|
||||
}
|
||||
|
||||
var policy map[string]int
|
||||
_ = json.Unmarshal(plan.QuotaPolicyJSON, &policy)
|
||||
total := policy["article_generation"]
|
||||
|
||||
return &domain.QuotaSummary{
|
||||
PlanCode: plan.PlanCode,
|
||||
PlanName: plan.PlanName,
|
||||
TotalQuota: total,
|
||||
UsedQuota: total - balance,
|
||||
Balance: balance,
|
||||
PlanCode: status.PlanCode,
|
||||
PlanName: status.PlanName,
|
||||
TotalQuota: status.Total,
|
||||
UsedQuota: status.Used,
|
||||
Balance: status.Balance,
|
||||
ResetAt: status.ResetAt,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user