diff --git a/billing/plan/plan.go b/billing/plan/plan.go index e04e4eff6..7da29fcbd 100644 --- a/billing/plan/plan.go +++ b/billing/plan/plan.go @@ -57,6 +57,17 @@ func (p Plan) GetUserSeatProduct() (product.Product, bool) { return product.Product{}, false } +func (p Plan) IsFree() bool { + for _, prod := range p.Products { + for _, price := range prod.Prices { + if price.Amount > 0 { + return false + } + } + } + return true +} + type Filter struct { IDs []string Interval string diff --git a/core/event/mocks/checkout_service.go b/core/event/mocks/checkout_service.go new file mode 100644 index 000000000..64ede79f0 --- /dev/null +++ b/core/event/mocks/checkout_service.go @@ -0,0 +1,157 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + checkout "github.com/raystack/frontier/billing/checkout" + + mock "github.com/stretchr/testify/mock" + + product "github.com/raystack/frontier/billing/product" + + subscription "github.com/raystack/frontier/billing/subscription" +) + +// CheckoutService is an autogenerated mock type for the CheckoutService type +type CheckoutService struct { + mock.Mock +} + +type CheckoutService_Expecter struct { + mock *mock.Mock +} + +func (_m *CheckoutService) EXPECT() *CheckoutService_Expecter { + return &CheckoutService_Expecter{mock: &_m.Mock} +} + +// Apply provides a mock function with given fields: ctx, ch +func (_m *CheckoutService) Apply(ctx context.Context, ch checkout.Checkout) (*subscription.Subscription, *product.Product, error) { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for Apply") + } + + var r0 *subscription.Subscription + var r1 *product.Product + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) (*subscription.Subscription, *product.Product, error)); ok { + return rf(ctx, ch) + } + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) *subscription.Subscription); ok { + r0 = rf(ctx, ch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*subscription.Subscription) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, checkout.Checkout) *product.Product); ok { + r1 = rf(ctx, ch) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*product.Product) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, checkout.Checkout) error); ok { + r2 = rf(ctx, ch) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// CheckoutService_Apply_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Apply' +type CheckoutService_Apply_Call struct { + *mock.Call +} + +// Apply is a helper method to define mock.On call +// - ctx context.Context +// - ch checkout.Checkout +func (_e *CheckoutService_Expecter) Apply(ctx interface{}, ch interface{}) *CheckoutService_Apply_Call { + return &CheckoutService_Apply_Call{Call: _e.mock.On("Apply", ctx, ch)} +} + +func (_c *CheckoutService_Apply_Call) Run(run func(ctx context.Context, ch checkout.Checkout)) *CheckoutService_Apply_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(checkout.Checkout)) + }) + return _c +} + +func (_c *CheckoutService_Apply_Call) Return(_a0 *subscription.Subscription, _a1 *product.Product, _a2 error) *CheckoutService_Apply_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *CheckoutService_Apply_Call) RunAndReturn(run func(context.Context, checkout.Checkout) (*subscription.Subscription, *product.Product, error)) *CheckoutService_Apply_Call { + _c.Call.Return(run) + return _c +} + +// TriggerSyncByProviderID provides a mock function with given fields: ctx, id +func (_m *CheckoutService) TriggerSyncByProviderID(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for TriggerSyncByProviderID") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CheckoutService_TriggerSyncByProviderID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TriggerSyncByProviderID' +type CheckoutService_TriggerSyncByProviderID_Call struct { + *mock.Call +} + +// TriggerSyncByProviderID is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *CheckoutService_Expecter) TriggerSyncByProviderID(ctx interface{}, id interface{}) *CheckoutService_TriggerSyncByProviderID_Call { + return &CheckoutService_TriggerSyncByProviderID_Call{Call: _e.mock.On("TriggerSyncByProviderID", ctx, id)} +} + +func (_c *CheckoutService_TriggerSyncByProviderID_Call) Run(run func(ctx context.Context, id string)) *CheckoutService_TriggerSyncByProviderID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *CheckoutService_TriggerSyncByProviderID_Call) Return(_a0 error) *CheckoutService_TriggerSyncByProviderID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CheckoutService_TriggerSyncByProviderID_Call) RunAndReturn(run func(context.Context, string) error) *CheckoutService_TriggerSyncByProviderID_Call { + _c.Call.Return(run) + return _c +} + +// NewCheckoutService creates a new instance of CheckoutService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCheckoutService(t interface { + mock.TestingT + Cleanup(func()) +}) *CheckoutService { + mock := &CheckoutService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/event/mocks/credit_service.go b/core/event/mocks/credit_service.go new file mode 100644 index 000000000..c39f6b3e8 --- /dev/null +++ b/core/event/mocks/credit_service.go @@ -0,0 +1,85 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + credit "github.com/raystack/frontier/billing/credit" + + mock "github.com/stretchr/testify/mock" +) + +// CreditService is an autogenerated mock type for the CreditService type +type CreditService struct { + mock.Mock +} + +type CreditService_Expecter struct { + mock *mock.Mock +} + +func (_m *CreditService) EXPECT() *CreditService_Expecter { + return &CreditService_Expecter{mock: &_m.Mock} +} + +// Add provides a mock function with given fields: ctx, ch +func (_m *CreditService) Add(ctx context.Context, ch credit.Credit) error { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, credit.Credit) error); ok { + r0 = rf(ctx, ch) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CreditService_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' +type CreditService_Add_Call struct { + *mock.Call +} + +// Add is a helper method to define mock.On call +// - ctx context.Context +// - ch credit.Credit +func (_e *CreditService_Expecter) Add(ctx interface{}, ch interface{}) *CreditService_Add_Call { + return &CreditService_Add_Call{Call: _e.mock.On("Add", ctx, ch)} +} + +func (_c *CreditService_Add_Call) Run(run func(ctx context.Context, ch credit.Credit)) *CreditService_Add_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(credit.Credit)) + }) + return _c +} + +func (_c *CreditService_Add_Call) Return(_a0 error) *CreditService_Add_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CreditService_Add_Call) RunAndReturn(run func(context.Context, credit.Credit) error) *CreditService_Add_Call { + _c.Call.Return(run) + return _c +} + +// NewCreditService creates a new instance of CreditService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCreditService(t interface { + mock.TestingT + Cleanup(func()) +}) *CreditService { + mock := &CreditService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/event/mocks/customer_service.go b/core/event/mocks/customer_service.go new file mode 100644 index 000000000..46e80f812 --- /dev/null +++ b/core/event/mocks/customer_service.go @@ -0,0 +1,202 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + customer "github.com/raystack/frontier/billing/customer" + + mock "github.com/stretchr/testify/mock" +) + +// CustomerService is an autogenerated mock type for the CustomerService type +type CustomerService struct { + mock.Mock +} + +type CustomerService_Expecter struct { + mock *mock.Mock +} + +func (_m *CustomerService) EXPECT() *CustomerService_Expecter { + return &CustomerService_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, _a1, offline +func (_m *CustomerService) Create(ctx context.Context, _a1 customer.Customer, offline bool) (customer.Customer, error) { + ret := _m.Called(ctx, _a1, offline) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 customer.Customer + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, customer.Customer, bool) (customer.Customer, error)); ok { + return rf(ctx, _a1, offline) + } + if rf, ok := ret.Get(0).(func(context.Context, customer.Customer, bool) customer.Customer); ok { + r0 = rf(ctx, _a1, offline) + } else { + r0 = ret.Get(0).(customer.Customer) + } + + if rf, ok := ret.Get(1).(func(context.Context, customer.Customer, bool) error); ok { + r1 = rf(ctx, _a1, offline) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CustomerService_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type CustomerService_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - _a1 customer.Customer +// - offline bool +func (_e *CustomerService_Expecter) Create(ctx interface{}, _a1 interface{}, offline interface{}) *CustomerService_Create_Call { + return &CustomerService_Create_Call{Call: _e.mock.On("Create", ctx, _a1, offline)} +} + +func (_c *CustomerService_Create_Call) Run(run func(ctx context.Context, _a1 customer.Customer, offline bool)) *CustomerService_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(customer.Customer), args[2].(bool)) + }) + return _c +} + +func (_c *CustomerService_Create_Call) Return(_a0 customer.Customer, _a1 error) *CustomerService_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CustomerService_Create_Call) RunAndReturn(run func(context.Context, customer.Customer, bool) (customer.Customer, error)) *CustomerService_Create_Call { + _c.Call.Return(run) + return _c +} + +// List provides a mock function with given fields: ctx, flt +func (_m *CustomerService) List(ctx context.Context, flt customer.Filter) ([]customer.Customer, error) { + ret := _m.Called(ctx, flt) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 []customer.Customer + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, customer.Filter) ([]customer.Customer, error)); ok { + return rf(ctx, flt) + } + if rf, ok := ret.Get(0).(func(context.Context, customer.Filter) []customer.Customer); ok { + r0 = rf(ctx, flt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]customer.Customer) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, customer.Filter) error); ok { + r1 = rf(ctx, flt) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CustomerService_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' +type CustomerService_List_Call struct { + *mock.Call +} + +// List is a helper method to define mock.On call +// - ctx context.Context +// - flt customer.Filter +func (_e *CustomerService_Expecter) List(ctx interface{}, flt interface{}) *CustomerService_List_Call { + return &CustomerService_List_Call{Call: _e.mock.On("List", ctx, flt)} +} + +func (_c *CustomerService_List_Call) Run(run func(ctx context.Context, flt customer.Filter)) *CustomerService_List_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(customer.Filter)) + }) + return _c +} + +func (_c *CustomerService_List_Call) Return(_a0 []customer.Customer, _a1 error) *CustomerService_List_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CustomerService_List_Call) RunAndReturn(run func(context.Context, customer.Filter) ([]customer.Customer, error)) *CustomerService_List_Call { + _c.Call.Return(run) + return _c +} + +// TriggerSyncByProviderID provides a mock function with given fields: ctx, id +func (_m *CustomerService) TriggerSyncByProviderID(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for TriggerSyncByProviderID") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CustomerService_TriggerSyncByProviderID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TriggerSyncByProviderID' +type CustomerService_TriggerSyncByProviderID_Call struct { + *mock.Call +} + +// TriggerSyncByProviderID is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *CustomerService_Expecter) TriggerSyncByProviderID(ctx interface{}, id interface{}) *CustomerService_TriggerSyncByProviderID_Call { + return &CustomerService_TriggerSyncByProviderID_Call{Call: _e.mock.On("TriggerSyncByProviderID", ctx, id)} +} + +func (_c *CustomerService_TriggerSyncByProviderID_Call) Run(run func(ctx context.Context, id string)) *CustomerService_TriggerSyncByProviderID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *CustomerService_TriggerSyncByProviderID_Call) Return(_a0 error) *CustomerService_TriggerSyncByProviderID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CustomerService_TriggerSyncByProviderID_Call) RunAndReturn(run func(context.Context, string) error) *CustomerService_TriggerSyncByProviderID_Call { + _c.Call.Return(run) + return _c +} + +// NewCustomerService creates a new instance of CustomerService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCustomerService(t interface { + mock.TestingT + Cleanup(func()) +}) *CustomerService { + mock := &CustomerService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/event/mocks/organization_service.go b/core/event/mocks/organization_service.go new file mode 100644 index 000000000..4aec03bd0 --- /dev/null +++ b/core/event/mocks/organization_service.go @@ -0,0 +1,95 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + organization "github.com/raystack/frontier/core/organization" +) + +// OrganizationService is an autogenerated mock type for the OrganizationService type +type OrganizationService struct { + mock.Mock +} + +type OrganizationService_Expecter struct { + mock *mock.Mock +} + +func (_m *OrganizationService) EXPECT() *OrganizationService_Expecter { + return &OrganizationService_Expecter{mock: &_m.Mock} +} + +// GetRaw provides a mock function with given fields: ctx, id +func (_m *OrganizationService) GetRaw(ctx context.Context, id string) (organization.Organization, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetRaw") + } + + var r0 organization.Organization + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (organization.Organization, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) organization.Organization); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(organization.Organization) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OrganizationService_GetRaw_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRaw' +type OrganizationService_GetRaw_Call struct { + *mock.Call +} + +// GetRaw is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *OrganizationService_Expecter) GetRaw(ctx interface{}, id interface{}) *OrganizationService_GetRaw_Call { + return &OrganizationService_GetRaw_Call{Call: _e.mock.On("GetRaw", ctx, id)} +} + +func (_c *OrganizationService_GetRaw_Call) Run(run func(ctx context.Context, id string)) *OrganizationService_GetRaw_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *OrganizationService_GetRaw_Call) Return(_a0 organization.Organization, _a1 error) *OrganizationService_GetRaw_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OrganizationService_GetRaw_Call) RunAndReturn(run func(context.Context, string) (organization.Organization, error)) *OrganizationService_GetRaw_Call { + _c.Call.Return(run) + return _c +} + +// NewOrganizationService creates a new instance of OrganizationService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewOrganizationService(t interface { + mock.TestingT + Cleanup(func()) +}) *OrganizationService { + mock := &OrganizationService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/event/mocks/plan_service.go b/core/event/mocks/plan_service.go new file mode 100644 index 000000000..1de04d4cc --- /dev/null +++ b/core/event/mocks/plan_service.go @@ -0,0 +1,95 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + plan "github.com/raystack/frontier/billing/plan" +) + +// PlanService is an autogenerated mock type for the PlanService type +type PlanService struct { + mock.Mock +} + +type PlanService_Expecter struct { + mock *mock.Mock +} + +func (_m *PlanService) EXPECT() *PlanService_Expecter { + return &PlanService_Expecter{mock: &_m.Mock} +} + +// GetByID provides a mock function with given fields: ctx, id +func (_m *PlanService) GetByID(ctx context.Context, id string) (plan.Plan, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetByID") + } + + var r0 plan.Plan + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (plan.Plan, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) plan.Plan); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(plan.Plan) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PlanService_GetByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByID' +type PlanService_GetByID_Call struct { + *mock.Call +} + +// GetByID is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *PlanService_Expecter) GetByID(ctx interface{}, id interface{}) *PlanService_GetByID_Call { + return &PlanService_GetByID_Call{Call: _e.mock.On("GetByID", ctx, id)} +} + +func (_c *PlanService_GetByID_Call) Run(run func(ctx context.Context, id string)) *PlanService_GetByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *PlanService_GetByID_Call) Return(_a0 plan.Plan, _a1 error) *PlanService_GetByID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *PlanService_GetByID_Call) RunAndReturn(run func(context.Context, string) (plan.Plan, error)) *PlanService_GetByID_Call { + _c.Call.Return(run) + return _c +} + +// NewPlanService creates a new instance of PlanService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPlanService(t interface { + mock.TestingT + Cleanup(func()) +}) *PlanService { + mock := &PlanService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/event/mocks/subscription_service.go b/core/event/mocks/subscription_service.go new file mode 100644 index 000000000..83f28e082 --- /dev/null +++ b/core/event/mocks/subscription_service.go @@ -0,0 +1,83 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// SubscriptionService is an autogenerated mock type for the SubscriptionService type +type SubscriptionService struct { + mock.Mock +} + +type SubscriptionService_Expecter struct { + mock *mock.Mock +} + +func (_m *SubscriptionService) EXPECT() *SubscriptionService_Expecter { + return &SubscriptionService_Expecter{mock: &_m.Mock} +} + +// TriggerSyncByProviderID provides a mock function with given fields: ctx, id +func (_m *SubscriptionService) TriggerSyncByProviderID(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for TriggerSyncByProviderID") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubscriptionService_TriggerSyncByProviderID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TriggerSyncByProviderID' +type SubscriptionService_TriggerSyncByProviderID_Call struct { + *mock.Call +} + +// TriggerSyncByProviderID is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *SubscriptionService_Expecter) TriggerSyncByProviderID(ctx interface{}, id interface{}) *SubscriptionService_TriggerSyncByProviderID_Call { + return &SubscriptionService_TriggerSyncByProviderID_Call{Call: _e.mock.On("TriggerSyncByProviderID", ctx, id)} +} + +func (_c *SubscriptionService_TriggerSyncByProviderID_Call) Run(run func(ctx context.Context, id string)) *SubscriptionService_TriggerSyncByProviderID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *SubscriptionService_TriggerSyncByProviderID_Call) Return(_a0 error) *SubscriptionService_TriggerSyncByProviderID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *SubscriptionService_TriggerSyncByProviderID_Call) RunAndReturn(run func(context.Context, string) error) *SubscriptionService_TriggerSyncByProviderID_Call { + _c.Call.Return(run) + return _c +} + +// NewSubscriptionService creates a new instance of SubscriptionService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSubscriptionService(t interface { + mock.TestingT + Cleanup(func()) +}) *SubscriptionService { + mock := &SubscriptionService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/event/mocks/user_service.go b/core/event/mocks/user_service.go new file mode 100644 index 000000000..a99cdb64b --- /dev/null +++ b/core/event/mocks/user_service.go @@ -0,0 +1,98 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + user "github.com/raystack/frontier/core/user" +) + +// UserService is an autogenerated mock type for the UserService type +type UserService struct { + mock.Mock +} + +type UserService_Expecter struct { + mock *mock.Mock +} + +func (_m *UserService) EXPECT() *UserService_Expecter { + return &UserService_Expecter{mock: &_m.Mock} +} + +// ListByOrg provides a mock function with given fields: ctx, orgID, roleFilter +func (_m *UserService) ListByOrg(ctx context.Context, orgID string, roleFilter string) ([]user.User, error) { + ret := _m.Called(ctx, orgID, roleFilter) + + if len(ret) == 0 { + panic("no return value specified for ListByOrg") + } + + var r0 []user.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]user.User, error)); ok { + return rf(ctx, orgID, roleFilter) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) []user.User); ok { + r0 = rf(ctx, orgID, roleFilter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]user.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, orgID, roleFilter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UserService_ListByOrg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListByOrg' +type UserService_ListByOrg_Call struct { + *mock.Call +} + +// ListByOrg is a helper method to define mock.On call +// - ctx context.Context +// - orgID string +// - roleFilter string +func (_e *UserService_Expecter) ListByOrg(ctx interface{}, orgID interface{}, roleFilter interface{}) *UserService_ListByOrg_Call { + return &UserService_ListByOrg_Call{Call: _e.mock.On("ListByOrg", ctx, orgID, roleFilter)} +} + +func (_c *UserService_ListByOrg_Call) Run(run func(ctx context.Context, orgID string, roleFilter string)) *UserService_ListByOrg_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *UserService_ListByOrg_Call) Return(_a0 []user.User, _a1 error) *UserService_ListByOrg_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UserService_ListByOrg_Call) RunAndReturn(run func(context.Context, string, string) ([]user.User, error)) *UserService_ListByOrg_Call { + _c.Call.Return(run) + return _c +} + +// NewUserService creates a new instance of UserService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUserService(t interface { + mock.TestingT + Cleanup(func()) +}) *UserService { + mock := &UserService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/event/service.go b/core/event/service.go index 61da39f10..589f61d3a 100644 --- a/core/event/service.go +++ b/core/event/service.go @@ -27,6 +27,8 @@ import ( "github.com/raystack/frontier/core/organization" ) +var DefaultPlanNotFree = errors.New("default plan is not free") + type CheckoutService interface { Apply(ctx context.Context, ch checkout.Checkout) (*subscription.Subscription, *product.Product, error) TriggerSyncByProviderID(ctx context.Context, id string) error @@ -135,12 +137,8 @@ func (p *Service) EnsureDefaultPlan(ctx context.Context, orgID string) error { return fmt.Errorf("failed to get default plan: %w", err) } - for _, prod := range defaultPlan.Products { - for _, price := range prod.Prices { - if price.Amount > 0 { - return fmt.Errorf("default plan is not free") - } - } + if !defaultPlan.IsFree() { + return DefaultPlanNotFree } _, _, err = p.checkoutService.Apply(ctx, checkout.Checkout{ CustomerID: customr.ID, diff --git a/core/event/service_test.go b/core/event/service_test.go new file mode 100644 index 000000000..0a41f34ac --- /dev/null +++ b/core/event/service_test.go @@ -0,0 +1,441 @@ +package event + +import ( + "context" + "errors" + "testing" + + "github.com/raystack/frontier/billing" + "github.com/raystack/frontier/billing/checkout" + "github.com/raystack/frontier/billing/credit" + "github.com/raystack/frontier/billing/customer" + "github.com/raystack/frontier/billing/plan" + "github.com/raystack/frontier/billing/product" + "github.com/raystack/frontier/core/event/mocks" + "github.com/raystack/frontier/core/organization" + "github.com/raystack/frontier/core/user" + "github.com/raystack/frontier/pkg/metadata" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var sampleError = errors.New("sample error") + +func mockService(t *testing.T) (*billing.Config, *mocks.CheckoutService, *mocks.CustomerService, *mocks.OrganizationService, *mocks.PlanService, *mocks.UserService, *mocks.SubscriptionService, *mocks.CreditService) { + t.Helper() + billingConf := &billing.Config{ + StripeKey: "test_key", + StripeAutoTax: false, + StripeWebhookSecrets: nil, + PlansPath: "", + DefaultCurrency: "USD", + AccountConfig: billing.AccountConfig{AutoCreateWithOrg: true, DefaultPlan: "default_plan", DefaultOffline: false}, + PlanChangeConfig: billing.PlanChangeConfig{}, + SubscriptionConfig: billing.SubscriptionConfig{}, + ProductConfig: billing.ProductConfig{}, + RefreshInterval: billing.RefreshInterval{}, + } + checkoutService := mocks.NewCheckoutService(t) + customerService := mocks.NewCustomerService(t) + orgService := mocks.NewOrganizationService(t) + planService := mocks.NewPlanService(t) + userService := mocks.NewUserService(t) + subsService := mocks.NewSubscriptionService(t) + creditService := mocks.NewCreditService(t) + + return billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService +} + +func TestEnsureDefaultPlan(t *testing.T) { + ctx := context.Background() + + type args struct { + ctx context.Context + orgID string + } + + tests := []struct { + name string + args args + wantErr error + setup func() *Service + }{ + { + name: "return error if customerService.List returns error", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: sampleError, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return(nil, sampleError).Once() + return service + }, + }, + { + name: "short circuit if customer account exists", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: nil, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return([]customer.Customer{{ID: "1"}}, nil).Once() + return service + }, + }, + { + name: "return error if orgService.GetRaw returns error", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: sampleError, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return([]customer.Customer{}, nil).Once() + orgService.On("GetRaw", ctx, "").Return(organization.Organization{}, sampleError).Once() + return service + }, + }, + { + name: "return error if userService.ListByOrg returns error", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: sampleError, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return(nil, nil).Once() + orgService.On("GetRaw", ctx, "").Return(organization.Organization{ID: "org_1"}, nil).Once() + userService.On("ListByOrg", ctx, "org_1", organization.AdminRole).Return(nil, sampleError).Once() + return service + }, + }, + { + name: "return error if customerService.Create returns error", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: sampleError, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return(nil, nil).Once() + org := organization.Organization{ID: "org_1", Title: "org_title"} + orgService.On("GetRaw", ctx, "").Return(org, nil).Once() + userService.On("ListByOrg", ctx, "org_1", organization.AdminRole).Return([]user.User{{Email: "email@example.com"}}, nil).Once() + customerService.On("Create", ctx, customer.Customer{ + OrgID: "org_1", + Name: getCustomerName(org), + Email: "email@example.com", + Currency: "USD", + Metadata: map[string]any{ + "auto_created": "true", + }}, false).Return(customer.Customer{}, sampleError).Once() + return service + }, + }, + { + name: "return error if planService.GetByID returns error", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: sampleError, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return(nil, nil).Once() + org := organization.Organization{ID: "org_1", Title: "org_title"} + orgService.On("GetRaw", ctx, "").Return(org, nil).Once() + userService.On("ListByOrg", ctx, "org_1", organization.AdminRole).Return([]user.User{{Email: "email@example.com"}}, nil).Once() + customerService.On("Create", ctx, customer.Customer{ + OrgID: "org_1", + Name: getCustomerName(org), + Email: "email@example.com", + Currency: "USD", + Metadata: map[string]any{ + "auto_created": "true", + }}, false).Return(customer.Customer{ID: "cid_1"}, nil).Once() + planService.On("GetByID", ctx, "default_plan").Return(plan.Plan{}, sampleError).Once() + return service + }, + }, + { + name: "return error if defaultPlan is not free", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: DefaultPlanNotFree, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return(nil, nil).Once() + org := organization.Organization{ID: "org_1", Title: "org_title"} + orgService.On("GetRaw", ctx, "").Return(org, nil).Once() + userService.On("ListByOrg", ctx, "org_1", organization.AdminRole).Return([]user.User{{Email: "email@example.com"}}, nil).Once() + customerService.On("Create", ctx, customer.Customer{ + OrgID: "org_1", + Name: getCustomerName(org), + Email: "email@example.com", + Currency: "USD", + Metadata: map[string]any{ + "auto_created": "true", + }}, false).Return(customer.Customer{ID: "cid_1"}, nil).Once() + planService.On("GetByID", ctx, "default_plan"). + Return(plan.Plan{ + Products: []product.Product{ + { + Prices: []product.Price{ + { + Amount: 100.0, + }, + }, + }, + }, + }, nil).Once() + return service + }, + }, + { + name: "return error if checkoutService.Apply returns error", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: sampleError, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return(nil, nil).Once() + org := organization.Organization{ID: "org_1", Title: "org_title"} + orgService.On("GetRaw", ctx, "").Return(org, nil).Once() + userService.On("ListByOrg", ctx, "org_1", organization.AdminRole).Return([]user.User{{Email: "email@example.com"}}, nil).Once() + customerService.On("Create", ctx, customer.Customer{ + OrgID: "org_1", + Name: getCustomerName(org), + Email: "email@example.com", + Currency: "USD", + Metadata: map[string]any{ + "auto_created": "true", + }}, false).Return(customer.Customer{ID: "cid_1"}, nil).Once() + planService.On("GetByID", ctx, "default_plan"). + Return(plan.Plan{ + ID: "plan_1", + Products: []product.Product{ + { + Prices: []product.Price{ + { + Amount: 0.0, + }, + }, + }, + }, + }, nil).Once() + checkoutService.On("Apply", ctx, checkout.Checkout{ + CustomerID: "cid_1", + PlanID: "plan_1", + SkipTrial: true, + }).Return(nil, nil, sampleError).Once() + return service + }, + }, + { + name: "return no error if creditService.Add returns no error", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: nil, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + const onboardingAmount = 10.0 + billingConf.AccountConfig.OnboardCreditsWithOrg = onboardingAmount + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return(nil, nil).Once() + org := organization.Organization{ID: "org_1", Title: "org_title"} + orgService.On("GetRaw", ctx, "").Return(org, nil).Once() + userService.On("ListByOrg", ctx, "org_1", organization.AdminRole).Return([]user.User{{Email: "email@example.com"}}, nil).Once() + customerService.On("Create", ctx, customer.Customer{ + OrgID: "org_1", + Name: getCustomerName(org), + Email: "email@example.com", + Currency: "USD", + Metadata: map[string]any{ + "auto_created": "true", + }}, false).Return(customer.Customer{ID: "cid_1"}, nil).Once() + planService.On("GetByID", ctx, "default_plan"). + Return(plan.Plan{ + ID: "plan_1", + Products: []product.Product{ + { + Prices: []product.Price{ + { + Amount: 0.0, + }, + }, + }, + }, + }, nil).Once() + checkoutService.On("Apply", ctx, checkout.Checkout{ + CustomerID: "cid_1", + PlanID: "plan_1", + SkipTrial: true, + }).Return(nil, nil, nil).Once() + creditService.On("Add", ctx, credit.Credit{ + ID: "43b9d78f-ccd7-5011-88a7-27791d9baeb2", + CustomerID: "cid_1", + Amount: onboardingAmount, + UserID: "", + Source: "system.awarded", + Description: "Awarded 10 credits for onboarding", + Metadata: metadata.Metadata{"auto_created": "true"}, + }).Return(nil).Once() + return service + }, + }, + { + name: "return error if creditService.Add returns error", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: sampleError, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + const onboardingAmount = 10.0 + billingConf.AccountConfig.OnboardCreditsWithOrg = onboardingAmount + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return(nil, nil).Once() + org := organization.Organization{ID: "org_1", Title: "org_title"} + orgService.On("GetRaw", ctx, "").Return(org, nil).Once() + userService.On("ListByOrg", ctx, "org_1", organization.AdminRole).Return([]user.User{{Email: "email@example.com"}}, nil).Once() + customerService.On("Create", ctx, customer.Customer{ + OrgID: "org_1", + Name: getCustomerName(org), + Email: "email@example.com", + Currency: "USD", + Metadata: map[string]any{ + "auto_created": "true", + }}, false).Return(customer.Customer{ID: "cid_1"}, nil).Once() + planService.On("GetByID", ctx, "default_plan"). + Return(plan.Plan{ + ID: "plan_1", + Products: []product.Product{ + { + Prices: []product.Price{ + { + Amount: 0.0, + }, + }, + }, + }, + }, nil).Once() + checkoutService.On("Apply", ctx, checkout.Checkout{ + CustomerID: "cid_1", + PlanID: "plan_1", + SkipTrial: true, + }).Return(nil, nil, nil).Once() + creditService.On("Add", ctx, credit.Credit{ + ID: "43b9d78f-ccd7-5011-88a7-27791d9baeb2", + CustomerID: "cid_1", + Amount: onboardingAmount, + UserID: "", + Source: "system.awarded", + Description: "Awarded 10 credits for onboarding", + Metadata: metadata.Metadata{"auto_created": "true"}, + }).Return(sampleError).Once() + return service + }, + }, + { + name: "return no error if creditService.Add returns credit.ErrAlreadyApplied error", + args: args{ + ctx: ctx, + orgID: "", + }, + wantErr: nil, + setup: func() *Service { + billingConf, checkoutService, customerService, orgService, planService, userService, subsService, creditService := mockService(t) + const onboardingAmount = 10.0 + billingConf.AccountConfig.OnboardCreditsWithOrg = onboardingAmount + service := NewService(*billingConf, orgService, checkoutService, customerService, planService, userService, subsService, creditService) + + customerService.On("List", ctx, customer.Filter{}).Return(nil, nil).Once() + org := organization.Organization{ID: "org_1", Title: "org_title"} + orgService.On("GetRaw", ctx, "").Return(org, nil).Once() + userService.On("ListByOrg", ctx, "org_1", organization.AdminRole).Return([]user.User{{Email: "email@example.com"}}, nil).Once() + customerService.On("Create", ctx, customer.Customer{ + OrgID: "org_1", + Name: getCustomerName(org), + Email: "email@example.com", + Currency: "USD", + Metadata: map[string]any{ + "auto_created": "true", + }}, false).Return(customer.Customer{ID: "cid_1"}, nil).Once() + planService.On("GetByID", ctx, "default_plan"). + Return(plan.Plan{ + ID: "plan_1", + Products: []product.Product{ + { + Prices: []product.Price{ + { + Amount: 0.0, + }, + }, + }, + }, + }, nil).Once() + checkoutService.On("Apply", ctx, checkout.Checkout{ + CustomerID: "cid_1", + PlanID: "plan_1", + SkipTrial: true, + }).Return(nil, nil, nil).Once() + creditService.On("Add", ctx, credit.Credit{ + ID: "43b9d78f-ccd7-5011-88a7-27791d9baeb2", + CustomerID: "cid_1", + Amount: onboardingAmount, + UserID: "", + Source: "system.awarded", + Description: "Awarded 10 credits for onboarding", + Metadata: metadata.Metadata{"auto_created": "true"}, + }).Return(credit.ErrAlreadyApplied).Once() + return service + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := tt.setup() + err := s.EnsureDefaultPlan(ctx, tt.args.orgID) + + if tt.wantErr != nil { + require.Error(t, err) + assert.ErrorIs(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +}