[MM-47565]: In product true up for yearly products (#21704)

Этот коммит содержится в:
emmyni
2022-11-29 11:47:51 -05:00
коммит произвёл GitHub
родитель 37466fe438
Коммит 2455de99ff
8 изменённых файлов: 215 добавлений и 15 удалений

Просмотреть файл

@@ -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.

Просмотреть файл

@@ -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
}