Skip to content

Commit 8d00926

Browse files
easyCZroboquat
authored andcommitted
[billing] Set report ID on Invoices when updating usage credits
1 parent 62bb1c4 commit 8d00926

File tree

2 files changed

+78
-49
lines changed

2 files changed

+78
-49
lines changed

Diff for: components/usage/pkg/apiv1/billing.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/gitpod-io/gitpod/usage/pkg/db"
1616
"github.com/gitpod-io/gitpod/usage/pkg/stripe"
1717
"github.com/google/uuid"
18+
stripesdk "github.com/stripe/stripe-go/v72"
1819
"google.golang.org/grpc/codes"
1920
"google.golang.org/grpc/status"
2021
"gorm.io/gorm"
@@ -62,14 +63,19 @@ func (s *BillingService) GetUpcomingInvoice(ctx context.Context, in *v1.GetUpcom
6263
if in.GetTeamId() == "" && in.GetUserId() == "" {
6364
return nil, status.Errorf(codes.InvalidArgument, "teamId or userId is required")
6465
}
65-
var customerKind = "user"
66-
var metaId = in.GetUserId()
67-
if in.GetTeamId() != "" {
68-
customerKind = "team"
69-
metaId = in.GetTeamId()
66+
67+
var customer *stripesdk.Customer
68+
var err error
69+
if teamID := in.GetTeamId(); teamID != "" {
70+
customer, err = s.stripeClient.GetCustomerByTeamID(ctx, teamID)
71+
} else {
72+
customer, err = s.stripeClient.GetCustomerByUserID(ctx, in.GetUserId())
73+
}
74+
if err != nil {
75+
return nil, status.Errorf(codes.Internal, "failed to find customer")
7076
}
7177

72-
invoice, err := s.stripeClient.GetUpcomingInvoice(ctx, stripe.CustomerKind(customerKind), metaId)
78+
invoice, err := s.stripeClient.GetUpcomingInvoice(ctx, customer.ID)
7379
if err != nil {
7480
log.Log.WithError(err).Errorf("Failed to fetch upcoming invoice from stripe.")
7581
return nil, status.Errorf(codes.Internal, "failed to fetcht upcoming invoice from stripe")

Diff for: components/usage/pkg/stripe/stripe.go

+66-43
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package stripe
77
import (
88
"context"
99
"encoding/json"
10-
"errors"
1110
"fmt"
1211
"os"
1312
"strings"
@@ -51,26 +50,14 @@ type UsageRecord struct {
5150
Quantity int64
5251
}
5352

54-
type StripeInvoice struct {
53+
type Invoice struct {
5554
ID string
5655
SubscriptionID string
5756
Amount int64
5857
Currency string
5958
Credits int64
6059
}
6160

62-
type CustomerKind string
63-
64-
const (
65-
User CustomerKind = "user"
66-
Team = "team"
67-
)
68-
69-
var (
70-
// ErrorCustomerNotFound is returned when no stripe customer is found for a given user account
71-
ErrorCustomerNotFound = errors.New("invalid size")
72-
)
73-
7461
// UpdateUsage updates teams' Stripe subscriptions with usage data
7562
// `usageForTeam` is a map from team name to total workspace seconds used within a billing period.
7663
func (c *Client) UpdateUsage(ctx context.Context, creditsPerTeam map[string]int64) error {
@@ -82,16 +69,13 @@ func (c *Client) UpdateUsage(ctx context.Context, creditsPerTeam map[string]int6
8269

8370
for _, query := range queries {
8471
log.Infof("Searching customers in Stripe with query: %q", query)
85-
params := &stripe.CustomerSearchParams{
86-
SearchParams: stripe.SearchParams{
87-
Query: query,
88-
Expand: []*string{stripe.String("data.subscriptions")},
89-
Context: ctx,
90-
},
72+
73+
customers, err := c.findCustomers(ctx, query)
74+
if err != nil {
75+
return fmt.Errorf("failed to udpate usage: %w", err)
9176
}
92-
iter := c.sc.Customers.Search(params)
93-
for iter.Next() {
94-
customer := iter.Customer()
77+
78+
for _, customer := range customers {
9579
teamID := customer.Metadata["teamId"]
9680
log.Infof("Found customer %q for teamId %q", customer.Name, teamID)
9781

@@ -112,6 +96,27 @@ func (c *Client) UpdateUsage(ctx context.Context, creditsPerTeam map[string]int6
11296
return nil
11397
}
11498

99+
func (c *Client) findCustomers(ctx context.Context, query string) ([]*stripe.Customer, error) {
100+
params := &stripe.CustomerSearchParams{
101+
SearchParams: stripe.SearchParams{
102+
Query: query,
103+
Expand: []*string{stripe.String("data.subscriptions")},
104+
Context: ctx,
105+
},
106+
}
107+
iter := c.sc.Customers.Search(params)
108+
if iter.Err() != nil {
109+
return nil, fmt.Errorf("failed to search for customers: %w", iter.Err())
110+
}
111+
112+
var customers []*stripe.Customer
113+
for iter.Next() {
114+
customers = append(customers, iter.Customer())
115+
}
116+
117+
return customers, nil
118+
}
119+
115120
func (c *Client) updateUsageForCustomer(ctx context.Context, customer *stripe.Customer, credits int64) (*UsageRecord, error) {
116121
subscriptions := customer.Subscriptions.Data
117122
if len(subscriptions) != 1 {
@@ -143,37 +148,55 @@ func (c *Client) updateUsageForCustomer(ctx context.Context, customer *stripe.Cu
143148
}, nil
144149
}
145150

146-
// GetUpcomingInvoice fetches the upcoming invoice for the given team or user id.
147-
func (c *Client) GetUpcomingInvoice(ctx context.Context, kind CustomerKind, id string) (*StripeInvoice, error) {
148-
log.Infof("Fetching upcoming invoice of customer. (%q %q)", kind, id)
149-
query := fmt.Sprintf("metadata['%sId']:'%s'", kind, id)
150-
searchParams := &stripe.CustomerSearchParams{
151-
SearchParams: stripe.SearchParams{
152-
Query: query,
153-
Expand: []*string{stripe.String("data.subscriptions")},
154-
Context: ctx,
155-
},
151+
func (c *Client) GetCustomerByTeamID(ctx context.Context, teamID string) (*stripe.Customer, error) {
152+
customers, err := c.findCustomers(ctx, fmt.Sprintf("metadata['teamId']:'%s'", teamID))
153+
if err != nil {
154+
return nil, fmt.Errorf("failed to find customers: %w", err)
156155
}
157-
iter := c.sc.Customers.Search(searchParams)
158-
if iter.Err() != nil || !iter.Next() {
159-
return nil, ErrorCustomerNotFound
156+
157+
if len(customers) == 0 {
158+
return nil, fmt.Errorf("no team customer found for id: %s", teamID)
160159
}
161-
customer := iter.Customer()
162-
if iter.Next() {
163-
return nil, fmt.Errorf("found more than one customer for query %s", query)
160+
if len(customers) > 1 {
161+
return nil, fmt.Errorf("found multiple team customers for id: %s", teamID)
162+
}
163+
164+
return customers[0], nil
165+
}
166+
167+
func (c *Client) GetCustomerByUserID(ctx context.Context, userID string) (*stripe.Customer, error) {
168+
customers, err := c.findCustomers(ctx, fmt.Sprintf("metadata['userId']:'%s'", userID))
169+
if err != nil {
170+
return nil, fmt.Errorf("failed to find customers: %w", err)
164171
}
172+
173+
if len(customers) == 0 {
174+
return nil, fmt.Errorf("no user customer found for id: %s", userID)
175+
}
176+
if len(customers) > 1 {
177+
return nil, fmt.Errorf("found multiple user customers for id: %s", userID)
178+
}
179+
180+
return customers[0], nil
181+
}
182+
183+
// GetUpcomingInvoice fetches the upcoming invoice for the given team or user id.
184+
func (c *Client) GetUpcomingInvoice(ctx context.Context, customerID string) (*Invoice, error) {
165185
invoiceParams := &stripe.InvoiceParams{
166-
Customer: stripe.String(customer.ID),
186+
Params: stripe.Params{
187+
Context: ctx,
188+
},
189+
Customer: stripe.String(customerID),
167190
}
168191
invoice, err := c.sc.Invoices.GetNext(invoiceParams)
169192
if err != nil {
170-
return nil, fmt.Errorf("failed to fetch the upcoming invoice for customer %s", customer.ID)
193+
return nil, fmt.Errorf("failed to fetch the upcoming invoice for customer %s", customerID)
171194
}
172195
if len(invoice.Lines.Data) < 1 {
173-
return nil, fmt.Errorf("no line items on invoice %s", invoice.ID)
196+
return nil, fmt.Errorf("no line items on invoice %s for customer %s", invoice.ID, customerID)
174197
}
175198

176-
return &StripeInvoice{
199+
return &Invoice{
177200
ID: invoice.ID,
178201
SubscriptionID: invoice.Subscription.ID,
179202
Amount: invoice.AmountRemaining,

0 commit comments

Comments
 (0)