mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
feat: Replace 5% grace period with configurable ExtraUsers field (#31629)
* feat: Replace 5% grace period with configurable ExtraUsers field - Rename ExtraSeats to ExtraUsers in license Features struct - Remove fixed 5% grace period and minimum 1 extra user logic - Add configurable ExtraUsers field that allows exact control over additional seats - Update calculateGraceLimit() to use extraUsers parameter directly - When ExtraUsers is nil, defaults to 0 (hard cap with no overage) - Special case maintained: zero user licenses always return 0 grace limit - Update all tests to use new ExtraUsers functionality Closes #31628 Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * feat: eliminate calculateGraceLimit function, use inline baseLimit + extraUsers - Remove calculateGraceLimit function and replace with inline calculation - Allow extraUsers even when baseLimit is 0 (behavioral change) - Update tests to reflect new behavior - Remove TestCalculateGraceLimit since function no longer exists Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * feat: move ExtraUsers field to top level License struct Move ExtraUsers field from Features struct to the top level License struct for better organization and direct access. Update all references in limits.go and limits_test.go to use the new field location. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * feat: use model.NewPointer for creating integer pointers in tests Replace inline function declarations with model.NewPointer calls for cleaner code. Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * feat: reorder ExtraUsers field to be after IsSeatCountEnforced Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * fix: format Go files with gofmt - Remove extra blank line in limits.go - Align struct fields in limits_test.go table test Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> * Fix user limits tests and document ExtraUsers field - Fix TestCreateUserOrGuestSeatCountEnforcement to use ExtraUsers instead of old grace period - Add documentation to ExtraUsers field explaining it as a grace mechanism - Update test comments to reflect hard limit terminology 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Jesse Hallam <lieut-data@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
744d284069
commit
e367872c0b
4 changed files with 127 additions and 114 deletions
|
|
@ -14,21 +14,6 @@ const (
|
|||
maxUsersHardLimit = 5_000
|
||||
)
|
||||
|
||||
// calculateGraceLimit calculates a grace limit that is 5% above the base limit
|
||||
// or at least 1 user above the base limit, whichever is higher.
|
||||
// Special case: if baseLimit is 0, returns 0.
|
||||
func calculateGraceLimit(baseLimit int64) int64 {
|
||||
if baseLimit == 0 {
|
||||
return 0
|
||||
}
|
||||
graceFromPercentage := int64(float64(baseLimit) * 1.05)
|
||||
graceFromFloor := baseLimit + 1
|
||||
if graceFromPercentage > graceFromFloor {
|
||||
return graceFromPercentage
|
||||
}
|
||||
return graceFromFloor
|
||||
}
|
||||
|
||||
func (a *App) GetServerLimits() (*model.ServerLimits, *model.AppError) {
|
||||
limits := &model.ServerLimits{}
|
||||
license := a.License()
|
||||
|
|
@ -38,10 +23,17 @@ func (a *App) GetServerLimits() (*model.ServerLimits, *model.AppError) {
|
|||
limits.MaxUsersLimit = maxUsersLimit
|
||||
limits.MaxUsersHardLimit = maxUsersHardLimit
|
||||
} else if license != nil && license.IsSeatCountEnforced && license.Features != nil && license.Features.Users != nil {
|
||||
// Enforce license limits as required by the license with grace period.
|
||||
// Enforce license limits as required by the license with configurable extra users.
|
||||
licenseUserLimit := int64(*license.Features.Users)
|
||||
limits.MaxUsersLimit = licenseUserLimit
|
||||
limits.MaxUsersHardLimit = calculateGraceLimit(licenseUserLimit)
|
||||
|
||||
// Use ExtraUsers if configured, otherwise default to 0 (no extra users)
|
||||
extraUsers := 0
|
||||
if license.ExtraUsers != nil {
|
||||
extraUsers = *license.ExtraUsers
|
||||
}
|
||||
|
||||
limits.MaxUsersHardLimit = licenseUserLimit + int64(extraUsers)
|
||||
}
|
||||
|
||||
activeUserCount, appErr := a.Srv().Store().User().Count(model.UserCountOptions{})
|
||||
|
|
|
|||
|
|
@ -168,14 +168,16 @@ func TestGetServerLimits(t *testing.T) {
|
|||
require.Equal(t, int64(0), serverLimits.MaxUsersHardLimit)
|
||||
})
|
||||
|
||||
t.Run("licensed server with seat count enforcement shows license limits with grace period", func(t *testing.T) {
|
||||
t.Run("licensed server with seat count enforcement shows license limits with configurable extra users", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userLimit := 100
|
||||
extraUsers := 10
|
||||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = &extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
serverLimits, appErr := th.App.GetServerLimits()
|
||||
|
|
@ -184,7 +186,48 @@ func TestGetServerLimits(t *testing.T) {
|
|||
// InitBasic creates 3 users by default
|
||||
require.Equal(t, int64(3), serverLimits.ActiveUserCount)
|
||||
require.Equal(t, int64(100), serverLimits.MaxUsersLimit)
|
||||
require.Equal(t, int64(105), serverLimits.MaxUsersHardLimit) // 100 + 5% = 105
|
||||
require.Equal(t, int64(110), serverLimits.MaxUsersHardLimit) // 100 + 10 extra users = 110
|
||||
})
|
||||
|
||||
t.Run("licensed server with seat count enforcement and no ExtraUsers configured defaults to zero", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userLimit := 100
|
||||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = nil // Not configured
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
serverLimits, appErr := th.App.GetServerLimits()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// InitBasic creates 3 users by default
|
||||
require.Equal(t, int64(3), serverLimits.ActiveUserCount)
|
||||
require.Equal(t, int64(100), serverLimits.MaxUsersLimit)
|
||||
require.Equal(t, int64(100), serverLimits.MaxUsersHardLimit) // 100 + 0 extra users = 100 (hard cap)
|
||||
})
|
||||
|
||||
t.Run("licensed server with seat count enforcement and zero ExtraUsers creates hard cap", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userLimit := 100
|
||||
extraUsers := 0
|
||||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = &extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
serverLimits, appErr := th.App.GetServerLimits()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// InitBasic creates 3 users by default
|
||||
require.Equal(t, int64(3), serverLimits.ActiveUserCount)
|
||||
require.Equal(t, int64(100), serverLimits.MaxUsersLimit)
|
||||
require.Equal(t, int64(100), serverLimits.MaxUsersHardLimit) // 100 + 0 extra users = 100 (hard cap)
|
||||
})
|
||||
|
||||
t.Run("licensed server with seat count enforcement but no Users feature shows no limits", func(t *testing.T) {
|
||||
|
|
@ -219,7 +262,7 @@ func TestGetServerLimits(t *testing.T) {
|
|||
|
||||
require.Greater(t, serverLimits.ActiveUserCount, int64(0))
|
||||
require.Equal(t, int64(0), serverLimits.MaxUsersLimit)
|
||||
require.Equal(t, int64(0), serverLimits.MaxUsersHardLimit) // No grace for 0 users
|
||||
require.Equal(t, int64(0), serverLimits.MaxUsersHardLimit) // 0 + 0 (default) extra users = 0
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -293,37 +336,41 @@ func TestIsAtUserLimit(t *testing.T) {
|
|||
require.False(t, atLimit)
|
||||
})
|
||||
|
||||
t.Run("at base limit but below grace limit", func(t *testing.T) {
|
||||
t.Run("at base limit but below hard limit with extra users", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
userLimit := 5
|
||||
extraUsers := 2
|
||||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = &extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
// Create 2 additional users to have 5 total (at base limit of 5, but below grace limit of 6)
|
||||
// Create 2 additional users to have 5 total (at base limit of 5, but below hard limit of 7)
|
||||
th.CreateUser()
|
||||
th.CreateUser()
|
||||
|
||||
atLimit, appErr := th.App.isAtUserLimit()
|
||||
require.Nil(t, appErr)
|
||||
require.False(t, atLimit) // Should be false due to grace period
|
||||
require.False(t, atLimit) // Should be false due to extra users
|
||||
})
|
||||
|
||||
t.Run("at grace limit", func(t *testing.T) {
|
||||
t.Run("at hard limit with extra users", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
userLimit := 5
|
||||
extraUsers := 1
|
||||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = &extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
mockUserStore := storemocks.UserStore{}
|
||||
mockUserStore.On("Count", mock.Anything).Return(int64(6), nil) // At grace limit of 6 (5 + 1)
|
||||
mockUserStore.On("Count", mock.Anything).Return(int64(6), nil) // At hard limit of 6 (5 + 1)
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
|
||||
|
|
@ -332,18 +379,20 @@ func TestIsAtUserLimit(t *testing.T) {
|
|||
require.True(t, atLimit)
|
||||
})
|
||||
|
||||
t.Run("above grace limit", func(t *testing.T) {
|
||||
t.Run("above hard limit with extra users", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
userLimit := 5
|
||||
extraUsers := 1
|
||||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = &extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
mockUserStore := storemocks.UserStore{}
|
||||
mockUserStore.On("Count", mock.Anything).Return(int64(7), nil) // Above grace limit of 6
|
||||
mockUserStore.On("Count", mock.Anything).Return(int64(7), nil) // Above hard limit of 6
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
|
||||
|
|
@ -418,39 +467,51 @@ func TestIsAtUserLimit(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGracePeriodBehavior(t *testing.T) {
|
||||
func TestExtraUsersBehavior(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
t.Run("grace period examples", func(t *testing.T) {
|
||||
t.Run("extra users examples", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
licenseUserLimit int
|
||||
expectedBaseLimit int64
|
||||
expectedGraceLimit int64
|
||||
name string
|
||||
licenseUserLimit int
|
||||
extraUsers *int
|
||||
expectedBaseLimit int64
|
||||
expectedHardLimit int64
|
||||
}{
|
||||
{
|
||||
name: "zero license users gets zero grace",
|
||||
licenseUserLimit: 0,
|
||||
expectedBaseLimit: 0,
|
||||
expectedGraceLimit: 0, // Special case: 0 users = 0 grace limit
|
||||
name: "zero license users with extra users",
|
||||
licenseUserLimit: 0,
|
||||
extraUsers: model.NewPointer(5),
|
||||
expectedBaseLimit: 0,
|
||||
expectedHardLimit: 5, // 0 + 5 extra users = 5
|
||||
},
|
||||
{
|
||||
name: "small license uses floor (10 users)",
|
||||
licenseUserLimit: 10,
|
||||
expectedBaseLimit: 10,
|
||||
expectedGraceLimit: 11, // 10 + max(5%, 1) = 10 + 1
|
||||
name: "license with configured extra users",
|
||||
licenseUserLimit: 10,
|
||||
extraUsers: model.NewPointer(2),
|
||||
expectedBaseLimit: 10,
|
||||
expectedHardLimit: 12, // 10 + 2 extra users = 12
|
||||
},
|
||||
{
|
||||
name: "medium license uses percentage (100 users)",
|
||||
licenseUserLimit: 100,
|
||||
expectedBaseLimit: 100,
|
||||
expectedGraceLimit: 105, // 100 + max(5%, 1) = 100 + 5
|
||||
name: "license with zero extra users (hard cap)",
|
||||
licenseUserLimit: 100,
|
||||
extraUsers: model.NewPointer(0),
|
||||
expectedBaseLimit: 100,
|
||||
expectedHardLimit: 100, // 100 + 0 extra users = 100 (hard cap)
|
||||
},
|
||||
{
|
||||
name: "large license uses percentage (1000 users)",
|
||||
licenseUserLimit: 1000,
|
||||
expectedBaseLimit: 1000,
|
||||
expectedGraceLimit: 1050, // 1000 + max(5%, 1) = 1000 + 50
|
||||
name: "license with no extra users configured defaults to zero",
|
||||
licenseUserLimit: 100,
|
||||
extraUsers: nil,
|
||||
expectedBaseLimit: 100,
|
||||
expectedHardLimit: 100, // 100 + 0 (default) extra users = 100 (hard cap)
|
||||
},
|
||||
{
|
||||
name: "license with large number of extra users",
|
||||
licenseUserLimit: 1000,
|
||||
extraUsers: model.NewPointer(200),
|
||||
expectedBaseLimit: 1000,
|
||||
expectedHardLimit: 1200, // 1000 + 200 extra users = 1200
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -462,18 +523,19 @@ func TestGracePeriodBehavior(t *testing.T) {
|
|||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &tt.licenseUserLimit
|
||||
license.ExtraUsers = tt.extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
serverLimits, appErr := th.App.GetServerLimits()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
require.Equal(t, tt.expectedBaseLimit, serverLimits.MaxUsersLimit)
|
||||
require.Equal(t, tt.expectedGraceLimit, serverLimits.MaxUsersHardLimit)
|
||||
require.Equal(t, tt.expectedHardLimit, serverLimits.MaxUsersHardLimit)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unlicensed server has no grace period", func(t *testing.T) {
|
||||
t.Run("unlicensed server has no extra users", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
|
|
@ -482,61 +544,8 @@ func TestGracePeriodBehavior(t *testing.T) {
|
|||
serverLimits, appErr := th.App.GetServerLimits()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Unlicensed servers should not get grace period
|
||||
// Unlicensed servers use hard-coded limits without extra users
|
||||
require.Equal(t, int64(2500), serverLimits.MaxUsersLimit)
|
||||
require.Equal(t, int64(5000), serverLimits.MaxUsersHardLimit) // No grace, stays at 5000
|
||||
require.Equal(t, int64(5000), serverLimits.MaxUsersHardLimit)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCalculateGraceLimit(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
baseLimit int64
|
||||
expected int64
|
||||
}{
|
||||
{
|
||||
name: "zero base limit",
|
||||
baseLimit: 0,
|
||||
expected: 0, // Special case: 0 users = 0 grace limit
|
||||
},
|
||||
{
|
||||
name: "one user base limit",
|
||||
baseLimit: 1,
|
||||
expected: 2, // max(1 * 1.05, 1 + 1) = max(1.05 -> 1, 2) = 2
|
||||
},
|
||||
{
|
||||
name: "small base limit where floor applies",
|
||||
baseLimit: 10,
|
||||
expected: 11, // max(10 * 1.05, 10 + 1) = max(10.5 -> 10, 11) = 11
|
||||
},
|
||||
{
|
||||
name: "small base limit where percentage applies",
|
||||
baseLimit: 20,
|
||||
expected: 21, // max(20 * 1.05, 20 + 1) = max(21, 21) = 21
|
||||
},
|
||||
{
|
||||
name: "medium base limit where percentage applies",
|
||||
baseLimit: 100,
|
||||
expected: 105, // max(100 * 1.05, 100 + 1) = max(105, 101) = 105
|
||||
},
|
||||
{
|
||||
name: "large base limit where percentage applies",
|
||||
baseLimit: 1000,
|
||||
expected: 1050, // max(1000 * 1.05, 1000 + 1) = max(1050, 1001) = 1050
|
||||
},
|
||||
{
|
||||
name: "very large base limit",
|
||||
baseLimit: 5000,
|
||||
expected: 5250, // max(5000 * 1.05, 5000 + 1) = max(5250, 5001) = 5250
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := calculateGraceLimit(tt.baseLimit)
|
||||
require.Equal(t, tt.expected, result, "calculateGraceLimit(%d) = %d, expected %d", tt.baseLimit, result, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,18 +306,20 @@ func TestCreateUserOrGuestSeatCountEnforcement(t *testing.T) {
|
|||
defer th.TearDown()
|
||||
|
||||
userLimit := 5
|
||||
extraUsers := 1
|
||||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = &extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
// Create 3 additional users to reach the grace limit of 6 (3 from InitBasic + 3)
|
||||
// Grace limit for 5 users is 6 (5% grace period)
|
||||
// Create 3 additional users to reach the hard limit of 6 (3 from InitBasic + 3)
|
||||
// Hard limit = 5 base users + 1 extra user = 6 total
|
||||
th.CreateUser()
|
||||
th.CreateUser()
|
||||
th.CreateUser()
|
||||
|
||||
// Now at grace limit - attempting to create another user should fail
|
||||
// Now at hard limit - attempting to create another user should fail
|
||||
user := &model.User{
|
||||
Email: "TestSeatCount@example.com",
|
||||
Username: "seat_test_user",
|
||||
|
|
@ -337,7 +339,8 @@ func TestCreateUserOrGuestSeatCountEnforcement(t *testing.T) {
|
|||
defer th.TearDown()
|
||||
|
||||
userLimit := 5
|
||||
currentUserCount := int64(6) // Over limit
|
||||
extraUsers := 0
|
||||
currentUserCount := int64(6) // Over limit (limit=5, hard limit=5+0=5, current=6)
|
||||
|
||||
mockUserStore := storemocks.UserStore{}
|
||||
mockUserStore.On("Count", mock.Anything).Return(currentUserCount, nil)
|
||||
|
|
@ -353,6 +356,7 @@ func TestCreateUserOrGuestSeatCountEnforcement(t *testing.T) {
|
|||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = &extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
user := &model.User{
|
||||
|
|
@ -445,18 +449,20 @@ func TestCreateUserOrGuestSeatCountEnforcement(t *testing.T) {
|
|||
defer th.TearDown()
|
||||
|
||||
userLimit := 5
|
||||
extraUsers := 1
|
||||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = &extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
// Create 3 additional users to reach the grace limit of 6 (3 from InitBasic + 3)
|
||||
// Grace limit for 5 users is 6 (5% grace period)
|
||||
// Create 3 additional users to reach the hard limit of 6 (3 from InitBasic + 3)
|
||||
// Hard limit = 5 base users + 1 extra user = 6 total
|
||||
th.CreateUser()
|
||||
th.CreateUser()
|
||||
th.CreateUser()
|
||||
|
||||
// Now at grace limit - attempting to create a guest should fail
|
||||
// Now at hard limit - attempting to create a guest should fail
|
||||
user := &model.User{
|
||||
Email: "TestSeatCountGuest@example.com",
|
||||
Username: "seat_test_guest",
|
||||
|
|
@ -475,9 +481,11 @@ func TestCreateUserOrGuestSeatCountEnforcement(t *testing.T) {
|
|||
defer th.TearDown()
|
||||
|
||||
userLimit := 5
|
||||
extraUsers := 0
|
||||
license := model.NewTestLicense("")
|
||||
license.IsSeatCountEnforced = true
|
||||
license.Features.Users = &userLimit
|
||||
license.ExtraUsers = &extraUsers
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
// InitBasic creates 3 users, so we're under the limit of 5
|
||||
|
|
|
|||
|
|
@ -68,7 +68,11 @@ type License struct {
|
|||
IsTrial bool `json:"is_trial"`
|
||||
IsGovSku bool `json:"is_gov_sku"`
|
||||
IsSeatCountEnforced bool `json:"is_seat_count_enforced"`
|
||||
SignupJWT *string `json:"signup_jwt"`
|
||||
// ExtraUsers provides a grace mechanism that allows a configurable number of users
|
||||
// beyond the base license limit before restricting user creation. When nil, defaults to 0.
|
||||
// For example: 100 licensed users + 5 ExtraUsers = 105 total allowed users.
|
||||
ExtraUsers *int `json:"extra_users"`
|
||||
SignupJWT *string `json:"signup_jwt"`
|
||||
}
|
||||
|
||||
type Customer struct {
|
||||
|
|
|
|||
Loading…
Reference in a new issue