-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[usage] Cost centers get nextBillingTime
#12913
Conversation
b5bf52b
to
6e9c65e
Compare
6e9c65e
to
e3ed2c6
Compare
nextBillingTime
started the job as gitpod-build-se-cost-center-next-billing.4 because the annotations in the pull request description changed |
ab2cfcb
to
23a3f31
Compare
23a3f31
to
dedd727
Compare
@@ -114,4 +114,7 @@ message CostCenter { | |||
BILLING_STRATEGY_OTHER = 1; | |||
} | |||
BillingStrategy billing_strategy = 3; | |||
|
|||
// next_billing_time specifies when the next billing cycle happens. Only set when billing strategy is 'other'. This property is readonly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice use of proto comments to describe semantics!
components/usage/config.json
Outdated
"defaultSpendingLimitForUsers": 5000, | ||
"defaultSpendingLimitForTeams": 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
components/usage/pkg/apiv1/usage.go
Outdated
DefaultSpendingLimitForUsers int32 `json:"defaultSpendingLimitForUsers,omitempty"` | ||
|
||
DefaultSpendingLimitForTeams int32 `json:"defaultSpendingLimitForTeams,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DefaultSpendingLimitForUsers int32 `json:"defaultSpendingLimitForUsers,omitempty"` | |
DefaultSpendingLimitForTeams int32 `json:"defaultSpendingLimitForTeams,omitempty"` | |
DefaultSpendingLimitForUsers int `json:"defaultSpendingLimitForUsers,omitempty"` | |
DefaultSpendingLimitForTeams int `json:"defaultSpendingLimitForTeams,omitempty"` |
This should be sufficient. int
is actually compiler target dependant but often it's easier to use int
and let the compiler deal with it.
@@ -35,6 +36,8 @@ type Config struct { | |||
StripeCredentialsFile string `json:"stripeCredentialsFile,omitempty"` | |||
|
|||
Server *baseserver.Configuration `json:"server,omitempty"` | |||
|
|||
apiv1.UsageConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd name this explicitly. It also allows you to be explicit about the json
field name which guards against issues when you rename the apiv1.UsageConfig
in the future.
dedd727
to
cda8e7a
Compare
cda8e7a
to
f78a64d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good, left a couple of questions.
/hold
We can tackle these in a follow-up if we want.
I'd also recommend isolating DB migrations away from application changes which make use of those values. When we introduced #12873 and rolled it out, we actually saw requests fail as the migration was not compatible with the application changes - while this can happen with migrations being independent as well, it makes it easier to reason about it in the given PR.
@@ -30,28 +33,77 @@ func TestCostCenter_WriteRead(t *testing.T) { | |||
require.NoError(t, tx.Error) | |||
require.Equal(t, costCenter.ID, read.ID) | |||
require.Equal(t, costCenter.SpendingLimit, read.SpendingLimit) | |||
} | |||
|
|||
func TestGetCostCenter(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func TestGetCostCenter(t *testing.T) { | |
func TestCostCenterManager_GetOrCreateCostCenter(t *testing.T) { |
} | ||
|
||
func TestGetCostCenter(t *testing.T) { | ||
func TestSaveCostCenter(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func TestSaveCostCenter(t *testing.T) { | |
func TestCostCenterManager_SaveCostCenter(t *testing.T) { |
db := conn.WithContext(ctx) | ||
costCenter.CreationTime = NewVarcharTime(time.Now()) | ||
db = db.Save(costCenter) | ||
func (c *CostCenterManager) SaveCostCenter(ctx context.Context, costCenter CostCenter) (CostCenter, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a consumer of the CostCenterManager
API perspective, it's quite unclear what the difference is between GetOrCreate
and SaveCostCenter
. Why would I use one over the other?
I think here, we can rename it to
func (c *CostCenterManager) SaveCostCenter(ctx context.Context, costCenter CostCenter) (CostCenter, error) { | |
func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, costCenter CostCenter) (CostCenter, error) { |
To better communicate that this is intended to work on an existing record(even if it needs to be created under the hood)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I aligned with the naming of gorm but have no problem changing this to 'Update..'.
} | ||
return costCenter, nil | ||
} | ||
|
||
func (c *CostCenterManager) ComputeFinalization(ctx context.Context, costCenter CostCenter) (Usage, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func (c *CostCenterManager) ComputeFinalization(ctx context.Context, costCenter CostCenter) (Usage, error) { | |
func (c *CostCenterManager) ComputeFinalization(ctx context.Context, costCenter CostCenter) (Usage, error) { |
From this signature, I'd have expected that it will also store the Usage record in the DB. Perhaps renaming to InvoiceUsageRecordForCostCenter
would convey this better.
Additionally, why this is method accept the costCenter CostCenter
when it only uses the costCenter.ID
? Could we supply only the ID here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally use 'Compute...' to indicate that a function doesn't have aside-effects but just computes something. But I'm happy to change the name to make it clearer for you.
I'm passing in the costCenter because it contains information about spendingLimit
which is likely to be used pretty soon, but we can fetch it within the function if needed.
func cleanUp(t *testing.T, conn *gorm.DB, attributionIds ...db.AttributionID) { | ||
t.Cleanup(func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
func cleanUp(t *testing.T, conn *gorm.DB, attributionIds ...db.AttributionID) { | |
t.Cleanup(func() { | |
func cleanUp(t *testing.T, conn *gorm.DB, attributionIds ...db.AttributionID) { | |
t.Helper() | |
t.Cleanup(func() { |
just for general practice
@@ -43,7 +43,7 @@ type Usage struct { | |||
CreditCents CreditCents `gorm:"column:creditCents;type:bigint;" json:"creditCents"` | |||
EffectiveTime VarcharTime `gorm:"column:effectiveTime;type:varchar;size:255;" json:"effectiveTime"` | |||
Kind UsageKind `gorm:"column:kind;type:char;size:10;" json:"kind"` | |||
WorkspaceInstanceID uuid.UUID `gorm:"column:workspaceInstanceId;type:char;size:36;" json:"workspaceInstanceId"` | |||
WorkspaceInstanceID *uuid.UUID `gorm:"column:workspaceInstanceId;type:char;size:36;" json:"workspaceInstanceId"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I'd have split this into a separate pre-PR to isolate it from the rest of the main logic in this PR.
func (a AttributionID) IsUser() bool { | ||
entity, _ := a.Values() | ||
return entity == "user" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[personal] I generally prefer not to introduce Binary methods on an object which can have more than a binary number of outcomes. This forces the caller to use
switch a.Kind {
case User:
...
case Team:
...
default:
...
}
This tends to be more resilient over time as you don't make binary choices anymore, but are forced to acknowledge existence of other values.
f78a64d
to
58dd497
Compare
started the job as gitpod-build-se-cost-center-next-billing.11 because the annotations in the pull request description changed |
58dd497
to
39790df
Compare
39790df
to
89bb051
Compare
/werft run with-clean-slate-deployment 👍 started the job as gitpod-build-se-cost-center-next-billing.14 |
7642da9
to
aa21d77
Compare
/werft run 👍 started the job as gitpod-build-se-cost-center-next-billing.17 |
f3b9967
to
05c183e
Compare
/werft run with-clean-slate-deployment 👍 started the job as gitpod-build-se-cost-center-next-billing.20 |
/unhold |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/hold
See comment for reasons for the hold
. Happy to discuss further if necessary
@@ -246,6 +246,7 @@ type UsageConfig struct { | |||
Enabled bool `json:"enabled"` | |||
Schedule string `json:"schedule"` | |||
BillInstancesAfter *time.Time `json:"billInstancesAfter"` | |||
DefaultSpendingLimit map[string]int32 `json:"defaultSpendingLimit"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using a map
, my preference would be for this to use the DefaultSpendingLimit struct.
As this is in the experimental
section, I'll approve this on the basis that it's not for public usage, but it would keep with the consistency of using structs in the configuration
05c183e
to
51b0fd5
Compare
/unhold |
Description
This PR contains small steps towards having cost centers for all users and teams, even if they haven't signed up for stripe pay-as-you go, yet, but using the billing strategy
other
.In this PR I
nextBillingTime
on cost centers that will be used in an upcoming PR where a control loop uses the date to find cost centers that need to run finalization (i.e. create a synthetic invoice to balance out the ledger table).GetCostCenter
always return a cost centerRelated Issue(s)
Fixes #
How to test
Release Notes
Documentation
Werft options: