[MM-47565]: In product true up for yearly products (#21704)
Этот коммит содержится в:
@@ -83,6 +83,8 @@ type AppIface interface {
|
|||||||
ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError)
|
ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError)
|
||||||
// ConvertUserToBot converts a user to bot.
|
// ConvertUserToBot converts a user to bot.
|
||||||
ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError)
|
ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError)
|
||||||
|
// Create/ Update a subscription history event
|
||||||
|
SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error)
|
||||||
// CreateBot creates the given bot and corresponding user.
|
// CreateBot creates the given bot and corresponding user.
|
||||||
CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError)
|
CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError)
|
||||||
// CreateChannelScheme creates a new Scheme of scope channel and assigns it to the channel.
|
// CreateChannelScheme creates a new Scheme of scope channel and assigns it to the channel.
|
||||||
|
|||||||
17
app/cloud.go
17
app/cloud.go
@@ -230,3 +230,20 @@ func (a *App) SendNoCardPaymentFailedEmail() *model.AppError {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create/ Update a subscription history event
|
||||||
|
func (a *App) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
|
||||||
|
license := a.Srv().License()
|
||||||
|
|
||||||
|
// No need to create a Subscription History Event if the license isn't cloud
|
||||||
|
if !license.IsCloud() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user count
|
||||||
|
userCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return a.Cloud().CreateOrUpdateSubscriptionHistoryEvent(userID, int(userCount))
|
||||||
|
}
|
||||||
|
|||||||
@@ -15441,6 +15441,28 @@ func (a *OpenTracingAppLayer) SendPaymentFailedEmail(failedPayment *model.Failed
|
|||||||
return resultVar0
|
return resultVar0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *OpenTracingAppLayer) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
|
||||||
|
origCtx := a.ctx
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendSubscriptionHistoryEvent")
|
||||||
|
|
||||||
|
a.ctx = newCtx
|
||||||
|
a.app.Srv().Store().SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
a.app.Srv().Store().SetContext(origCtx)
|
||||||
|
a.ctx = origCtx
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
resultVar0, resultVar1 := a.app.SendSubscriptionHistoryEvent(userID)
|
||||||
|
|
||||||
|
if resultVar1 != nil {
|
||||||
|
span.LogFields(spanlog.Error(resultVar1))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultVar0, resultVar1
|
||||||
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string {
|
func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendTestPushNotification")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendTestPushNotification")
|
||||||
|
|||||||
@@ -318,6 +318,11 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, cwsErr := a.SendSubscriptionHistoryEvent(ruser.Id)
|
||||||
|
if cwsErr != nil {
|
||||||
|
c.Logger().Error("Failed to create/update the SubscriptionHistoryEvent", mlog.Err(cwsErr))
|
||||||
|
}
|
||||||
|
|
||||||
return ruser, nil
|
return ruser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1852,3 +1852,85 @@ func TestIsFirstAdmin(t *testing.T) {
|
|||||||
require.True(t, isFirstAdmin)
|
require.True(t, isFirstAdmin)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSendSubscriptionHistoryEvent(t *testing.T) {
|
||||||
|
cloudProduct := &model.Product{
|
||||||
|
ID: "prod_test1",
|
||||||
|
Name: "name1",
|
||||||
|
Description: "description1",
|
||||||
|
PricePerSeat: 1000,
|
||||||
|
SKU: "sku1",
|
||||||
|
PriceID: "price_id1",
|
||||||
|
Family: "family1",
|
||||||
|
RecurringInterval: "year",
|
||||||
|
BillingScheme: "billing_scheme1",
|
||||||
|
CrossSellsTo: "prod_test2",
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription := &model.Subscription{
|
||||||
|
ID: "MySubscriptionID",
|
||||||
|
CustomerID: "MyCustomer",
|
||||||
|
ProductID: "SomeProductId",
|
||||||
|
AddOns: []string{},
|
||||||
|
StartAt: 1000000000,
|
||||||
|
EndAt: 2000000000,
|
||||||
|
CreateAt: 1000000000,
|
||||||
|
Seats: 10,
|
||||||
|
DNS: "some.dns.server",
|
||||||
|
IsPaidTier: "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionHistory := &model.SubscriptionHistory{
|
||||||
|
ID: "sub_history",
|
||||||
|
SubscriptionID: "MySubscriptionID",
|
||||||
|
Seats: 10,
|
||||||
|
CreateAt: 1000000000,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Should not create SubscriptionHistoryEvent if the license is not cloud", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
th.App.Srv().SetLicense(model.NewTestLicense(""))
|
||||||
|
|
||||||
|
userID := "123"
|
||||||
|
|
||||||
|
subscriptionHistoryEvent, err := th.App.SendSubscriptionHistoryEvent(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, subscriptionHistoryEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should create SubscriptionHistoryEvent if the license is cloud and the product is yearly", func(t *testing.T) {
|
||||||
|
th := SetupWithStoreMock(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||||
|
|
||||||
|
cloud := mocks.CloudInterface{}
|
||||||
|
|
||||||
|
// mock the cloud functions
|
||||||
|
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
|
||||||
|
cloud.Mock.On("GetCloudProduct", mock.Anything, mock.Anything).Return(cloudProduct, nil)
|
||||||
|
cloud.Mock.On("CreateOrUpdateSubscriptionHistoryEvent", mock.Anything, mock.Anything).Return(subscriptionHistory, nil)
|
||||||
|
|
||||||
|
cloudImpl := th.App.Srv().Cloud
|
||||||
|
defer func() {
|
||||||
|
th.App.Srv().Cloud = cloudImpl
|
||||||
|
}()
|
||||||
|
th.App.Srv().Cloud = &cloud
|
||||||
|
|
||||||
|
// Mock to get the user count
|
||||||
|
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||||
|
mockUserStore := storemocks.UserStore{}
|
||||||
|
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
|
||||||
|
|
||||||
|
mockStore.On("User").Return(&mockUserStore)
|
||||||
|
|
||||||
|
userID := "123"
|
||||||
|
|
||||||
|
subscriptionHistoryEvent, err := th.App.SendSubscriptionHistoryEvent(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, subscription.ID, subscriptionHistoryEvent.SubscriptionID, "subscription ID doesn't match")
|
||||||
|
require.Equal(t, 10, subscriptionHistoryEvent.Seats, "Number of seats doesn't match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CloudInterface interface {
|
type CloudInterface interface {
|
||||||
|
GetCloudProduct(userID string, productID string) (*model.Product, error)
|
||||||
GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error)
|
GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error)
|
||||||
GetCloudLimits(userID string) (*model.ProductLimits, error)
|
GetCloudLimits(userID string) (*model.ProductLimits, error)
|
||||||
|
|
||||||
@@ -30,5 +31,7 @@ type CloudInterface interface {
|
|||||||
// GetLicenseRenewalStatus checks on the portal whether it is possible to use token to renew a license
|
// GetLicenseRenewalStatus checks on the portal whether it is possible to use token to renew a license
|
||||||
GetLicenseRenewalStatus(userID, token string) error
|
GetLicenseRenewalStatus(userID, token string) error
|
||||||
InvalidateCaches() error
|
InvalidateCaches() error
|
||||||
|
|
||||||
|
CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error)
|
||||||
HandleLicenseChange() error
|
HandleLicenseChange() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,29 @@ func (_m *CloudInterface) CreateCustomerPayment(userID string) (*model.StripeSet
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdateSubscriptionHistoryEvent provides a mock function with given fields: userID, userCount
|
||||||
|
func (_m *CloudInterface) CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) {
|
||||||
|
ret := _m.Called(userID, userCount)
|
||||||
|
|
||||||
|
var r0 *model.SubscriptionHistory
|
||||||
|
if rf, ok := ret.Get(0).(func(string, int) *model.SubscriptionHistory); ok {
|
||||||
|
r0 = rf(userID, userCount)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.SubscriptionHistory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string, int) error); ok {
|
||||||
|
r1 = rf(userID, userCount)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetCloudCustomer provides a mock function with given fields: userID
|
// GetCloudCustomer provides a mock function with given fields: userID
|
||||||
func (_m *CloudInterface) GetCloudCustomer(userID string) (*model.CloudCustomer, error) {
|
func (_m *CloudInterface) GetCloudCustomer(userID string) (*model.CloudCustomer, error) {
|
||||||
ret := _m.Called(userID)
|
ret := _m.Called(userID)
|
||||||
@@ -120,6 +143,29 @@ func (_m *CloudInterface) GetCloudLimits(userID string) (*model.ProductLimits, e
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCloudProduct provides a mock function with given fields: userID, productID
|
||||||
|
func (_m *CloudInterface) GetCloudProduct(userID string, productID string) (*model.Product, error) {
|
||||||
|
ret := _m.Called(userID, productID)
|
||||||
|
|
||||||
|
var r0 *model.Product
|
||||||
|
if rf, ok := ret.Get(0).(func(string, string) *model.Product); ok {
|
||||||
|
r0 = rf(userID, productID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.Product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||||
|
r1 = rf(userID, productID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetCloudProducts provides a mock function with given fields: userID, includeLegacyProducts
|
// GetCloudProducts provides a mock function with given fields: userID, includeLegacyProducts
|
||||||
func (_m *CloudInterface) GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error) {
|
func (_m *CloudInterface) GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error) {
|
||||||
ret := _m.Called(userID, includeLegacyProducts)
|
ret := _m.Called(userID, includeLegacyProducts)
|
||||||
|
|||||||
@@ -140,21 +140,36 @@ type PaymentMethod struct {
|
|||||||
|
|
||||||
// Subscription model represents a subscription on the system.
|
// Subscription model represents a subscription on the system.
|
||||||
type Subscription struct {
|
type Subscription struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
CustomerID string `json:"customer_id"`
|
CustomerID string `json:"customer_id"`
|
||||||
ProductID string `json:"product_id"`
|
ProductID string `json:"product_id"`
|
||||||
AddOns []string `json:"add_ons"`
|
AddOns []string `json:"add_ons"`
|
||||||
StartAt int64 `json:"start_at"`
|
StartAt int64 `json:"start_at"`
|
||||||
EndAt int64 `json:"end_at"`
|
EndAt int64 `json:"end_at"`
|
||||||
CreateAt int64 `json:"create_at"`
|
CreateAt int64 `json:"create_at"`
|
||||||
Seats int `json:"seats"`
|
Seats int `json:"seats"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
DNS string `json:"dns"`
|
DNS string `json:"dns"`
|
||||||
IsPaidTier string `json:"is_paid_tier"`
|
IsPaidTier string `json:"is_paid_tier"`
|
||||||
LastInvoice *Invoice `json:"last_invoice"`
|
LastInvoice *Invoice `json:"last_invoice"`
|
||||||
IsFreeTrial string `json:"is_free_trial"`
|
IsFreeTrial string `json:"is_free_trial"`
|
||||||
TrialEndAt int64 `json:"trial_end_at"`
|
TrialEndAt int64 `json:"trial_end_at"`
|
||||||
DelinquentSince *int64 `json:"delinquent_since"`
|
DelinquentSince *int64 `json:"delinquent_since"`
|
||||||
|
OriginallyLicensedSeats int `json:"originally_licensed_seats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscription History model represents true up event in a yearly subscription
|
||||||
|
type SubscriptionHistory struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SubscriptionID string `json:"subscription_id"`
|
||||||
|
Seats int `json:"seats"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionHistoryChange struct {
|
||||||
|
SubscriptionID string `json:"subscription_id"`
|
||||||
|
Seats int `json:"seats"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWorkSpaceNameFromDNS returns the work space name. For example from test.mattermost.cloud.com, it returns test
|
// GetWorkSpaceNameFromDNS returns the work space name. For example from test.mattermost.cloud.com, it returns test
|
||||||
@@ -258,3 +273,11 @@ type ProductLimits struct {
|
|||||||
Messages *MessagesLimits `json:"messages,omitempty"`
|
Messages *MessagesLimits `json:"messages,omitempty"`
|
||||||
Teams *TeamsLimits `json:"teams,omitempty"`
|
Teams *TeamsLimits `json:"teams,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Product) IsYearly() bool {
|
||||||
|
return p.RecurringInterval == RecurringIntervalYearly
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Product) IsMonthly() bool {
|
||||||
|
return p.RecurringInterval == RecurringIntervalMonthly
|
||||||
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user