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
|
name: Free
|
||||||
article_generation: 3
|
article_generation: 3
|
||||||
article_quota_cycle: lifetime
|
article_quota_cycle: lifetime
|
||||||
|
ai_points_monthly: 0
|
||||||
|
ai_point_base_chars: 1000
|
||||||
image_storage_bytes: 10485760
|
image_storage_bytes: 10485760
|
||||||
company_limit: 1
|
company_limit: 1
|
||||||
subscription_duration: 72h
|
subscription_duration: 72h
|
||||||
@@ -56,6 +58,8 @@ membership:
|
|||||||
name: Plus
|
name: Plus
|
||||||
article_generation: 200
|
article_generation: 200
|
||||||
article_quota_cycle: monthly
|
article_quota_cycle: monthly
|
||||||
|
ai_points_monthly: 0
|
||||||
|
ai_point_base_chars: 1000
|
||||||
image_storage_bytes: 524288000
|
image_storage_bytes: 524288000
|
||||||
company_limit: 1
|
company_limit: 1
|
||||||
subscription_duration: 720h
|
subscription_duration: 720h
|
||||||
@@ -63,6 +67,8 @@ membership:
|
|||||||
name: Pro
|
name: Pro
|
||||||
article_generation: 400
|
article_generation: 400
|
||||||
article_quota_cycle: monthly
|
article_quota_cycle: monthly
|
||||||
|
ai_points_monthly: 1500
|
||||||
|
ai_point_base_chars: 1000
|
||||||
image_storage_bytes: 1073741824
|
image_storage_bytes: 1073741824
|
||||||
company_limit: 2
|
company_limit: 2
|
||||||
subscription_duration: 720h
|
subscription_duration: 720h
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ membership:
|
|||||||
name: Free
|
name: Free
|
||||||
article_generation: 3
|
article_generation: 3
|
||||||
article_quota_cycle: lifetime
|
article_quota_cycle: lifetime
|
||||||
|
ai_points_monthly: 0
|
||||||
|
ai_point_base_chars: 1000
|
||||||
image_storage_bytes: 10485760
|
image_storage_bytes: 10485760
|
||||||
company_limit: 1
|
company_limit: 1
|
||||||
subscription_duration: 72h
|
subscription_duration: 72h
|
||||||
@@ -80,6 +82,8 @@ membership:
|
|||||||
name: Plus
|
name: Plus
|
||||||
article_generation: 200
|
article_generation: 200
|
||||||
article_quota_cycle: monthly
|
article_quota_cycle: monthly
|
||||||
|
ai_points_monthly: 0
|
||||||
|
ai_point_base_chars: 1000
|
||||||
image_storage_bytes: 524288000
|
image_storage_bytes: 524288000
|
||||||
company_limit: 1
|
company_limit: 1
|
||||||
subscription_duration: 720h
|
subscription_duration: 720h
|
||||||
@@ -87,6 +91,8 @@ membership:
|
|||||||
name: Pro
|
name: Pro
|
||||||
article_generation: 400
|
article_generation: 400
|
||||||
article_quota_cycle: monthly
|
article_quota_cycle: monthly
|
||||||
|
ai_points_monthly: 1500
|
||||||
|
ai_point_base_chars: 1000
|
||||||
image_storage_bytes: 1073741824
|
image_storage_bytes: 1073741824
|
||||||
company_limit: 2
|
company_limit: 2
|
||||||
subscription_duration: 720h
|
subscription_duration: 720h
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ membership:
|
|||||||
name: Free
|
name: Free
|
||||||
article_generation: 3
|
article_generation: 3
|
||||||
article_quota_cycle: lifetime
|
article_quota_cycle: lifetime
|
||||||
|
ai_points_monthly: 0
|
||||||
|
ai_point_base_chars: 1000
|
||||||
image_storage_bytes: 10485760
|
image_storage_bytes: 10485760
|
||||||
company_limit: 1
|
company_limit: 1
|
||||||
subscription_duration: 72h
|
subscription_duration: 72h
|
||||||
@@ -100,6 +102,8 @@ membership:
|
|||||||
name: Plus
|
name: Plus
|
||||||
article_generation: 200
|
article_generation: 200
|
||||||
article_quota_cycle: monthly
|
article_quota_cycle: monthly
|
||||||
|
ai_points_monthly: 0
|
||||||
|
ai_point_base_chars: 1000
|
||||||
image_storage_bytes: 524288000
|
image_storage_bytes: 524288000
|
||||||
company_limit: 1
|
company_limit: 1
|
||||||
subscription_duration: 720h
|
subscription_duration: 720h
|
||||||
@@ -107,6 +111,8 @@ membership:
|
|||||||
name: Pro
|
name: Pro
|
||||||
article_generation: 400
|
article_generation: 400
|
||||||
article_quota_cycle: monthly
|
article_quota_cycle: monthly
|
||||||
|
ai_points_monthly: 1500
|
||||||
|
ai_point_base_chars: 1000
|
||||||
image_storage_bytes: 1073741824
|
image_storage_bytes: 1073741824
|
||||||
company_limit: 2
|
company_limit: 2
|
||||||
subscription_duration: 720h
|
subscription_duration: 720h
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ type MembershipPlanConfig struct {
|
|||||||
Name string `mapstructure:"name"`
|
Name string `mapstructure:"name"`
|
||||||
ArticleGeneration int `mapstructure:"article_generation"`
|
ArticleGeneration int `mapstructure:"article_generation"`
|
||||||
ArticleQuotaCycle string `mapstructure:"article_quota_cycle"`
|
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"`
|
ImageStorageBytes int64 `mapstructure:"image_storage_bytes"`
|
||||||
CompanyLimit int `mapstructure:"company_limit"`
|
CompanyLimit int `mapstructure:"company_limit"`
|
||||||
SubscriptionDuration time.Duration `mapstructure:"subscription_duration"`
|
SubscriptionDuration time.Duration `mapstructure:"subscription_duration"`
|
||||||
@@ -154,6 +156,8 @@ type MembershipPlanConfig struct {
|
|||||||
type PlanQuotaPolicy struct {
|
type PlanQuotaPolicy struct {
|
||||||
ArticleGeneration int `json:"article_generation"`
|
ArticleGeneration int `json:"article_generation"`
|
||||||
ArticleQuotaCycle string `json:"article_quota_cycle"`
|
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"`
|
ImageStorageBytes int64 `json:"image_storage_bytes"`
|
||||||
BrandLimit int `json:"brand_limit"`
|
BrandLimit int `json:"brand_limit"`
|
||||||
SubscriptionDurationSecs int64 `json:"subscription_duration_seconds,omitempty"`
|
SubscriptionDurationSecs int64 `json:"subscription_duration_seconds,omitempty"`
|
||||||
@@ -174,6 +178,8 @@ func (p MembershipPlanConfig) QuotaPolicy() PlanQuotaPolicy {
|
|||||||
policy := PlanQuotaPolicy{
|
policy := PlanQuotaPolicy{
|
||||||
ArticleGeneration: maxInt(p.ArticleGeneration, 0),
|
ArticleGeneration: maxInt(p.ArticleGeneration, 0),
|
||||||
ArticleQuotaCycle: normalizeArticleQuotaCycle(p.ArticleQuotaCycle, p.Code),
|
ArticleQuotaCycle: normalizeArticleQuotaCycle(p.ArticleQuotaCycle, p.Code),
|
||||||
|
AIPointsMonthly: maxInt(p.AIPointsMonthly, 0),
|
||||||
|
AIPointBaseChars: normalizeAIPointBaseChars(p.AIPointBaseChars),
|
||||||
ImageStorageBytes: maxInt64(p.ImageStorageBytes, 0),
|
ImageStorageBytes: maxInt64(p.ImageStorageBytes, 0),
|
||||||
BrandLimit: maxInt(p.CompanyLimit, 0),
|
BrandLimit: maxInt(p.CompanyLimit, 0),
|
||||||
ContactAdminOnExpiry: p.ContactAdminOnExpiry,
|
ContactAdminOnExpiry: p.ContactAdminOnExpiry,
|
||||||
@@ -695,6 +701,10 @@ func normalizeMembershipConfig(cfg *MembershipConfig) {
|
|||||||
if plan.ArticleGeneration < 0 {
|
if plan.ArticleGeneration < 0 {
|
||||||
plan.ArticleGeneration = 0
|
plan.ArticleGeneration = 0
|
||||||
}
|
}
|
||||||
|
if plan.AIPointsMonthly < 0 {
|
||||||
|
plan.AIPointsMonthly = 0
|
||||||
|
}
|
||||||
|
plan.AIPointBaseChars = normalizeAIPointBaseChars(plan.AIPointBaseChars)
|
||||||
if plan.ImageStorageBytes < 0 {
|
if plan.ImageStorageBytes < 0 {
|
||||||
plan.ImageStorageBytes = 0
|
plan.ImageStorageBytes = 0
|
||||||
}
|
}
|
||||||
@@ -725,6 +735,7 @@ func defaultMembershipPlans() []MembershipPlanConfig {
|
|||||||
Name: "Free",
|
Name: "Free",
|
||||||
ArticleGeneration: 3,
|
ArticleGeneration: 3,
|
||||||
ArticleQuotaCycle: ArticleQuotaCycleLifetime,
|
ArticleQuotaCycle: ArticleQuotaCycleLifetime,
|
||||||
|
AIPointBaseChars: 1000,
|
||||||
ImageStorageBytes: 10 * 1024 * 1024,
|
ImageStorageBytes: 10 * 1024 * 1024,
|
||||||
CompanyLimit: 1,
|
CompanyLimit: 1,
|
||||||
SubscriptionDuration: 72 * time.Hour,
|
SubscriptionDuration: 72 * time.Hour,
|
||||||
@@ -735,6 +746,7 @@ func defaultMembershipPlans() []MembershipPlanConfig {
|
|||||||
Name: "Plus",
|
Name: "Plus",
|
||||||
ArticleGeneration: 200,
|
ArticleGeneration: 200,
|
||||||
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
|
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
|
||||||
|
AIPointBaseChars: 1000,
|
||||||
ImageStorageBytes: 500 * 1024 * 1024,
|
ImageStorageBytes: 500 * 1024 * 1024,
|
||||||
CompanyLimit: 1,
|
CompanyLimit: 1,
|
||||||
SubscriptionDuration: 720 * time.Hour,
|
SubscriptionDuration: 720 * time.Hour,
|
||||||
@@ -744,6 +756,8 @@ func defaultMembershipPlans() []MembershipPlanConfig {
|
|||||||
Name: "Pro",
|
Name: "Pro",
|
||||||
ArticleGeneration: 400,
|
ArticleGeneration: 400,
|
||||||
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
|
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
|
||||||
|
AIPointsMonthly: 1500,
|
||||||
|
AIPointBaseChars: 1000,
|
||||||
ImageStorageBytes: 1024 * 1024 * 1024,
|
ImageStorageBytes: 1024 * 1024 * 1024,
|
||||||
CompanyLimit: 2,
|
CompanyLimit: 2,
|
||||||
SubscriptionDuration: 720 * time.Hour,
|
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 {
|
func normalizeArticleQuotaCycle(value, planCode string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
case ArticleQuotaCycleLifetime:
|
case ArticleQuotaCycleLifetime:
|
||||||
|
|||||||
@@ -237,6 +237,12 @@ membership: {}
|
|||||||
if pro.ArticleGeneration != 400 {
|
if pro.ArticleGeneration != 400 {
|
||||||
t.Fatalf("expected pro article generation 400, got %d", pro.ArticleGeneration)
|
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 {
|
if pro.CompanyLimit != 2 {
|
||||||
t.Fatalf("expected pro company limit 2, got %d", pro.CompanyLimit)
|
t.Fatalf("expected pro company limit 2, got %d", pro.CompanyLimit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ type MembershipInfo struct {
|
|||||||
EndAt *string `json:"end_at"`
|
EndAt *string `json:"end_at"`
|
||||||
ArticleGenerationLimit int `json:"article_generation_limit"`
|
ArticleGenerationLimit int `json:"article_generation_limit"`
|
||||||
ArticleQuotaCycle string `json:"article_quota_cycle"`
|
ArticleQuotaCycle string `json:"article_quota_cycle"`
|
||||||
|
AIPointsMonthly int `json:"ai_points_monthly"`
|
||||||
|
AIPointBaseChars int `json:"ai_point_base_chars"`
|
||||||
CompanyLimit int `json:"company_limit"`
|
CompanyLimit int `json:"company_limit"`
|
||||||
ImageStorageBytes int64 `json:"image_storage_bytes"`
|
ImageStorageBytes int64 `json:"image_storage_bytes"`
|
||||||
ContactAdminOnExpiry bool `json:"contact_admin_on_expiry"`
|
ContactAdminOnExpiry bool `json:"contact_admin_on_expiry"`
|
||||||
@@ -271,6 +273,8 @@ func (s *AuthService) loadMembershipInfo(ctx context.Context, tenantID int64) (*
|
|||||||
EndAt: formatTimeValuePtr(access.EndAt),
|
EndAt: formatTimeValuePtr(access.EndAt),
|
||||||
ArticleGenerationLimit: access.ArticleGenerationLimit(),
|
ArticleGenerationLimit: access.ArticleGenerationLimit(),
|
||||||
ArticleQuotaCycle: access.ArticleQuotaCycle(),
|
ArticleQuotaCycle: access.ArticleQuotaCycle(),
|
||||||
|
AIPointsMonthly: access.AIPointsMonthlyLimit(),
|
||||||
|
AIPointBaseChars: access.AIPointBaseChars(),
|
||||||
CompanyLimit: access.BrandLimit(),
|
CompanyLimit: access.BrandLimit(),
|
||||||
ImageStorageBytes: access.ImageStorageBytes(),
|
ImageStorageBytes: access.ImageStorageBytes(),
|
||||||
ContactAdminOnExpiry: access.Policy.ContactAdminOnExpiry,
|
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) {
|
func (a *TenantPlanAccess) ArticleQuotaWindow(now time.Time) (time.Time, time.Time, *time.Time) {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return time.Time{}, time.Time{}, nil
|
return time.Time{}, time.Time{}, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user