Skip to content

Commit 9554966

Browse files
Andrew Farriesroboquat
Andrew Farries
authored andcommitted
Update subscription item with usage
* Convert the usage report so that it contains one entry for each team that registered usage during the billing period. * For each team, query Stripe to find the corresponding Customer. * Update each Customer's subscription with their used credits.
1 parent 0001dc6 commit 9554966

File tree

3 files changed

+95
-12
lines changed

3 files changed

+95
-12
lines changed

components/usage/pkg/controller/reconciler.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,13 @@ func (u *UsageReconciler) ReconcileTimeRange(ctx context.Context, from, to time.
132132
}
133133

134134
func submitUsageReport(report []TeamUsage) {
135-
var teamIdSet = make(map[string]bool)
136-
137-
// Convert the usage report into a set of teamIds occurring in the report
135+
// Convert the usage report to sum all entries for the same team.
136+
var summedReport = make(map[string]int64)
138137
for _, usageEntry := range report {
139-
teamIdSet[usageEntry.TeamID] = true
140-
}
141-
teamIds := make([]string, 0, len(teamIdSet))
142-
for k := range teamIdSet {
143-
teamIds = append(teamIds, k)
138+
summedReport[usageEntry.TeamID] += usageEntry.WorkspaceSeconds
144139
}
145140

146-
stripe.FindCustomersForTeamIds(teamIds)
141+
stripe.UpdateUsage(summedReport)
147142
}
148143

149144
func generateUsageReport(teams []teamWithWorkspaces, maxStopTime time.Time) ([]TeamUsage, error) {

components/usage/pkg/stripe/stripe.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/gitpod-io/gitpod/common-go/log"
1414
"github.com/stripe/stripe-go/v72"
1515
"github.com/stripe/stripe-go/v72/customer"
16+
"github.com/stripe/stripe-go/v72/usagerecord"
1617
)
1718

1819
type stripeKeys struct {
@@ -37,19 +38,54 @@ func Authenticate(apiKeyFile string) error {
3738
return nil
3839
}
3940

40-
// FindCustomersForTeamIds queries the stripe API to find all customers with a teamId in `teamIds`.
41-
func FindCustomersForTeamIds(teamIds []string) {
41+
// UpdateUsage updates teams' Stripe subscriptions with usage data
42+
// `usageForTeam` is a map from team name to total workspace seconds used within a billing period.
43+
func UpdateUsage(usageForTeam map[string]int64) error {
44+
teamIds := make([]string, 0, len(usageForTeam))
45+
for k := range usageForTeam {
46+
teamIds = append(teamIds, k)
47+
}
4248
queries := queriesForCustomersWithTeamIds(teamIds)
4349

4450
for _, query := range queries {
4551
log.Infof("about to make query %q", query)
46-
params := &stripe.CustomerSearchParams{SearchParams: stripe.SearchParams{Query: query}}
52+
params := &stripe.CustomerSearchParams{
53+
SearchParams: stripe.SearchParams{
54+
Query: query,
55+
Expand: []*string{stripe.String("data.subscriptions")},
56+
},
57+
}
4758
iter := customer.Search(params)
4859
for iter.Next() {
4960
customer := iter.Customer()
5061
log.Infof("found customer %q for teamId %q", customer.Name, customer.Metadata["teamId"])
62+
subscriptions := customer.Subscriptions.Data
63+
if len(subscriptions) != 1 {
64+
log.Errorf("customer has an unexpected number of subscriptions (expected 1, got %d)", len(subscriptions))
65+
continue
66+
}
67+
subscription := customer.Subscriptions.Data[0]
68+
69+
log.Infof("customer has subscription: %q", subscription.ID)
70+
if len(subscription.Items.Data) != 1 {
71+
log.Errorf("this subscription has an unexpected number of subscriptionItems (expected 1, got %d)", len(subscription.Items.Data))
72+
continue
73+
}
74+
75+
creditsUsed := workspaceSecondsToCredits(usageForTeam[customer.Metadata["teamId"]])
76+
77+
subscriptionItemId := subscription.Items.Data[0].ID
78+
log.Infof("registering usage against subscriptionItem %q", subscriptionItemId)
79+
_, err := usagerecord.New(&stripe.UsageRecordParams{
80+
SubscriptionItem: stripe.String(subscriptionItemId),
81+
Quantity: stripe.Int64(creditsUsed),
82+
})
83+
if err != nil {
84+
log.WithError(err).Errorf("failed to register usage for customer %q", customer.Name)
85+
}
5186
}
5287
}
88+
return nil
5389
}
5490

5591
// queriesForCustomersWithTeamIds constructs Stripe query strings to find the Stripe Customer for each teamId
@@ -73,3 +109,8 @@ func queriesForCustomersWithTeamIds(teamIds []string) []string {
73109

74110
return queries
75111
}
112+
113+
// workspaceSecondsToCredits converts seconds (of workspace usage) into Stripe credits.
114+
func workspaceSecondsToCredits(seconds int64) int64 {
115+
return (seconds + 59) / 60
116+
}

components/usage/pkg/stripe/stripe_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,50 @@ func TestCustomerQueriesForTeamIds_MultipleQueries(t *testing.T) {
8383
})
8484
}
8585
}
86+
87+
func TestWorkspaceSecondsToCreditsCalcuation(t *testing.T) {
88+
testCases := []struct {
89+
Name string
90+
Seconds int64
91+
ExpectedCredits int64
92+
}{
93+
{
94+
Name: "0 seconds",
95+
Seconds: 0,
96+
ExpectedCredits: 0,
97+
},
98+
{
99+
Name: "1 second",
100+
Seconds: 1,
101+
ExpectedCredits: 1,
102+
},
103+
{
104+
Name: "60 seconds",
105+
Seconds: 60,
106+
ExpectedCredits: 1,
107+
},
108+
{
109+
Name: "61 seconds",
110+
Seconds: 61,
111+
ExpectedCredits: 2,
112+
},
113+
{
114+
Name: "90 seconds",
115+
Seconds: 90,
116+
ExpectedCredits: 2,
117+
},
118+
{
119+
Name: "1 hour",
120+
Seconds: 3600,
121+
ExpectedCredits: 60,
122+
},
123+
}
124+
125+
for _, tc := range testCases {
126+
t.Run(tc.Name, func(t *testing.T) {
127+
actualCredits := workspaceSecondsToCredits(tc.Seconds)
128+
129+
require.Equal(t, tc.ExpectedCredits, actualCredits)
130+
})
131+
}
132+
}

0 commit comments

Comments
 (0)