feat(server/membership): add ai_points plan policy fields
Introduces ai_points_monthly and ai_point_base_chars on the membership plan policy, exposes them through TenantPlanAccess and MembershipInfo, and seeds Pro with a 1500-point monthly allowance across the local, server, and deploy configs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,8 @@ membership:
|
||||
name: Free
|
||||
article_generation: 3
|
||||
article_quota_cycle: lifetime
|
||||
ai_points_monthly: 0
|
||||
ai_point_base_chars: 1000
|
||||
image_storage_bytes: 10485760
|
||||
company_limit: 1
|
||||
subscription_duration: 72h
|
||||
@@ -56,6 +58,8 @@ membership:
|
||||
name: Plus
|
||||
article_generation: 200
|
||||
article_quota_cycle: monthly
|
||||
ai_points_monthly: 0
|
||||
ai_point_base_chars: 1000
|
||||
image_storage_bytes: 524288000
|
||||
company_limit: 1
|
||||
subscription_duration: 720h
|
||||
@@ -63,6 +67,8 @@ membership:
|
||||
name: Pro
|
||||
article_generation: 400
|
||||
article_quota_cycle: monthly
|
||||
ai_points_monthly: 1500
|
||||
ai_point_base_chars: 1000
|
||||
image_storage_bytes: 1073741824
|
||||
company_limit: 2
|
||||
subscription_duration: 720h
|
||||
|
||||
@@ -72,6 +72,8 @@ membership:
|
||||
name: Free
|
||||
article_generation: 3
|
||||
article_quota_cycle: lifetime
|
||||
ai_points_monthly: 0
|
||||
ai_point_base_chars: 1000
|
||||
image_storage_bytes: 10485760
|
||||
company_limit: 1
|
||||
subscription_duration: 72h
|
||||
@@ -80,6 +82,8 @@ membership:
|
||||
name: Plus
|
||||
article_generation: 200
|
||||
article_quota_cycle: monthly
|
||||
ai_points_monthly: 0
|
||||
ai_point_base_chars: 1000
|
||||
image_storage_bytes: 524288000
|
||||
company_limit: 1
|
||||
subscription_duration: 720h
|
||||
@@ -87,6 +91,8 @@ membership:
|
||||
name: Pro
|
||||
article_generation: 400
|
||||
article_quota_cycle: monthly
|
||||
ai_points_monthly: 1500
|
||||
ai_point_base_chars: 1000
|
||||
image_storage_bytes: 1073741824
|
||||
company_limit: 2
|
||||
subscription_duration: 720h
|
||||
|
||||
@@ -92,6 +92,8 @@ membership:
|
||||
name: Free
|
||||
article_generation: 3
|
||||
article_quota_cycle: lifetime
|
||||
ai_points_monthly: 0
|
||||
ai_point_base_chars: 1000
|
||||
image_storage_bytes: 10485760
|
||||
company_limit: 1
|
||||
subscription_duration: 72h
|
||||
@@ -100,6 +102,8 @@ membership:
|
||||
name: Plus
|
||||
article_generation: 200
|
||||
article_quota_cycle: monthly
|
||||
ai_points_monthly: 0
|
||||
ai_point_base_chars: 1000
|
||||
image_storage_bytes: 524288000
|
||||
company_limit: 1
|
||||
subscription_duration: 720h
|
||||
@@ -107,6 +111,8 @@ membership:
|
||||
name: Pro
|
||||
article_generation: 400
|
||||
article_quota_cycle: monthly
|
||||
ai_points_monthly: 1500
|
||||
ai_point_base_chars: 1000
|
||||
image_storage_bytes: 1073741824
|
||||
company_limit: 2
|
||||
subscription_duration: 720h
|
||||
|
||||
@@ -145,6 +145,8 @@ type MembershipPlanConfig struct {
|
||||
Name string `mapstructure:"name"`
|
||||
ArticleGeneration int `mapstructure:"article_generation"`
|
||||
ArticleQuotaCycle string `mapstructure:"article_quota_cycle"`
|
||||
AIPointsMonthly int `mapstructure:"ai_points_monthly"`
|
||||
AIPointBaseChars int `mapstructure:"ai_point_base_chars"`
|
||||
ImageStorageBytes int64 `mapstructure:"image_storage_bytes"`
|
||||
CompanyLimit int `mapstructure:"company_limit"`
|
||||
SubscriptionDuration time.Duration `mapstructure:"subscription_duration"`
|
||||
@@ -154,6 +156,8 @@ type MembershipPlanConfig struct {
|
||||
type PlanQuotaPolicy struct {
|
||||
ArticleGeneration int `json:"article_generation"`
|
||||
ArticleQuotaCycle string `json:"article_quota_cycle"`
|
||||
AIPointsMonthly int `json:"ai_points_monthly"`
|
||||
AIPointBaseChars int `json:"ai_point_base_chars"`
|
||||
ImageStorageBytes int64 `json:"image_storage_bytes"`
|
||||
BrandLimit int `json:"brand_limit"`
|
||||
SubscriptionDurationSecs int64 `json:"subscription_duration_seconds,omitempty"`
|
||||
@@ -174,6 +178,8 @@ func (p MembershipPlanConfig) QuotaPolicy() PlanQuotaPolicy {
|
||||
policy := PlanQuotaPolicy{
|
||||
ArticleGeneration: maxInt(p.ArticleGeneration, 0),
|
||||
ArticleQuotaCycle: normalizeArticleQuotaCycle(p.ArticleQuotaCycle, p.Code),
|
||||
AIPointsMonthly: maxInt(p.AIPointsMonthly, 0),
|
||||
AIPointBaseChars: normalizeAIPointBaseChars(p.AIPointBaseChars),
|
||||
ImageStorageBytes: maxInt64(p.ImageStorageBytes, 0),
|
||||
BrandLimit: maxInt(p.CompanyLimit, 0),
|
||||
ContactAdminOnExpiry: p.ContactAdminOnExpiry,
|
||||
@@ -695,6 +701,10 @@ func normalizeMembershipConfig(cfg *MembershipConfig) {
|
||||
if plan.ArticleGeneration < 0 {
|
||||
plan.ArticleGeneration = 0
|
||||
}
|
||||
if plan.AIPointsMonthly < 0 {
|
||||
plan.AIPointsMonthly = 0
|
||||
}
|
||||
plan.AIPointBaseChars = normalizeAIPointBaseChars(plan.AIPointBaseChars)
|
||||
if plan.ImageStorageBytes < 0 {
|
||||
plan.ImageStorageBytes = 0
|
||||
}
|
||||
@@ -725,6 +735,7 @@ func defaultMembershipPlans() []MembershipPlanConfig {
|
||||
Name: "Free",
|
||||
ArticleGeneration: 3,
|
||||
ArticleQuotaCycle: ArticleQuotaCycleLifetime,
|
||||
AIPointBaseChars: 1000,
|
||||
ImageStorageBytes: 10 * 1024 * 1024,
|
||||
CompanyLimit: 1,
|
||||
SubscriptionDuration: 72 * time.Hour,
|
||||
@@ -735,6 +746,7 @@ func defaultMembershipPlans() []MembershipPlanConfig {
|
||||
Name: "Plus",
|
||||
ArticleGeneration: 200,
|
||||
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
|
||||
AIPointBaseChars: 1000,
|
||||
ImageStorageBytes: 500 * 1024 * 1024,
|
||||
CompanyLimit: 1,
|
||||
SubscriptionDuration: 720 * time.Hour,
|
||||
@@ -744,6 +756,8 @@ func defaultMembershipPlans() []MembershipPlanConfig {
|
||||
Name: "Pro",
|
||||
ArticleGeneration: 400,
|
||||
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
|
||||
AIPointsMonthly: 1500,
|
||||
AIPointBaseChars: 1000,
|
||||
ImageStorageBytes: 1024 * 1024 * 1024,
|
||||
CompanyLimit: 2,
|
||||
SubscriptionDuration: 720 * time.Hour,
|
||||
@@ -751,6 +765,13 @@ func defaultMembershipPlans() []MembershipPlanConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAIPointBaseChars(value int) int {
|
||||
if value <= 0 {
|
||||
return 1000
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeArticleQuotaCycle(value, planCode string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case ArticleQuotaCycleLifetime:
|
||||
|
||||
@@ -237,6 +237,12 @@ membership: {}
|
||||
if pro.ArticleGeneration != 400 {
|
||||
t.Fatalf("expected pro article generation 400, got %d", pro.ArticleGeneration)
|
||||
}
|
||||
if pro.AIPointsMonthly != 1500 {
|
||||
t.Fatalf("expected pro ai points 1500, got %d", pro.AIPointsMonthly)
|
||||
}
|
||||
if pro.AIPointBaseChars != 1000 {
|
||||
t.Fatalf("expected pro ai point base chars 1000, got %d", pro.AIPointBaseChars)
|
||||
}
|
||||
if pro.CompanyLimit != 2 {
|
||||
t.Fatalf("expected pro company limit 2, got %d", pro.CompanyLimit)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ type MembershipInfo struct {
|
||||
EndAt *string `json:"end_at"`
|
||||
ArticleGenerationLimit int `json:"article_generation_limit"`
|
||||
ArticleQuotaCycle string `json:"article_quota_cycle"`
|
||||
AIPointsMonthly int `json:"ai_points_monthly"`
|
||||
AIPointBaseChars int `json:"ai_point_base_chars"`
|
||||
CompanyLimit int `json:"company_limit"`
|
||||
ImageStorageBytes int64 `json:"image_storage_bytes"`
|
||||
ContactAdminOnExpiry bool `json:"contact_admin_on_expiry"`
|
||||
@@ -271,6 +273,8 @@ func (s *AuthService) loadMembershipInfo(ctx context.Context, tenantID int64) (*
|
||||
EndAt: formatTimeValuePtr(access.EndAt),
|
||||
ArticleGenerationLimit: access.ArticleGenerationLimit(),
|
||||
ArticleQuotaCycle: access.ArticleQuotaCycle(),
|
||||
AIPointsMonthly: access.AIPointsMonthlyLimit(),
|
||||
AIPointBaseChars: access.AIPointBaseChars(),
|
||||
CompanyLimit: access.BrandLimit(),
|
||||
ImageStorageBytes: access.ImageStorageBytes(),
|
||||
ContactAdminOnExpiry: access.Policy.ContactAdminOnExpiry,
|
||||
|
||||
@@ -98,6 +98,38 @@ func (a *TenantPlanAccess) ArticleQuotaCycle() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *TenantPlanAccess) AIPointsMonthlyLimit() int {
|
||||
if a == nil {
|
||||
return 0
|
||||
}
|
||||
return maxInt(a.Policy.AIPointsMonthly, 0)
|
||||
}
|
||||
|
||||
func (a *TenantPlanAccess) AIPointBaseChars() int {
|
||||
if a == nil || a.Policy.AIPointBaseChars <= 0 {
|
||||
return 1000
|
||||
}
|
||||
return a.Policy.AIPointBaseChars
|
||||
}
|
||||
|
||||
func (a *TenantPlanAccess) AIPointsWindow(now time.Time) (time.Time, time.Time, *time.Time) {
|
||||
if a == nil {
|
||||
return time.Time{}, time.Time{}, nil
|
||||
}
|
||||
|
||||
windowStart := a.StartAt.UTC()
|
||||
windowEnd := a.EndAt.UTC()
|
||||
cycleStart, cycleEnd := monthlyCycleBounds(windowStart, now.UTC())
|
||||
if cycleStart.Before(windowStart) {
|
||||
cycleStart = windowStart
|
||||
}
|
||||
if !windowEnd.IsZero() && cycleEnd.After(windowEnd) {
|
||||
cycleEnd = windowEnd
|
||||
}
|
||||
resetAt := cycleEnd
|
||||
return cycleStart, cycleEnd, &resetAt
|
||||
}
|
||||
|
||||
func (a *TenantPlanAccess) ArticleQuotaWindow(now time.Time) (time.Time, time.Time, *time.Time) {
|
||||
if a == nil {
|
||||
return time.Time{}, time.Time{}, nil
|
||||
|
||||
Reference in New Issue
Block a user