[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)
|
||||
// ConvertUserToBot converts a user to bot.
|
||||
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(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError)
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 {
|
||||
origCtx := a.ctx
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1852,3 +1852,85 @@ func TestIsFirstAdmin(t *testing.T) {
|
||||
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 {
|
||||
GetCloudProduct(userID string, productID string) (*model.Product, error)
|
||||
GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, 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(userID, token string) error
|
||||
InvalidateCaches() error
|
||||
|
||||
CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error)
|
||||
HandleLicenseChange() error
|
||||
}
|
||||
|
||||
@@ -74,6 +74,29 @@ func (_m *CloudInterface) CreateCustomerPayment(userID string) (*model.StripeSet
|
||||
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
|
||||
func (_m *CloudInterface) GetCloudCustomer(userID string) (*model.CloudCustomer, error) {
|
||||
ret := _m.Called(userID)
|
||||
@@ -120,6 +143,29 @@ func (_m *CloudInterface) GetCloudLimits(userID string) (*model.ProductLimits, e
|
||||
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
|
||||
func (_m *CloudInterface) GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error) {
|
||||
ret := _m.Called(userID, includeLegacyProducts)
|
||||
|
||||
@@ -140,21 +140,36 @@ type PaymentMethod struct {
|
||||
|
||||
// Subscription model represents a subscription on the system.
|
||||
type Subscription struct {
|
||||
ID string `json:"id"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
AddOns []string `json:"add_ons"`
|
||||
StartAt int64 `json:"start_at"`
|
||||
EndAt int64 `json:"end_at"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
Seats int `json:"seats"`
|
||||
Status string `json:"status"`
|
||||
DNS string `json:"dns"`
|
||||
IsPaidTier string `json:"is_paid_tier"`
|
||||
LastInvoice *Invoice `json:"last_invoice"`
|
||||
IsFreeTrial string `json:"is_free_trial"`
|
||||
TrialEndAt int64 `json:"trial_end_at"`
|
||||
DelinquentSince *int64 `json:"delinquent_since"`
|
||||
ID string `json:"id"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
AddOns []string `json:"add_ons"`
|
||||
StartAt int64 `json:"start_at"`
|
||||
EndAt int64 `json:"end_at"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
Seats int `json:"seats"`
|
||||
Status string `json:"status"`
|
||||
DNS string `json:"dns"`
|
||||
IsPaidTier string `json:"is_paid_tier"`
|
||||
LastInvoice *Invoice `json:"last_invoice"`
|
||||
IsFreeTrial string `json:"is_free_trial"`
|
||||
TrialEndAt int64 `json:"trial_end_at"`
|
||||
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
|
||||
@@ -258,3 +273,11 @@ type ProductLimits struct {
|
||||
Messages *MessagesLimits `json:"messages,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