Merge pull request #1193 from trheyi/main
Enhance user type management with pricing and status fields
This commit is contained in:
commit
6f85e78a57
7 changed files with 723 additions and 159 deletions
282
data/bindata.go
282
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -116,15 +116,18 @@ var (
|
|||
|
||||
// DefaultTypeFields contains basic type fields
|
||||
DefaultTypeFields = []interface{}{
|
||||
"id", "type_id", "name", "description", "is_active", "is_default", "sort_order",
|
||||
"default_role_id", "max_sessions", "session_timeout", "created_at", "updated_at",
|
||||
"id", "type_id", "name", "description", "is_active", "is_default", "sort_order", "status",
|
||||
"default_role_id", "max_sessions", "session_timeout", "price_daily", "price_monthly",
|
||||
"price_yearly", "credits_monthly", "created_at", "updated_at",
|
||||
}
|
||||
|
||||
// DefaultTypeDetailFields contains all type fields including configuration and metadata
|
||||
DefaultTypeDetailFields = []interface{}{
|
||||
"id", "type_id", "name", "description", "default_role_id", "schema", "metadata",
|
||||
"is_active", "is_default", "sort_order", "max_sessions", "session_timeout",
|
||||
"password_policy", "features", "limits", "created_at", "updated_at",
|
||||
"is_active", "is_default", "sort_order", "status", "max_sessions", "session_timeout",
|
||||
"password_policy", "features", "limits", "price_daily", "price_monthly", "price_yearly",
|
||||
"credits_monthly", "introduction", "sale_type", "sale_link", "sale_price_label",
|
||||
"sale_description", "created_at", "updated_at",
|
||||
}
|
||||
|
||||
// DefaultTeamFields contains basic team fields
|
||||
|
|
|
|||
|
|
@ -290,3 +290,167 @@ func (u *DefaultUser) SetTypeConfiguration(ctx context.Context, typeID string, c
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTypePricing retrieves pricing information for a type
|
||||
func (u *DefaultUser) GetTypePricing(ctx context.Context, typeID string) (maps.MapStrAny, error) {
|
||||
m := model.Select(u.typeModel)
|
||||
types, err := m.Get(model.QueryParam{
|
||||
Select: []interface{}{
|
||||
"type_id", "name", "price_daily", "price_monthly", "price_yearly",
|
||||
"credits_monthly", "introduction", "sale_type", "sale_link",
|
||||
"sale_price_label", "sale_description", "status",
|
||||
},
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "type_id", Value: typeID},
|
||||
},
|
||||
Limit: 1,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(ErrFailedToGetType, err)
|
||||
}
|
||||
|
||||
if len(types) == 0 {
|
||||
return nil, fmt.Errorf(ErrTypeNotFound)
|
||||
}
|
||||
|
||||
return types[0], nil
|
||||
}
|
||||
|
||||
// GetPublishedTypes retrieves all published types with pricing information
|
||||
func (u *DefaultUser) GetPublishedTypes(ctx context.Context, param model.QueryParam) ([]maps.MapStr, error) {
|
||||
// Add published status filter
|
||||
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||
Column: "status",
|
||||
Value: "published",
|
||||
})
|
||||
|
||||
// Add active filter
|
||||
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||
Column: "is_active",
|
||||
Value: true,
|
||||
})
|
||||
|
||||
// Set default select fields if not provided
|
||||
if param.Select == nil {
|
||||
param.Select = []interface{}{
|
||||
"type_id", "name", "description", "price_daily", "price_monthly", "price_yearly",
|
||||
"credits_monthly", "introduction", "sale_type", "sale_link",
|
||||
"sale_price_label", "sale_description", "sort_order", "status", "is_active", "features", "limits",
|
||||
}
|
||||
}
|
||||
|
||||
// Default ordering by sort_order
|
||||
if param.Orders == nil {
|
||||
param.Orders = []model.QueryOrder{
|
||||
{Column: "sort_order", Option: "asc"},
|
||||
}
|
||||
}
|
||||
|
||||
m := model.Select(u.typeModel)
|
||||
types, err := m.Get(param)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(ErrFailedToGetType, err)
|
||||
}
|
||||
|
||||
return types, nil
|
||||
}
|
||||
|
||||
// SetTypePricing updates pricing information for a type
|
||||
func (u *DefaultUser) SetTypePricing(ctx context.Context, typeID string, pricing maps.MapStrAny) error {
|
||||
// Prepare update data - only allow pricing-related fields
|
||||
updateData := maps.MapStrAny{}
|
||||
|
||||
if priceDaily, ok := pricing["price_daily"]; ok {
|
||||
updateData["price_daily"] = priceDaily
|
||||
}
|
||||
|
||||
if priceMonthly, ok := pricing["price_monthly"]; ok {
|
||||
updateData["price_monthly"] = priceMonthly
|
||||
}
|
||||
|
||||
if priceYearly, ok := pricing["price_yearly"]; ok {
|
||||
updateData["price_yearly"] = priceYearly
|
||||
}
|
||||
|
||||
if creditsMonthly, ok := pricing["credits_monthly"]; ok {
|
||||
updateData["credits_monthly"] = creditsMonthly
|
||||
}
|
||||
|
||||
if introduction, ok := pricing["introduction"]; ok {
|
||||
updateData["introduction"] = introduction
|
||||
}
|
||||
|
||||
if saleType, ok := pricing["sale_type"]; ok {
|
||||
updateData["sale_type"] = saleType
|
||||
}
|
||||
|
||||
if saleLink, ok := pricing["sale_link"]; ok {
|
||||
updateData["sale_link"] = saleLink
|
||||
}
|
||||
|
||||
if salePriceLabel, ok := pricing["sale_price_label"]; ok {
|
||||
updateData["sale_price_label"] = salePriceLabel
|
||||
}
|
||||
|
||||
if saleDescription, ok := pricing["sale_description"]; ok {
|
||||
updateData["sale_description"] = saleDescription
|
||||
}
|
||||
|
||||
// Skip update if no pricing fields provided
|
||||
if len(updateData) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := model.Select(u.typeModel)
|
||||
affected, err := m.UpdateWhere(model.QueryParam{
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "type_id", Value: typeID},
|
||||
},
|
||||
Limit: 1, // Safety: ensure only one record is updated
|
||||
}, updateData)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(ErrFailedToUpdateType, err)
|
||||
}
|
||||
|
||||
if affected == 0 {
|
||||
return fmt.Errorf(ErrTypeNotFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTypeStatus updates the status of a type (draft/published/archived)
|
||||
func (u *DefaultUser) UpdateTypeStatus(ctx context.Context, typeID string, status string) error {
|
||||
// Validate status
|
||||
validStatuses := map[string]bool{
|
||||
"draft": true,
|
||||
"published": true,
|
||||
"archived": true,
|
||||
}
|
||||
|
||||
if !validStatuses[status] {
|
||||
return fmt.Errorf("invalid status: %s, must be one of: draft, published, archived", status)
|
||||
}
|
||||
|
||||
m := model.Select(u.typeModel)
|
||||
affected, err := m.UpdateWhere(model.QueryParam{
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "type_id", Value: typeID},
|
||||
},
|
||||
Limit: 1,
|
||||
}, maps.MapStrAny{
|
||||
"status": status,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(ErrFailedToUpdateType, err)
|
||||
}
|
||||
|
||||
if affected == 0 {
|
||||
return fmt.Errorf(ErrTypeNotFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,20 +13,30 @@ import (
|
|||
|
||||
// TestTypeData represents test type data structure
|
||||
type TestTypeData struct {
|
||||
TypeID string `json:"type_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
DefaultRoleID string `json:"default_role_id"`
|
||||
MaxSessions *int `json:"max_sessions"`
|
||||
SessionTimeout int `json:"session_timeout"`
|
||||
Schema map[string]interface{} `json:"schema"`
|
||||
Features map[string]interface{} `json:"features"`
|
||||
Limits map[string]interface{} `json:"limits"`
|
||||
PasswordPolicy map[string]interface{} `json:"password_policy"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
TypeID string `json:"type_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Status string `json:"status"`
|
||||
DefaultRoleID string `json:"default_role_id"`
|
||||
MaxSessions *int `json:"max_sessions"`
|
||||
SessionTimeout int `json:"session_timeout"`
|
||||
PriceDaily int `json:"price_daily"`
|
||||
PriceMonthly int `json:"price_monthly"`
|
||||
PriceYearly int `json:"price_yearly"`
|
||||
CreditsMonthly int `json:"credits_monthly"`
|
||||
Introduction string `json:"introduction"`
|
||||
SaleType string `json:"sale_type"`
|
||||
SaleLink string `json:"sale_link"`
|
||||
SalePriceLabel string `json:"sale_price_label"`
|
||||
SaleDescription string `json:"sale_description"`
|
||||
Schema map[string]interface{} `json:"schema"`
|
||||
Features map[string]interface{} `json:"features"`
|
||||
Limits map[string]interface{} `json:"limits"`
|
||||
PasswordPolicy map[string]interface{} `json:"password_policy"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
func TestTypeBasicOperations(t *testing.T) {
|
||||
|
|
@ -810,3 +820,307 @@ func TestTypeErrorHandling(t *testing.T) {
|
|||
assert.GreaterOrEqual(t, count, int64(0)) // Should handle complex filters without error
|
||||
})
|
||||
}
|
||||
|
||||
func TestTypePricingOperations(t *testing.T) {
|
||||
prepare(t)
|
||||
defer clean()
|
||||
|
||||
ctx := context.Background()
|
||||
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||
|
||||
// Create test types with pricing information
|
||||
testTypes := []struct {
|
||||
TypeID string
|
||||
Name string
|
||||
PriceDaily int
|
||||
PriceMonthly int
|
||||
PriceYearly int
|
||||
CreditsMonthly int
|
||||
Introduction string
|
||||
SaleType string
|
||||
SaleLink string
|
||||
SalePriceLabel string
|
||||
SaleDescription string
|
||||
Status string
|
||||
}{
|
||||
{
|
||||
TypeID: "free_" + testUUID,
|
||||
Name: "Free Plan",
|
||||
PriceDaily: 0,
|
||||
PriceMonthly: 0,
|
||||
PriceYearly: 0,
|
||||
CreditsMonthly: 1000,
|
||||
Introduction: "Perfect for personal use and small projects",
|
||||
SaleType: "online",
|
||||
SaleLink: "",
|
||||
Status: "published",
|
||||
},
|
||||
{
|
||||
TypeID: "pro_" + testUUID,
|
||||
Name: "Pro Plan",
|
||||
PriceDaily: 100,
|
||||
PriceMonthly: 2900,
|
||||
PriceYearly: 29900,
|
||||
CreditsMonthly: 10000,
|
||||
Introduction: "Perfect for professionals and team collaboration",
|
||||
SaleType: "online",
|
||||
SaleLink: "",
|
||||
Status: "published",
|
||||
},
|
||||
{
|
||||
TypeID: "enterprise_" + testUUID,
|
||||
Name: "Enterprise Plan",
|
||||
PriceDaily: 0,
|
||||
PriceMonthly: 0,
|
||||
PriceYearly: 0,
|
||||
CreditsMonthly: 0,
|
||||
Introduction: "For large-scale deployments",
|
||||
SaleType: "offline",
|
||||
SaleLink: "https://example.com/contact-sales",
|
||||
SalePriceLabel: "$999 - $4999 /month",
|
||||
SaleDescription: "Pricing based on deployment scale",
|
||||
Status: "published",
|
||||
},
|
||||
{
|
||||
TypeID: "beta_" + testUUID,
|
||||
Name: "Beta Plan",
|
||||
PriceDaily: 50,
|
||||
PriceMonthly: 1500,
|
||||
PriceYearly: 15000,
|
||||
CreditsMonthly: 5000,
|
||||
Introduction: "Beta testing plan",
|
||||
SaleType: "online",
|
||||
Status: "draft",
|
||||
},
|
||||
}
|
||||
|
||||
// Create all test types
|
||||
for _, testType := range testTypes {
|
||||
typeData := maps.MapStrAny{
|
||||
"type_id": testType.TypeID,
|
||||
"name": testType.Name,
|
||||
"price_daily": testType.PriceDaily,
|
||||
"price_monthly": testType.PriceMonthly,
|
||||
"price_yearly": testType.PriceYearly,
|
||||
"credits_monthly": testType.CreditsMonthly,
|
||||
"introduction": testType.Introduction,
|
||||
"sale_type": testType.SaleType,
|
||||
"sale_link": testType.SaleLink,
|
||||
"sale_price_label": testType.SalePriceLabel,
|
||||
"sale_description": testType.SaleDescription,
|
||||
"status": testType.Status,
|
||||
"is_active": true,
|
||||
}
|
||||
|
||||
_, err := testProvider.CreateType(ctx, typeData)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test GetTypePricing
|
||||
t.Run("GetTypePricing", func(t *testing.T) {
|
||||
pricing, err := testProvider.GetTypePricing(ctx, "pro_"+testUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, pricing)
|
||||
|
||||
assert.Equal(t, "pro_"+testUUID, pricing["type_id"])
|
||||
assert.Equal(t, "Pro Plan", pricing["name"])
|
||||
|
||||
// Handle different numeric types from database
|
||||
priceMonthlyInterface := pricing["price_monthly"]
|
||||
switch v := priceMonthlyInterface.(type) {
|
||||
case int:
|
||||
assert.Equal(t, 2900, v)
|
||||
case int32:
|
||||
assert.Equal(t, int32(2900), v)
|
||||
case int64:
|
||||
assert.Equal(t, int64(2900), v)
|
||||
default:
|
||||
t.Errorf("unexpected price_monthly type: %T, value: %v", priceMonthlyInterface, priceMonthlyInterface)
|
||||
}
|
||||
|
||||
assert.Equal(t, "online", pricing["sale_type"])
|
||||
assert.Equal(t, "published", pricing["status"])
|
||||
})
|
||||
|
||||
// Test GetTypePricing for offline sales type
|
||||
t.Run("GetTypePricing_OfflineSales", func(t *testing.T) {
|
||||
pricing, err := testProvider.GetTypePricing(ctx, "enterprise_"+testUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, pricing)
|
||||
|
||||
assert.Equal(t, "offline", pricing["sale_type"])
|
||||
assert.Equal(t, "https://example.com/contact-sales", pricing["sale_link"])
|
||||
assert.Equal(t, "$999 - $4999 /month", pricing["sale_price_label"])
|
||||
assert.Equal(t, "Pricing based on deployment scale", pricing["sale_description"])
|
||||
})
|
||||
|
||||
// Test SetTypePricing
|
||||
t.Run("SetTypePricing", func(t *testing.T) {
|
||||
newPricing := maps.MapStrAny{
|
||||
"price_monthly": 3900,
|
||||
"price_yearly": 39900,
|
||||
"credits_monthly": 15000,
|
||||
"introduction": "Updated Pro Plan - Now with more features!",
|
||||
"sale_price_label": "Special Offer",
|
||||
}
|
||||
|
||||
err := testProvider.SetTypePricing(ctx, "pro_"+testUUID, newPricing)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify pricing was updated
|
||||
pricing, err := testProvider.GetTypePricing(ctx, "pro_"+testUUID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
priceMonthlyInterface := pricing["price_monthly"]
|
||||
switch v := priceMonthlyInterface.(type) {
|
||||
case int:
|
||||
assert.Equal(t, 3900, v)
|
||||
case int32:
|
||||
assert.Equal(t, int32(3900), v)
|
||||
case int64:
|
||||
assert.Equal(t, int64(3900), v)
|
||||
default:
|
||||
t.Errorf("unexpected price_monthly type: %T, value: %v", priceMonthlyInterface, priceMonthlyInterface)
|
||||
}
|
||||
|
||||
creditsInterface := pricing["credits_monthly"]
|
||||
switch v := creditsInterface.(type) {
|
||||
case int:
|
||||
assert.Equal(t, 15000, v)
|
||||
case int32:
|
||||
assert.Equal(t, int32(15000), v)
|
||||
case int64:
|
||||
assert.Equal(t, int64(15000), v)
|
||||
default:
|
||||
t.Errorf("unexpected credits_monthly type: %T, value: %v", creditsInterface, creditsInterface)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Updated Pro Plan - Now with more features!", pricing["introduction"])
|
||||
})
|
||||
|
||||
// Test UpdateTypeStatus
|
||||
t.Run("UpdateTypeStatus", func(t *testing.T) {
|
||||
// Update from draft to published
|
||||
err := testProvider.UpdateTypeStatus(ctx, "beta_"+testUUID, "published")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify status was updated
|
||||
typeRecord, err := testProvider.GetType(ctx, "beta_"+testUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "published", typeRecord["status"])
|
||||
|
||||
// Test archive status
|
||||
err = testProvider.UpdateTypeStatus(ctx, "beta_"+testUUID, "archived")
|
||||
assert.NoError(t, err)
|
||||
|
||||
typeRecord, err = testProvider.GetType(ctx, "beta_"+testUUID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "archived", typeRecord["status"])
|
||||
})
|
||||
|
||||
// Test UpdateTypeStatus with invalid status
|
||||
t.Run("UpdateTypeStatus_InvalidStatus", func(t *testing.T) {
|
||||
err := testProvider.UpdateTypeStatus(ctx, "pro_"+testUUID, "invalid_status")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid status")
|
||||
})
|
||||
|
||||
// Test GetPublishedTypes
|
||||
t.Run("GetPublishedTypes", func(t *testing.T) {
|
||||
param := model.QueryParam{}
|
||||
types, err := testProvider.GetPublishedTypes(ctx, param)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, types)
|
||||
|
||||
// Should have at least the 3 published types we created
|
||||
publishedCount := 0
|
||||
for _, typeRecord := range types {
|
||||
typeID := typeRecord["type_id"].(string)
|
||||
if strings.Contains(typeID, testUUID) {
|
||||
publishedCount++
|
||||
// Verify status is published
|
||||
assert.Equal(t, "published", typeRecord["status"])
|
||||
|
||||
// Verify is_active is true
|
||||
isActive := typeRecord["is_active"]
|
||||
switch v := isActive.(type) {
|
||||
case bool:
|
||||
assert.True(t, v)
|
||||
case int, int32, int64:
|
||||
assert.NotEqual(t, 0, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.GreaterOrEqual(t, publishedCount, 3) // free, pro, enterprise
|
||||
})
|
||||
|
||||
// Test GetPublishedTypes ordering
|
||||
t.Run("GetPublishedTypes_Ordering", func(t *testing.T) {
|
||||
// Update sort order for testing
|
||||
_ = testProvider.UpdateType(ctx, "free_"+testUUID, maps.MapStrAny{"sort_order": 10})
|
||||
_ = testProvider.UpdateType(ctx, "pro_"+testUUID, maps.MapStrAny{"sort_order": 20})
|
||||
_ = testProvider.UpdateType(ctx, "enterprise_"+testUUID, maps.MapStrAny{"sort_order": 30})
|
||||
|
||||
param := model.QueryParam{
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "type_id", OP: "like", Value: "%_" + testUUID},
|
||||
},
|
||||
}
|
||||
types, err := testProvider.GetPublishedTypes(ctx, param)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(types), 3)
|
||||
|
||||
// Verify ordering by sort_order
|
||||
if len(types) >= 2 {
|
||||
prevSortOrder := -1
|
||||
for _, typeRecord := range types {
|
||||
sortOrderInterface := typeRecord["sort_order"]
|
||||
var sortOrder int
|
||||
switch v := sortOrderInterface.(type) {
|
||||
case int:
|
||||
sortOrder = v
|
||||
case int32:
|
||||
sortOrder = int(v)
|
||||
case int64:
|
||||
sortOrder = int(v)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if prevSortOrder >= 0 {
|
||||
assert.GreaterOrEqual(t, sortOrder, prevSortOrder)
|
||||
}
|
||||
prevSortOrder = sortOrder
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Test SetTypePricing with empty data
|
||||
t.Run("SetTypePricing_EmptyData", func(t *testing.T) {
|
||||
emptyPricing := maps.MapStrAny{}
|
||||
err := testProvider.SetTypePricing(ctx, "pro_"+testUUID, emptyPricing)
|
||||
assert.NoError(t, err) // Should not error, just skip update
|
||||
})
|
||||
|
||||
// Test GetTypePricing for non-existent type
|
||||
t.Run("GetTypePricing_NotFound", func(t *testing.T) {
|
||||
_, err := testProvider.GetTypePricing(ctx, "nonexistent_"+testUUID)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "type not found")
|
||||
})
|
||||
|
||||
// Test SetTypePricing for non-existent type
|
||||
t.Run("SetTypePricing_NotFound", func(t *testing.T) {
|
||||
pricing := maps.MapStrAny{"price_monthly": 1000}
|
||||
err := testProvider.SetTypePricing(ctx, "nonexistent_"+testUUID, pricing)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "type not found")
|
||||
})
|
||||
|
||||
// Test UpdateTypeStatus for non-existent type
|
||||
t.Run("UpdateTypeStatus_NotFound", func(t *testing.T) {
|
||||
err := testProvider.UpdateTypeStatus(ctx, "nonexistent_"+testUUID, "published")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "type not found")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ func LoginThirdParty(providerID string, userinfo *oauthtypes.OIDCUserInfo, login
|
|||
"family_name": userinfo.FamilyName,
|
||||
"picture": userinfo.Picture,
|
||||
"role_id": provider.Register.Role,
|
||||
"type_id": provider.Register.Type,
|
||||
"status": "active",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ type ThirdParty struct {
|
|||
type RegisterConfig struct {
|
||||
Auto bool `json:"auto,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Type string `json:"type,omitempty"` // User type id
|
||||
}
|
||||
|
||||
// YaoClientConfig represents the Yao OpenAPI Client config
|
||||
|
|
|
|||
|
|
@ -98,6 +98,87 @@
|
|||
"default": 0,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"type": "enum",
|
||||
"label": "Status",
|
||||
"comment": "Publishing status for plan management",
|
||||
"option": ["draft", "published", "archived"],
|
||||
"default": "draft",
|
||||
"index": true
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Pricing & Subscription Fields
|
||||
// ============================================================================
|
||||
{
|
||||
"name": "price_daily",
|
||||
"type": "integer",
|
||||
"label": "Daily Price",
|
||||
"comment": "Daily subscription price in cents (e.g., 100 for $1.00)",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "price_monthly",
|
||||
"type": "integer",
|
||||
"label": "Monthly Price",
|
||||
"comment": "Monthly subscription price in cents (e.g., 2900 for $29.00)",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "price_yearly",
|
||||
"type": "integer",
|
||||
"label": "Yearly Price",
|
||||
"comment": "Yearly subscription price in cents (e.g., 29900 for $299.00)",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "credits_monthly",
|
||||
"type": "integer",
|
||||
"label": "Monthly Credits",
|
||||
"comment": "Monthly credits/quota allocation",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "introduction",
|
||||
"type": "text",
|
||||
"label": "Introduction",
|
||||
"comment": "Plan introduction (supports HTML/Markdown)",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "sale_type",
|
||||
"type": "enum",
|
||||
"label": "Sale Type",
|
||||
"comment": "Sales method: online or offline",
|
||||
"option": ["online", "offline"],
|
||||
"default": "online",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "sale_link",
|
||||
"type": "string",
|
||||
"label": "Sale Link",
|
||||
"comment": "Sales link URL (for offline purchases or external payment)",
|
||||
"length": 500,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "sale_price_label",
|
||||
"type": "string",
|
||||
"label": "Sale Price Label",
|
||||
"comment": "Custom price label for offline sales (e.g., '$999 - $4999 /month')",
|
||||
"length": 200,
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "sale_description",
|
||||
"type": "string",
|
||||
"label": "Sale Description",
|
||||
"comment": "Brief description for offline sales (e.g., 'Pricing based on deployment scale')",
|
||||
"length": 500,
|
||||
"nullable": true
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Access Control Fields
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue