Skip to content

Commit f01500f

Browse files
committed
[stripe] Set reportId on invoices after updating credits
1 parent 8d00926 commit f01500f

File tree

3 files changed

+76
-21
lines changed

3 files changed

+76
-21
lines changed

components/usage/pkg/apiv1/billing.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,17 @@ type BillingService struct {
3838
}
3939

4040
func (s *BillingService) UpdateInvoices(ctx context.Context, in *v1.UpdateInvoicesRequest) (*v1.UpdateInvoicesResponse, error) {
41-
credits, err := s.creditSummaryForTeams(in.GetSessions())
41+
if in.ReportId == "" {
42+
return nil, status.Errorf(codes.InvalidArgument, "missing report ID")
43+
}
44+
45+
creditSummary, err := s.creditSummaryForTeams(in.GetSessions(), in.ReportId)
4246
if err != nil {
4347
log.Log.WithError(err).Errorf("Failed to compute credit summary.")
4448
return nil, status.Errorf(codes.InvalidArgument, "failed to compute credit summary")
4549
}
4650

47-
err = s.stripeClient.UpdateUsage(ctx, credits)
51+
err = s.stripeClient.UpdateUsage(ctx, creditSummary)
4852
if err != nil {
4953
log.Log.WithError(err).Errorf("Failed to update stripe invoices.")
5054
return nil, status.Errorf(codes.Internal, "failed to update stripe invoices")
@@ -89,7 +93,7 @@ func (s *BillingService) GetUpcomingInvoice(ctx context.Context, in *v1.GetUpcom
8993
}, nil
9094
}
9195

92-
func (s *BillingService) creditSummaryForTeams(sessions []*v1.BilledSession) (map[string]int64, error) {
96+
func (s *BillingService) creditSummaryForTeams(sessions []*v1.BilledSession, reportID string) (map[string]stripe.CreditSummary, error) {
9397
creditsPerTeamID := map[string]float64{}
9498

9599
for _, session := range sessions {
@@ -114,9 +118,12 @@ func (s *BillingService) creditSummaryForTeams(sessions []*v1.BilledSession) (ma
114118
creditsPerTeamID[id] += session.GetCredits()
115119
}
116120

117-
rounded := map[string]int64{}
121+
rounded := map[string]stripe.CreditSummary{}
118122
for teamID, credits := range creditsPerTeamID {
119-
rounded[teamID] = int64(math.Ceil(credits))
123+
rounded[teamID] = stripe.CreditSummary{
124+
Credits: int64(math.Ceil(credits)),
125+
ReportID: reportID,
126+
}
120127
}
121128

122129
return rounded, nil

components/usage/pkg/apiv1/billing_test.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,26 @@ import (
1313
"github.com/gitpod-io/gitpod/usage/pkg/stripe"
1414
"github.com/google/uuid"
1515
"github.com/stretchr/testify/require"
16-
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
16+
"google.golang.org/protobuf/types/known/timestamppb"
1717
"gorm.io/gorm"
1818
)
1919

2020
func TestCreditSummaryForTeams(t *testing.T) {
2121
teamID_A, teamID_B := uuid.New().String(), uuid.New().String()
2222
teamAttributionID_A, teamAttributionID_B := db.NewTeamAttributionID(teamID_A), db.NewTeamAttributionID(teamID_B)
23+
reportID := "report_id_1"
2324

2425
scenarios := []struct {
2526
Name string
2627
Sessions []*v1.BilledSession
2728
BillSessionsAfter time.Time
28-
Expected map[string]int64
29+
Expected map[string]stripe.CreditSummary
2930
}{
3031
{
3132
Name: "no instances in report, no summary",
3233
BillSessionsAfter: time.Time{},
3334
Sessions: []*v1.BilledSession{},
34-
Expected: map[string]int64{},
35+
Expected: map[string]stripe.CreditSummary{},
3536
},
3637
{
3738
Name: "skips user attributions",
@@ -41,7 +42,7 @@ func TestCreditSummaryForTeams(t *testing.T) {
4142
AttributionId: string(db.NewUserAttributionID(uuid.New().String())),
4243
},
4344
},
44-
Expected: map[string]int64{},
45+
Expected: map[string]stripe.CreditSummary{},
4546
},
4647
{
4748
Name: "two workspace instances",
@@ -58,9 +59,12 @@ func TestCreditSummaryForTeams(t *testing.T) {
5859
Credits: 10,
5960
},
6061
},
61-
Expected: map[string]int64{
62+
Expected: map[string]stripe.CreditSummary{
6263
// total of 2 days runtime, at 10 credits per hour, that's 480 credits
63-
teamID_A: 480,
64+
teamID_A: {
65+
Credits: 480,
66+
ReportID: reportID,
67+
},
6468
},
6569
},
6670
{
@@ -78,10 +82,16 @@ func TestCreditSummaryForTeams(t *testing.T) {
7882
Credits: (24) * 10,
7983
},
8084
},
81-
Expected: map[string]int64{
85+
Expected: map[string]stripe.CreditSummary{
8286
// total of 2 days runtime, at 10 credits per hour, that's 480 credits
83-
teamID_A: 120,
84-
teamID_B: 240,
87+
teamID_A: {
88+
Credits: 120,
89+
ReportID: reportID,
90+
},
91+
teamID_B: {
92+
Credits: 240,
93+
ReportID: reportID,
94+
},
8595
},
8696
},
8797
{
@@ -101,16 +111,19 @@ func TestCreditSummaryForTeams(t *testing.T) {
101111
StartTime: timestamppb.New(time.Now().AddDate(0, 0, -3)),
102112
},
103113
},
104-
Expected: map[string]int64{
105-
teamID_A: 120,
114+
Expected: map[string]stripe.CreditSummary{
115+
teamID_A: {
116+
Credits: 120,
117+
ReportID: reportID,
118+
},
106119
},
107120
},
108121
}
109122

110123
for _, s := range scenarios {
111124
t.Run(s.Name, func(t *testing.T) {
112125
svc := NewBillingService(&stripe.Client{}, s.BillSessionsAfter, &gorm.DB{})
113-
actual, err := svc.creditSummaryForTeams(s.Sessions)
126+
actual, err := svc.creditSummaryForTeams(s.Sessions, reportID)
114127
require.NoError(t, err)
115128
require.Equal(t, s.Expected, actual)
116129
})

components/usage/pkg/stripe/stripe.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import (
1616
"github.com/stripe/stripe-go/v72/client"
1717
)
1818

19+
const (
20+
reportIDMetadataKey = "reportId"
21+
)
22+
1923
type Client struct {
2024
sc *client.API
2125
}
@@ -58,9 +62,14 @@ type Invoice struct {
5862
Credits int64
5963
}
6064

65+
type CreditSummary struct {
66+
Credits int64
67+
ReportID string
68+
}
69+
6170
// UpdateUsage updates teams' Stripe subscriptions with usage data
6271
// `usageForTeam` is a map from team name to total workspace seconds used within a billing period.
63-
func (c *Client) UpdateUsage(ctx context.Context, creditsPerTeam map[string]int64) error {
72+
func (c *Client) UpdateUsage(ctx context.Context, creditsPerTeam map[string]CreditSummary) error {
6473
teamIds := make([]string, 0, len(creditsPerTeam))
6574
for k := range creditsPerTeam {
6675
teamIds = append(teamIds, k)
@@ -117,7 +126,7 @@ func (c *Client) findCustomers(ctx context.Context, query string) ([]*stripe.Cus
117126
return customers, nil
118127
}
119128

120-
func (c *Client) updateUsageForCustomer(ctx context.Context, customer *stripe.Customer, credits int64) (*UsageRecord, error) {
129+
func (c *Client) updateUsageForCustomer(ctx context.Context, customer *stripe.Customer, summary CreditSummary) (*UsageRecord, error) {
121130
subscriptions := customer.Subscriptions.Data
122131
if len(subscriptions) != 1 {
123132
return nil, fmt.Errorf("customer has an unexpected number of subscriptions %v (expected 1, got %d)", subscriptions, len(subscriptions))
@@ -136,15 +145,27 @@ func (c *Client) updateUsageForCustomer(ctx context.Context, customer *stripe.Cu
136145
Context: ctx,
137146
},
138147
SubscriptionItem: stripe.String(subscriptionItemId),
139-
Quantity: stripe.Int64(credits),
148+
Quantity: stripe.Int64(summary.Credits),
140149
})
141150
if err != nil {
142151
return nil, fmt.Errorf("failed to register usage for customer %q on subscription item %s", customer.Name, subscriptionItemId)
143152
}
144153

154+
invoice, err := c.GetUpcomingInvoice(ctx, customer.ID)
155+
if err != nil {
156+
return nil, fmt.Errorf("failed to find upcoming invoice for customer %s: %w", customer.ID, err)
157+
}
158+
159+
_, err = c.UpdateInvoiceMetadata(ctx, invoice.ID, map[string]string{
160+
reportIDMetadataKey: summary.ReportID,
161+
})
162+
if err != nil {
163+
return nil, fmt.Errorf("failed to udpate invoice %s metadata with report ID: %w", invoice.ID, err)
164+
}
165+
145166
return &UsageRecord{
146167
SubscriptionItemID: subscriptionItemId,
147-
Quantity: credits,
168+
Quantity: summary.Credits,
148169
}, nil
149170
}
150171

@@ -205,6 +226,20 @@ func (c *Client) GetUpcomingInvoice(ctx context.Context, customerID string) (*In
205226
}, nil
206227
}
207228

229+
func (c *Client) UpdateInvoiceMetadata(ctx context.Context, invoiceID string, metadata map[string]string) (*stripe.Invoice, error) {
230+
invoice, err := c.sc.Invoices.Update(invoiceID, &stripe.InvoiceParams{
231+
Params: stripe.Params{
232+
Context: ctx,
233+
Metadata: metadata,
234+
},
235+
})
236+
if err != nil {
237+
return nil, fmt.Errorf("failed to update invoice %s metadata: %w", invoiceID, err)
238+
}
239+
240+
return invoice, nil
241+
}
242+
208243
// queriesForCustomersWithTeamIds constructs Stripe query strings to find the Stripe Customer for each teamId
209244
// It returns multiple queries, each being a big disjunction of subclauses so that we can process multiple teamIds in one query.
210245
// `clausesPerQuery` is a limit enforced by the Stripe API.

0 commit comments

Comments
 (0)