-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[billing] Create Stripe invoices for teams based on their usage #10713
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -132,18 +132,13 @@ func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time. | |
} | ||
|
||
func submitUsageReport(report []TeamUsage) { | ||
var teamIdSet = make(map[string]bool) | ||
|
||
// Convert the usage report into a set of teamIds occurring in the report | ||
// Convert the usage report to sum all entries for the same team. | ||
var summedReport = make(map[string]int64) | ||
for _, usageEntry := range report { | ||
teamIdSet[usageEntry.TeamID] = true | ||
} | ||
teamIds := make([]string, 0, len(teamIdSet)) | ||
for k := range teamIdSet { | ||
teamIds = append(teamIds, k) | ||
summedReport[usageEntry.TeamID] += usageEntry.WorkspaceSeconds | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Go newbie question: There is no need to initialize new Map entries to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, maps are initialised such that values have the default value for their type. |
||
} | ||
|
||
stripe.FindCustomersForTeamIds(teamIds) | ||
stripe.UpdateUsage(summedReport) | ||
} | ||
|
||
func generateUsageReport(teams []teamWithWorkspaces, maxStopTime time.Time) ([]TeamUsage, error) { | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -13,13 +13,15 @@ import ( | |||||
"github.com/gitpod-io/gitpod/common-go/log" | ||||||
"github.com/stripe/stripe-go/v72" | ||||||
"github.com/stripe/stripe-go/v72/customer" | ||||||
"github.com/stripe/stripe-go/v72/usagerecord" | ||||||
) | ||||||
|
||||||
type stripeKeys struct { | ||||||
PublishableKey string `json:"publishableKey"` | ||||||
SecretKey string `json:"secretKey"` | ||||||
} | ||||||
|
||||||
// Authenticate authenticates the Stripe client using a provided file containing a Stripe secret key. | ||||||
func Authenticate(apiKeyFile string) error { | ||||||
bytes, err := os.ReadFile(apiKeyFile) | ||||||
if err != nil { | ||||||
|
@@ -36,19 +38,54 @@ func Authenticate(apiKeyFile string) error { | |||||
return nil | ||||||
} | ||||||
|
||||||
// FindCustomersForTeamIds queries the stripe API to find all customers with a teamId in `teamIds`. | ||||||
func FindCustomersForTeamIds(teamIds []string) { | ||||||
// UpdateUsage updates teams' Stripe subscriptions with usage data | ||||||
// `usageForTeam` is a map from team name to total workspace seconds used within a billing period. | ||||||
andrew-farries marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
func UpdateUsage(usageForTeam map[string]int64) error { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See this comment. |
||||||
teamIds := make([]string, 0, len(usageForTeam)) | ||||||
for k := range usageForTeam { | ||||||
teamIds = append(teamIds, k) | ||||||
} | ||||||
queries := queriesForCustomersWithTeamIds(teamIds) | ||||||
|
||||||
for _, query := range queries { | ||||||
log.Infof("about to make query %q", query) | ||||||
params := &stripe.CustomerSearchParams{SearchParams: stripe.SearchParams{Query: query}} | ||||||
params := &stripe.CustomerSearchParams{ | ||||||
SearchParams: stripe.SearchParams{ | ||||||
Query: query, | ||||||
Expand: []*string{stripe.String("data.subscriptions")}, | ||||||
}, | ||||||
} | ||||||
iter := customer.Search(params) | ||||||
for iter.Next() { | ||||||
customer := iter.Customer() | ||||||
log.Infof("found customer %q for teamId %q", customer.Name, customer.Metadata["teamId"]) | ||||||
subscriptions := customer.Subscriptions.Data | ||||||
if len(subscriptions) != 1 { | ||||||
log.Errorf("customer has an unexpected number of subscriptions (expected 1, got %d)", len(subscriptions)) | ||||||
continue | ||||||
} | ||||||
subscription := customer.Subscriptions.Data[0] | ||||||
|
||||||
log.Infof("customer has subscription: %q", subscription.ID) | ||||||
if len(subscription.Items.Data) != 1 { | ||||||
log.Errorf("this subscription has an unexpected number of subscriptionItems (expected 1, got %d)", len(subscription.Items.Data)) | ||||||
continue | ||||||
} | ||||||
|
||||||
creditsUsed := workspaceSecondsToCredits(usageForTeam[customer.Metadata["teamId"]]) | ||||||
|
||||||
subscriptionItemId := subscription.Items.Data[0].ID | ||||||
log.Infof("registering usage against subscriptionItem %q", subscriptionItemId) | ||||||
_, err := usagerecord.New(&stripe.UsageRecordParams{ | ||||||
SubscriptionItem: stripe.String(subscriptionItemId), | ||||||
Quantity: stripe.Int64(creditsUsed), | ||||||
}) | ||||||
if err != nil { | ||||||
log.WithError(err).Errorf("failed to register usage for customer %q", customer.Name) | ||||||
} | ||||||
} | ||||||
} | ||||||
return nil | ||||||
} | ||||||
|
||||||
// queriesForCustomersWithTeamIds constructs Stripe query strings to find the Stripe Customer for each teamId | ||||||
|
@@ -72,3 +109,8 @@ func queriesForCustomersWithTeamIds(teamIds []string) []string { | |||||
|
||||||
return queries | ||||||
} | ||||||
|
||||||
// workspaceSecondsToCredits converts seconds (of workspace usage) into Stripe credits. | ||||||
func workspaceSecondsToCredits(seconds int64) int64 { | ||||||
return (seconds + 59) / 60 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, that's an interesting way to round up. I guess this is equivalent to something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, this is an easy way to ensure that we bill partial minutes as full minutes. |
||||||
} |
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.
uint64
?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.
We're using
int64
elsewhere to store workspace runtimes, which already gives us way more range on the positive side than we need to store monthly usage, so I don't think a switch touint64
is necessary.