Skip to content

[usage] Validate spending limits in UpdateCostCenter #13798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .werft/jobs/build/installer/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ EOF`);
exec(`yq w -i ${this.options.installerConfigPath} experimental.webapp.usage.billInstancesAfter "2022-08-11T08:05:32.499Z"`, { slice: slice })
exec(`yq w -i ${this.options.installerConfigPath} experimental.webapp.usage.defaultSpendingLimit.forUsers 500`, { slice: slice })
exec(`yq w -i ${this.options.installerConfigPath} experimental.webapp.usage.defaultSpendingLimit.forTeams 0`, { slice: slice })
exec(`yq w -i ${this.options.installerConfigPath} experimental.webapp.usage.defaultSpendingLimit.minForUsersOnStripe 1000`, { slice: slice })
exec(`yq w -i ${this.options.installerConfigPath} experimental.webapp.usage.creditsPerMinuteByWorkspaceClass['default'] 0.1666666667`, { slice: slice })
exec(`yq w -i ${this.options.installerConfigPath} experimental.webapp.usage.creditsPerMinuteByWorkspaceClass['gitpodio-internal-xl'] 0.3333333333`, { slice: slice })
}
Expand Down
17 changes: 9 additions & 8 deletions components/usage/pkg/apiv1/usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ func newUsageService(t *testing.T, dbconn *gorm.DB) v1.UsageServiceClient {
)

costCenterManager := db.NewCostCenterManager(dbconn, db.DefaultSpendingLimit{
ForTeams: 0,
ForUsers: 500,
ForTeams: 0,
ForUsers: 500,
MinForUsersOnStripe: 1000,
})

v1.RegisterUsageServiceServer(srv.GRPC(), NewUsageService(dbconn, DefaultWorkspacePricer, costCenterManager))
Expand Down Expand Up @@ -227,13 +228,13 @@ func TestGetAndSetCostCenter(t *testing.T) {
conn := dbtest.ConnectForTests(t)
costCenterUpdates := []*v1.CostCenter{
{
AttributionId: string(db.NewTeamAttributionID(uuid.New().String())),
SpendingLimit: 5000,
BillingStrategy: v1.CostCenter_BILLING_STRATEGY_OTHER,
AttributionId: string(db.NewUserAttributionID(uuid.New().String())),
SpendingLimit: 8000,
BillingStrategy: v1.CostCenter_BILLING_STRATEGY_STRIPE,
},
{
AttributionId: string(db.NewTeamAttributionID(uuid.New().String())),
SpendingLimit: 8000,
AttributionId: string(db.NewUserAttributionID(uuid.New().String())),
SpendingLimit: 500,
BillingStrategy: v1.CostCenter_BILLING_STRATEGY_OTHER,
},
{
Expand All @@ -243,7 +244,7 @@ func TestGetAndSetCostCenter(t *testing.T) {
},
{
AttributionId: string(db.NewTeamAttributionID(uuid.New().String())),
SpendingLimit: 5000,
SpendingLimit: 0,
BillingStrategy: v1.CostCenter_BILLING_STRATEGY_OTHER,
},
}
Expand Down
124 changes: 91 additions & 33 deletions components/usage/pkg/db/cost_center.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (

"github.com/gitpod-io/gitpod/common-go/log"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -39,8 +41,9 @@ func (d *CostCenter) TableName() string {
}

type DefaultSpendingLimit struct {
ForTeams int32 `json:"forTeams"`
ForUsers int32 `json:"forUsers"`
ForTeams int32 `json:"forTeams"`
ForUsers int32 `json:"forUsers"`
MinForUsersOnStripe int32 `json:"minForUsersOnStripe"`
}

func NewCostCenterManager(conn *gorm.DB, cfg DefaultSpendingLimit) *CostCenterManager {
Expand Down Expand Up @@ -102,57 +105,112 @@ func getCostCenter(ctx context.Context, conn *gorm.DB, attributionId Attribution
return costCenter, nil
}

func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, costCenter CostCenter) (CostCenter, error) {
func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, newCC CostCenter) (CostCenter, error) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The multi-level branching here could be simplified to shorten the code, but it's deliberately done this way for readability (at the cost of "duplicating" the handling of Other -> Stripe. I feel this is acceptable, as we win more points on understanding the flows.

if newCC.SpendingLimit < 0 {
return CostCenter{}, status.Errorf(codes.InvalidArgument, "Spending limit cannot be set below zero.")
}

attributionID := newCC.ID
// retrieving the existing cost center to maintain the readonly values
existingCostCenter, err := c.GetOrCreateCostCenter(ctx, costCenter.ID)
existingCC, err := c.GetOrCreateCostCenter(ctx, newCC.ID)
if err != nil {
return CostCenter{}, err
return CostCenter{}, status.Errorf(codes.NotFound, "cost center does not exist")
}

now := time.Now()

// we always update the creationTime
costCenter.CreationTime = NewVarcharTime(now)
newCC.CreationTime = NewVarcharTime(now)
// we don't allow setting the nextBillingTime from outside
costCenter.NextBillingTime = existingCostCenter.NextBillingTime

// Do we have a billing strategy update?
if costCenter.BillingStrategy != existingCostCenter.BillingStrategy {
switch costCenter.BillingStrategy {
case CostCenter_Stripe:
// moving to stripe -> let's run a finalization
finalizationUsage, err := c.ComputeInvoiceUsageRecord(ctx, costCenter.ID)
newCC.NextBillingTime = existingCC.NextBillingTime

isTeam := attributionID.IsEntity(AttributionEntity_Team)
isUser := attributionID.IsEntity(AttributionEntity_User)

if isUser {
// New billing strategy is Stripe
if newCC.BillingStrategy == CostCenter_Stripe {
if newCC.SpendingLimit < c.cfg.MinForUsersOnStripe {
return CostCenter{}, status.Errorf(codes.FailedPrecondition, "individual users cannot lower their spending below %d", c.cfg.ForUsers)
}
}

// Billing strategy remains unchanged (Other)
if newCC.BillingStrategy == CostCenter_Other && existingCC.BillingStrategy == CostCenter_Other {
if newCC.SpendingLimit != existingCC.SpendingLimit {
return CostCenter{}, status.Errorf(codes.FailedPrecondition, "individual users on a free plan cannot adjust their spending limit")
}
}

// Downgrading from stripe
if existingCC.BillingStrategy == CostCenter_Stripe && newCC.BillingStrategy == CostCenter_Other {
newCC.SpendingLimit = c.cfg.ForUsers
Copy link
Contributor

@jankeromnes jankeromnes Oct 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this flow still doesn't seem to work properly 🤔

  1. Create a team called "Gitpod [Something]"
  2. Go to /billing (you have 500 credits 👍)
(view DB state)
mysql> select * from d_b_cost_center where id = 'user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7';
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
| id                                        | spendingLimit | deleted | _lastModified              | creationTime             | billingStrategy | nextBillingTime          |
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           500 |       0 | 2022-10-13 08:59:20.491162 | 2022-10-13T08:59:20.488Z | other           | 2022-11-13T08:59:20.489Z |
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
1 row in set (0.01 sec)
  1. Upgrade by adding a payment method, then refresh (limit is 100 ? 😳 Should be 1000) EDIT: Actually, I'm not sure where the 100 comes from, but in any case the upgrade should fail if the provided limit is < 1000 right?
(view DB state)
mysql> select * from d_b_cost_center where id = 'user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7';
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
| id                                        | spendingLimit | deleted | _lastModified              | creationTime             | billingStrategy | nextBillingTime          |
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           500 |       0 | 2022-10-13 08:59:20.491162 | 2022-10-13T08:59:20.488Z | other           | 2022-11-13T08:59:20.489Z |
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           100 |       0 | 2022-10-13 09:01:33.580476 | 2022-10-13T09:01:33.576Z | stripe          |                          |
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
2 rows in set (0.02 sec)
  1. Try to set the limit to 150 -- it should fail because that's < 1000, but actually it works 😳
(view DB state)
mysql> select * from d_b_cost_center where id = 'user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7';
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
| id                                        | spendingLimit | deleted | _lastModified              | creationTime             | billingStrategy | nextBillingTime          |
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           500 |       0 | 2022-10-13 08:59:20.491162 | 2022-10-13T08:59:20.488Z | other           | 2022-11-13T08:59:20.489Z |
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           100 |       0 | 2022-10-13 09:01:33.580476 | 2022-10-13T09:01:33.576Z | stripe          |                          |
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           100 |       0 | 2022-10-13 09:03:49.189822 | 2022-10-13T09:03:49.186Z | stripe          |                          |
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           150 |       0 | 2022-10-13 09:03:53.190384 | 2022-10-13T09:03:53.187Z | stripe          |                          |
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
4 rows in set (0.02 sec)
  1. Manage plan, then cancel your Stripe subscription -- the limit should reset to 500, but instead it remains at its previous value 🐛
(view DB state)
mysql> select * from d_b_cost_center where id = 'user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7';
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
| id                                        | spendingLimit | deleted | _lastModified              | creationTime             | billingStrategy | nextBillingTime          |
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           500 |       0 | 2022-10-13 08:59:20.491162 | 2022-10-13T08:59:20.488Z | other           | 2022-11-13T08:59:20.489Z |
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           100 |       0 | 2022-10-13 09:01:33.580476 | 2022-10-13T09:01:33.576Z | stripe          |                          |
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           100 |       0 | 2022-10-13 09:03:49.189822 | 2022-10-13T09:03:49.186Z | stripe          |                          |
| user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 |           150 |       0 | 2022-10-13 09:03:53.190384 | 2022-10-13T09:03:53.187Z | stripe          |                          |
+-------------------------------------------+---------------+---------+----------------------------+--------------------------+-----------------+--------------------------+
4 rows in set (0.01 sec)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this rolls out, it will be 0 (due to missing config) but because we set it on the server side, this is okay and the check here will simply check if new limit is > 0. We'll add the config to ensure it does not prevent setting lower in subsequent PR.

Copy link
Contributor

@jankeromnes jankeromnes Oct 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, so it's even 0 in preview environments 👌 thanks for clarifying. That explains the allowed < 1000 custom limit.

Still, this doesn't explain why, upon cancellation, the user cost center does not go back to other and doesn't reset its usage limit to 500.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Manage plan, then cancel your Stripe subscription -- the limit should reset to 500, but instead it remains at its previous value 🐛

That's because the request to the usage component didn't set the billing strategy to Other

user:f0f6f6f5-f8e4-47e4-80f9-420f8cbcbfb7 | 150 | 0 | 2022-10-13 09:03:53.190384 | 2022-10-13T09:03:53.187Z | stripe

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But shouldn't that happen automatically when Stripe calls Gitpod to tell it that a subscription was cancelled?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the config (now done in preview also) addresses 3. not failing.

The default 100 comes from protected defaultSpendingLimit = 100; defined in gitpod-server-impl.ts

// see you next month
newCC.NextBillingTime = NewVarcharTime(now.AddDate(0, 1, 0))
}

// Upgrading to Stripe
if existingCC.BillingStrategy == CostCenter_Other && newCC.BillingStrategy == CostCenter_Stripe {
err := c.upgradeToStripe(ctx, attributionID)
if err != nil {
return CostCenter{}, err
}
if finalizationUsage != nil {
err = UpdateUsage(ctx, c.conn, *finalizationUsage)
if err != nil {
return CostCenter{}, err
}
}

// we don't manage stripe billing cycle
costCenter.NextBillingTime = VarcharTime{}

case CostCenter_Other:
// cancelled from stripe reset the spending limit
if costCenter.ID.IsEntity(AttributionEntity_Team) {
costCenter.SpendingLimit = c.cfg.ForTeams
} else {
costCenter.SpendingLimit = c.cfg.ForUsers
newCC.NextBillingTime = VarcharTime{}
}
} else if isTeam {
// Billing strategy is Other, and it remains unchanged
if existingCC.BillingStrategy == CostCenter_Other && newCC.BillingStrategy == CostCenter_Other {
// It is impossible for a team without Stripe billing to change their spending limit
if newCC.SpendingLimit != c.cfg.ForTeams {
return CostCenter{}, status.Errorf(codes.FailedPrecondition, "teams without a subscription cannot change their spending limit")
}
}

// Downgrading from stripe
if existingCC.BillingStrategy == CostCenter_Stripe && newCC.BillingStrategy == CostCenter_Other {
newCC.SpendingLimit = c.cfg.ForTeams
// see you next month
costCenter.NextBillingTime = NewVarcharTime(now.AddDate(0, 1, 0))
newCC.NextBillingTime = NewVarcharTime(now.AddDate(0, 1, 0))
}

// Upgrading to Stripe
if existingCC.BillingStrategy == CostCenter_Other && newCC.BillingStrategy == CostCenter_Stripe {
err := c.upgradeToStripe(ctx, attributionID)
if err != nil {
return CostCenter{}, err
}

// we don't manage stripe billing cycle
newCC.NextBillingTime = VarcharTime{}
}
} else {
return CostCenter{}, status.Errorf(codes.InvalidArgument, "Unknown attribution entity %s", string(attributionID))
}

log.WithField("cost_center", costCenter).Info("saving cost center.")
db := c.conn.Save(&costCenter)
log.WithField("cost_center", newCC).Info("saving cost center.")
db := c.conn.Save(&newCC)
if db.Error != nil {
return CostCenter{}, fmt.Errorf("failed to save cost center for attributionID %s: %w", costCenter.ID, db.Error)
return CostCenter{}, fmt.Errorf("failed to save cost center for attributionID %s: %w", newCC.ID, db.Error)
}
return costCenter, nil
return newCC, nil
}

func (c *CostCenterManager) upgradeToStripe(ctx context.Context, attributionID AttributionID) error {
// moving to stripe -> let's run a finalization
finalizationUsage, err := c.ComputeInvoiceUsageRecord(ctx, attributionID)
if err != nil {
return err
}
if finalizationUsage != nil {
err = UpdateUsage(ctx, c.conn, *finalizationUsage)
if err != nil {
return err
}
}

return nil
}

func (c *CostCenterManager) ComputeInvoiceUsageRecord(ctx context.Context, attributionID AttributionID) (*Usage, error) {
Expand Down
160 changes: 144 additions & 16 deletions components/usage/pkg/db/cost_center_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/gitpod-io/gitpod/usage/pkg/db/dbtest"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -58,26 +60,143 @@ func TestCostCenterManager_GetOrCreateCostCenter(t *testing.T) {

func TestCostCenterManager_UpdateCostCenter(t *testing.T) {
conn := dbtest.ConnectForTests(t)
mnr := db.NewCostCenterManager(conn, db.DefaultSpendingLimit{
ForTeams: 0,
ForUsers: 500,
limits := db.DefaultSpendingLimit{
ForTeams: 0,
ForUsers: 500,
MinForUsersOnStripe: 1000,
}

t.Run("prevents updates to negative spending limit", func(t *testing.T) {
mnr := db.NewCostCenterManager(conn, limits)
userAttributionID := db.NewUserAttributionID(uuid.New().String())
teamAttributionID := db.NewTeamAttributionID(uuid.New().String())
cleanUp(t, conn, userAttributionID, teamAttributionID)

_, err := mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: userAttributionID,
BillingStrategy: db.CostCenter_Other,
SpendingLimit: -1,
})
require.Error(t, err)
require.Equal(t, codes.InvalidArgument, status.Code(err))

_, err = mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: teamAttributionID,
BillingStrategy: db.CostCenter_Stripe,
SpendingLimit: -1,
})
require.Error(t, err)
require.Equal(t, codes.InvalidArgument, status.Code(err))
})
team := db.NewTeamAttributionID(uuid.New().String())
cleanUp(t, conn, team)
teamCC, err := mnr.GetOrCreateCostCenter(context.Background(), team)
t.Cleanup(func() {
conn.Model(&db.CostCenter{}).Delete(teamCC)

t.Run("individual user on Other billing strategy cannot change spending limit of 500", func(t *testing.T) {
mnr := db.NewCostCenterManager(conn, limits)
userAttributionID := db.NewUserAttributionID(uuid.New().String())
cleanUp(t, conn, userAttributionID)

_, err := mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: userAttributionID,
BillingStrategy: db.CostCenter_Other,
SpendingLimit: 501,
})
require.Error(t, err)
require.Equal(t, codes.FailedPrecondition, status.Code(err))

})
require.NoError(t, err)
require.Equal(t, int32(0), teamCC.SpendingLimit)

teamCC.SpendingLimit = 2000
teamCC, err = mnr.UpdateCostCenter(context.Background(), teamCC)
require.NoError(t, err)
t.Cleanup(func() {
conn.Model(&db.CostCenter{}).Delete(teamCC)
t.Run("individual user upgrading to stripe can set a limit of 1000 or more, but not less than 1000", func(t *testing.T) {
mnr := db.NewCostCenterManager(conn, limits)
userAttributionID := db.NewUserAttributionID(uuid.New().String())
cleanUp(t, conn, userAttributionID)

// Upgrading to Stripe requires spending limit
res, err := mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: userAttributionID,
BillingStrategy: db.CostCenter_Stripe,
SpendingLimit: 1000,
})
require.NoError(t, err)
requireCostCenterEqual(t, db.CostCenter{
ID: userAttributionID,
SpendingLimit: 1000,
BillingStrategy: db.CostCenter_Stripe,
}, res)

// Try to lower the spending limit below configured limit
_, err = mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: userAttributionID,
BillingStrategy: db.CostCenter_Stripe,
SpendingLimit: 999,
})
require.Error(t, err, "lowering spending limit below configured value is not allowed for user subscriptions")

// Try to update the cost center to higher usage limit
res, err = mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: userAttributionID,
BillingStrategy: db.CostCenter_Stripe,
SpendingLimit: 1001,
})
require.NoError(t, err)
requireCostCenterEqual(t, db.CostCenter{
ID: userAttributionID,
SpendingLimit: 1001,
BillingStrategy: db.CostCenter_Stripe,
}, res)
})

t.Run("team on Other billing strategy get a spending limit of 0, and cannot change it", func(t *testing.T) {
mnr := db.NewCostCenterManager(conn, limits)
teamAttributionID := db.NewTeamAttributionID(uuid.New().String())
cleanUp(t, conn, teamAttributionID)

// Allows udpating cost center as long as spending limit remains as configured
res, err := mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: teamAttributionID,
BillingStrategy: db.CostCenter_Other,
SpendingLimit: limits.ForTeams,
})
require.NoError(t, err)
requireCostCenterEqual(t, db.CostCenter{
ID: teamAttributionID,
SpendingLimit: limits.ForTeams,
BillingStrategy: db.CostCenter_Other,
}, res)

// Prevents updating when spending limit changes
_, err = mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: teamAttributionID,
BillingStrategy: db.CostCenter_Other,
SpendingLimit: 1,
})
require.Error(t, err)
})

t.Run("team on Stripe billing strategy can set arbitrary positive spending limit", func(t *testing.T) {
mnr := db.NewCostCenterManager(conn, limits)
teamAttributionID := db.NewTeamAttributionID(uuid.New().String())
cleanUp(t, conn, teamAttributionID)

// Allows udpating cost center as long as spending limit remains as configured
res, err := mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: teamAttributionID,
BillingStrategy: db.CostCenter_Stripe,
SpendingLimit: limits.ForTeams,
})
require.NoError(t, err)
requireCostCenterEqual(t, db.CostCenter{
ID: teamAttributionID,
BillingStrategy: db.CostCenter_Stripe,
SpendingLimit: limits.ForTeams,
}, res)

// Allows updating cost center to any positive value
_, err = mnr.UpdateCostCenter(context.Background(), db.CostCenter{
ID: teamAttributionID,
BillingStrategy: db.CostCenter_Stripe,
SpendingLimit: 10,
})
require.NoError(t, err)
})
require.Equal(t, int32(2000), teamCC.SpendingLimit)
}

func TestSaveCostCenterMovedToStripe(t *testing.T) {
Expand Down Expand Up @@ -116,3 +235,12 @@ func cleanUp(t *testing.T, conn *gorm.DB, attributionIds ...db.AttributionID) {
}
})
}

func requireCostCenterEqual(t *testing.T, expected, actual db.CostCenter) {
t.Helper()

// ignore timestamps in comparsion
require.Equal(t, expected.ID, actual.ID)
require.EqualValues(t, expected.SpendingLimit, actual.SpendingLimit)
require.Equal(t, expected.BillingStrategy, actual.BillingStrategy)
}
Loading