Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/usage/pkg/apiv1/billing.go
2499 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package apiv1
6
7
import (
8
"context"
9
"errors"
10
"fmt"
11
12
"math"
13
"time"
14
15
"github.com/bufbuild/connect-go"
16
"github.com/gitpod-io/gitpod/common-go/log"
17
db "github.com/gitpod-io/gitpod/components/gitpod-db/go"
18
experimental_v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
19
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
20
v1 "github.com/gitpod-io/gitpod/usage-api/v1"
21
"github.com/gitpod-io/gitpod/usage/pkg/stripe"
22
"github.com/google/uuid"
23
stripe_api "github.com/stripe/stripe-go/v72"
24
"google.golang.org/grpc/codes"
25
"google.golang.org/grpc/status"
26
"gorm.io/gorm"
27
)
28
29
func NewBillingService(stripeClient *stripe.Client, conn *gorm.DB, ccManager *db.CostCenterManager, stripePrices stripe.StripePrices, teamsService v1connect.TeamsServiceClient, userService v1connect.UserServiceClient) *BillingService {
30
return &BillingService{
31
stripeClient: stripeClient,
32
conn: conn,
33
ccManager: ccManager,
34
stripePrices: stripePrices,
35
36
teamsService: teamsService,
37
userService: userService,
38
}
39
}
40
41
type BillingService struct {
42
conn *gorm.DB
43
stripeClient *stripe.Client
44
ccManager *db.CostCenterManager
45
stripePrices stripe.StripePrices
46
47
teamsService v1connect.TeamsServiceClient
48
userService v1connect.UserServiceClient
49
50
v1.UnimplementedBillingServiceServer
51
}
52
53
func (s *BillingService) GetStripeCustomer(ctx context.Context, req *v1.GetStripeCustomerRequest) (*v1.GetStripeCustomerResponse, error) {
54
55
storeStripeCustomerAndRespond := func(ctx context.Context, cus *stripe_api.Customer, attributionID db.AttributionID) (*v1.GetStripeCustomerResponse, error) {
56
logger := log.WithField("stripe_customer_id", cus.ID).WithField("attribution_id", attributionID)
57
// Store it in the DB such that subsequent lookups don't need to go to Stripe
58
result, err := s.storeStripeCustomer(ctx, cus, attributionID)
59
if err != nil {
60
logger.WithError(err).Error("Failed to store stripe customer in the database.")
61
62
// Storing failed, but we don't want to block the caller since we do have the data, return it as a success
63
return &v1.GetStripeCustomerResponse{
64
Customer: convertStripeCustomer(cus),
65
}, nil
66
}
67
68
return &v1.GetStripeCustomerResponse{
69
Customer: result,
70
}, nil
71
}
72
73
updateStripeCustomer := func(stripeCustomerID string) (db.StripeCustomer, error) {
74
stripeCustomer, err := s.stripeClient.GetCustomer(ctx, stripeCustomerID)
75
if err != nil {
76
return db.StripeCustomer{}, err
77
}
78
79
customer, err := db.UpdateStripeCustomerInvalidBillingAddress(ctx, s.conn, stripeCustomer.ID, stripeCustomer.Tax.AutomaticTax == stripe_api.CustomerTaxAutomaticTaxUnrecognizedLocation)
80
if err != nil {
81
return db.StripeCustomer{}, err
82
}
83
84
return customer, nil
85
}
86
87
switch identifier := req.GetIdentifier().(type) {
88
case *v1.GetStripeCustomerRequest_AttributionId:
89
attributionID, err := db.ParseAttributionID(identifier.AttributionId)
90
if err != nil {
91
return nil, status.Errorf(codes.InvalidArgument, "Invalid attribution ID %s", attributionID)
92
}
93
94
logger := log.WithField("attribution_id", attributionID)
95
96
customer, err := db.GetStripeCustomerByAttributionID(ctx, s.conn, attributionID)
97
if err != nil {
98
// We don't yet have it in the DB
99
if errors.Is(err, db.ErrorNotFound) {
100
stripeCustomer, err := s.stripeClient.GetCustomerByAttributionID(ctx, string(attributionID))
101
if err != nil {
102
return nil, err
103
}
104
105
return storeStripeCustomerAndRespond(ctx, stripeCustomer, attributionID)
106
}
107
108
logger.WithError(err).Error("Failed to lookup stripe customer from DB")
109
110
return nil, status.Errorf(codes.NotFound, "Failed to lookup stripe customer from DB: %s", err.Error())
111
} else if customer.InvalidBillingAddress == nil {
112
// Update field for old entries in db
113
customer, err = updateStripeCustomer(customer.StripeCustomerID)
114
if err != nil {
115
logger.WithError(err).Error("Failed to update stripe customer from DB")
116
}
117
}
118
119
return &v1.GetStripeCustomerResponse{
120
Customer: convertDBStripeCustomerToResponse(customer),
121
}, nil
122
123
case *v1.GetStripeCustomerRequest_StripeCustomerId:
124
if identifier.StripeCustomerId == "" {
125
return nil, status.Errorf(codes.InvalidArgument, "empty stripe customer ID supplied")
126
}
127
128
logger := log.WithField("stripe_customer_id", identifier.StripeCustomerId)
129
130
customer, err := db.GetStripeCustomer(ctx, s.conn, identifier.StripeCustomerId)
131
if err != nil {
132
// We don't yet have it in the DB
133
if errors.Is(err, db.ErrorNotFound) {
134
stripeCustomer, err := s.stripeClient.GetCustomer(ctx, identifier.StripeCustomerId)
135
if err != nil {
136
return nil, err
137
}
138
139
attributionID, err := stripe.GetAttributionID(ctx, stripeCustomer)
140
if err != nil {
141
return nil, status.Errorf(codes.Internal, "Failed to parse attribution ID from Stripe customer %s", stripeCustomer.ID)
142
}
143
144
return storeStripeCustomerAndRespond(ctx, stripeCustomer, attributionID)
145
}
146
147
logger.WithError(err).Error("Failed to lookup stripe customer from DB")
148
149
return nil, status.Errorf(codes.NotFound, "Failed to lookup stripe customer from DB: %s", err.Error())
150
} else if customer.InvalidBillingAddress == nil {
151
// Update field for old entries in db
152
customer, err = updateStripeCustomer(customer.StripeCustomerID)
153
if err != nil {
154
logger.WithError(err).Error("Failed to update stripe customer from DB")
155
}
156
}
157
158
return &v1.GetStripeCustomerResponse{
159
Customer: convertDBStripeCustomerToResponse(customer),
160
}, nil
161
162
default:
163
return nil, status.Errorf(codes.InvalidArgument, "Unknown identifier")
164
}
165
}
166
167
func (s *BillingService) CreateStripeCustomer(ctx context.Context, req *v1.CreateStripeCustomerRequest) (*v1.CreateStripeCustomerResponse, error) {
168
attributionID, err := db.ParseAttributionID(req.GetAttributionId())
169
if err != nil {
170
return nil, status.Errorf(codes.InvalidArgument, "Invalid attribution ID %s", attributionID)
171
}
172
if req.GetCurrency() == "" {
173
return nil, status.Error(codes.InvalidArgument, "Invalid currency specified")
174
}
175
if req.GetEmail() == "" {
176
return nil, status.Error(codes.InvalidArgument, "Invalid email specified")
177
}
178
if req.GetName() == "" {
179
return nil, status.Error(codes.InvalidArgument, "Invalid name specified")
180
}
181
182
customer, err := s.stripeClient.CreateCustomer(ctx, stripe.CreateCustomerParams{
183
AttributionID: string(attributionID),
184
Currency: req.GetCurrency(),
185
Email: req.GetEmail(),
186
Name: req.GetName(),
187
BillingCreatorUserID: req.GetBillingCreatorUserId(),
188
})
189
if err != nil {
190
log.WithError(err).Errorf("Failed to create stripe customer.")
191
return nil, status.Errorf(codes.Internal, "Failed to create stripe customer")
192
}
193
194
err = db.CreateStripeCustomer(ctx, s.conn, db.StripeCustomer{
195
StripeCustomerID: customer.ID,
196
AttributionID: attributionID,
197
CreationTime: db.NewVarCharTime(time.Unix(customer.Created, 0)),
198
Currency: req.GetCurrency(),
199
InvalidBillingAddress: db.BoolPointer(true), // true as address is empty
200
})
201
if err != nil {
202
log.WithField("attribution_id", attributionID).WithField("stripe_customer_id", customer.ID).WithError(err).Error("Failed to store Stripe Customer in the database.")
203
// We do not return an error to the caller here, as we did manage to create the stripe customer in Stripe and we can proceed with other flows
204
// The StripeCustomer will be backfilled in the DB on the next GetStripeCustomer call by doing a search.
205
}
206
207
return &v1.CreateStripeCustomerResponse{
208
Customer: convertStripeCustomer(customer),
209
}, nil
210
}
211
212
func (s *BillingService) CreateHoldPaymentIntent(ctx context.Context, req *v1.CreateHoldPaymentIntentRequest) (*v1.CreateHoldPaymentIntentResponse, error) {
213
attributionID, err := db.ParseAttributionID(req.GetAttributionId())
214
if err != nil {
215
return nil, status.Errorf(codes.InvalidArgument, "Invalid attribution ID %s", attributionID)
216
}
217
218
customer, err := s.GetStripeCustomer(ctx, &v1.GetStripeCustomerRequest{
219
Identifier: &v1.GetStripeCustomerRequest_AttributionId{
220
AttributionId: string(attributionID),
221
},
222
})
223
if err != nil {
224
log.WithError(err).Errorf("Failed to find stripe customer.")
225
return nil, status.Errorf(codes.Internal, "Failed to find stripe customer")
226
}
227
stripeCustomer, err := s.stripeClient.GetCustomer(ctx, customer.Customer.Id)
228
if err != nil {
229
log.WithError(err).Errorf("Failed to get customer from stripe.")
230
return nil, status.Errorf(codes.Internal, "Failed to get customer from stripe.")
231
}
232
233
// Create a payment intent for 1.00 to test the card with a hold
234
holdPaymentIntent, err := s.stripeClient.CreateHoldPaymentIntent(ctx, stripeCustomer, 100)
235
if err != nil {
236
log.WithError(err).Errorf("Failed to create a payment intent to for playing a hold.")
237
return nil, status.Errorf(codes.Internal, "Failed to create a payment intent to for playing a hold.")
238
}
239
240
// This gets passed to the client where it can be confirmed/verified by the user
241
return &v1.CreateHoldPaymentIntentResponse{
242
PaymentIntentId: holdPaymentIntent.ID,
243
PaymentIntentClientSecret: holdPaymentIntent.ClientSecret,
244
}, nil
245
}
246
247
func (s *BillingService) CreateStripeSubscription(ctx context.Context, req *v1.CreateStripeSubscriptionRequest) (*v1.CreateStripeSubscriptionResponse, error) {
248
attributionID, err := db.ParseAttributionID(req.GetAttributionId())
249
if err != nil {
250
return nil, status.Errorf(codes.InvalidArgument, "Invalid attribution ID %s", attributionID)
251
}
252
253
paymentIntentID := req.PaymentIntentId
254
if paymentIntentID == "" {
255
return nil, status.Errorf(codes.InvalidArgument, "Invalid payment_intent_id")
256
}
257
258
customer, err := s.GetStripeCustomer(ctx, &v1.GetStripeCustomerRequest{
259
Identifier: &v1.GetStripeCustomerRequest_AttributionId{
260
AttributionId: string(attributionID),
261
},
262
})
263
if err != nil {
264
return nil, err
265
}
266
267
stripeCustomer, err := s.stripeClient.GetCustomer(ctx, customer.Customer.Id)
268
if err != nil {
269
return nil, err
270
}
271
// if the customer has a subscription, return error
272
for _, subscription := range stripeCustomer.Subscriptions.Data {
273
if subscription.Status != "canceled" {
274
return nil, status.Errorf(codes.AlreadyExists, "Customer (%s) already has an active subscription (%s)", attributionID, subscription.ID)
275
}
276
}
277
278
priceID, err := getPriceIdentifier(attributionID, stripeCustomer, s)
279
if err != nil {
280
return nil, err
281
}
282
283
// Handle a payment hold for the payment intent
284
// Make sure the provided payment intent hold was successful, and release it
285
result, err := s.stripeClient.TryHoldAmountForPaymentIntent(ctx, stripeCustomer, string(paymentIntentID))
286
if err != nil {
287
log.WithError(err).Errorf("Failed to verify credit card for customer %s", stripeCustomer.ID)
288
}
289
if result != stripe.PaymentHoldResultSucceeded {
290
log.Errorf("Failed to verify credit card for customer %s. Result: %s", stripeCustomer.ID, result)
291
return nil, status.Error(codes.InvalidArgument, "The provided payment method is invalid. Please provide working credit card information to proceed.")
292
}
293
294
// At this point we should have a valid payment method, so we can set it as default.
295
_, err = s.stripeClient.SetDefaultPaymentForCustomer(ctx, customer.Customer.Id, string(paymentIntentID))
296
if err != nil {
297
return nil, status.Errorf(codes.InvalidArgument, "Failed to set default payment for customer ID %s", customer.Customer.Id)
298
}
299
300
// Customers are created without a valid address by default,
301
// `SetDefaultPaymentForCustomer` will update the customer address to the one provided in the payment modal.
302
// Request the customer data again so the `Tax` property is properly set.
303
stripeCustomer, err = s.stripeClient.GetCustomer(ctx, customer.Customer.Id)
304
if err != nil {
305
return nil, err
306
}
307
308
var isAutomaticTaxSupported bool
309
if stripeCustomer.Tax != nil {
310
isAutomaticTaxSupported = stripeCustomer.Tax.AutomaticTax == stripe_api.CustomerTaxAutomaticTaxSupported
311
}
312
if !isAutomaticTaxSupported {
313
log.Warnf("Automatic Stripe tax is not supported for customer %s", stripeCustomer.ID)
314
}
315
316
subscription, err := s.stripeClient.CreateSubscription(ctx, stripeCustomer.ID, priceID, isAutomaticTaxSupported)
317
if err != nil {
318
return nil, status.Errorf(codes.Internal, "Failed to create subscription with customer ID %s", customer.Customer.Id)
319
}
320
321
return &v1.CreateStripeSubscriptionResponse{
322
Subscription: &v1.StripeSubscription{
323
Id: subscription.ID,
324
},
325
}, nil
326
}
327
328
func (s *BillingService) UpdateCustomerSubscriptionsTaxState(ctx context.Context, req *v1.UpdateCustomerSubscriptionsTaxStateRequest) (*v1.UpdateCustomerSubscriptionsTaxStateResponse, error) {
329
if req.GetCustomerId() == "" {
330
return nil, status.Errorf(codes.InvalidArgument, "Missing CustomerID")
331
}
332
333
stripeCustomer, err := s.stripeClient.GetCustomer(ctx, req.GetCustomerId())
334
if err != nil {
335
log.WithError(err).Error("Failed to retrieve customer from Stripe.")
336
return nil, status.Errorf(codes.NotFound, "Failed to get customer with ID %s: %s", req.GetCustomerId(), err.Error())
337
}
338
339
var isAutomaticTaxSupported bool
340
if stripeCustomer.Tax != nil {
341
isAutomaticTaxSupported = stripeCustomer.Tax.AutomaticTax == stripe_api.CustomerTaxAutomaticTaxSupported
342
343
_, err = db.UpdateStripeCustomerInvalidBillingAddress(ctx, s.conn, stripeCustomer.ID, stripeCustomer.Tax.AutomaticTax == stripe_api.CustomerTaxAutomaticTaxUnrecognizedLocation)
344
if err != nil {
345
log.WithError(err).Error("Failed to update stripe customer from DB")
346
}
347
}
348
if !isAutomaticTaxSupported {
349
log.Warnf("Automatic Stripe tax is not supported for customer %s", stripeCustomer.ID)
350
}
351
352
for _, subscription := range stripeCustomer.Subscriptions.Data {
353
if subscription.Status != "canceled" && subscription.AutomaticTax.Enabled != isAutomaticTaxSupported {
354
_, err := s.stripeClient.UpdateSubscriptionAutomaticTax(ctx, subscription.ID, isAutomaticTaxSupported)
355
if err != nil {
356
log.WithError(err).Errorf("Failed to update subscription automaticTax with ID %s", subscription.ID)
357
} else {
358
log.Infof("Updated subscription automatic tax supported with ID %s", subscription.ID)
359
}
360
}
361
}
362
363
return &v1.UpdateCustomerSubscriptionsTaxStateResponse{}, nil
364
}
365
366
func getPriceIdentifier(attributionID db.AttributionID, stripeCustomer *stripe_api.Customer, s *BillingService) (string, error) {
367
preferredCurrency := stripeCustomer.Metadata["preferredCurrency"]
368
if stripeCustomer.Metadata["preferredCurrency"] == "" {
369
log.
370
WithField("stripe_customer_id", stripeCustomer.ID).
371
Warn("No preferred currency set. Defaulting to USD")
372
}
373
374
switch preferredCurrency {
375
case "EUR":
376
return s.stripePrices.TeamUsagePriceIDs.EUR, nil
377
default:
378
return s.stripePrices.TeamUsagePriceIDs.USD, nil
379
}
380
}
381
382
func (s *BillingService) ReconcileInvoices(ctx context.Context, in *v1.ReconcileInvoicesRequest) (*v1.ReconcileInvoicesResponse, error) {
383
balances, err := db.ListBalance(ctx, s.conn)
384
if err != nil {
385
log.WithError(err).Errorf("Failed to reconcile invoices.")
386
return nil, status.Errorf(codes.Internal, "Failed to reconcile invoices.")
387
}
388
389
stripeBalances, err := balancesForStripeCostCenters(ctx, s.ccManager, balances)
390
if err != nil {
391
return nil, status.Errorf(codes.Internal, "Failed to identify stripe balances.")
392
}
393
394
creditCentsByAttribution := map[db.AttributionID]int64{}
395
for _, balance := range stripeBalances {
396
creditCentsByAttribution[balance.AttributionID] = int64(math.Ceil(balance.CreditCents.ToCredits()))
397
}
398
399
err = s.stripeClient.UpdateUsage(ctx, creditCentsByAttribution)
400
if err != nil {
401
log.WithError(err).Errorf("Failed to udpate usage in stripe.")
402
return nil, status.Errorf(codes.Internal, "Failed to update usage in stripe")
403
}
404
err = s.ReconcileStripeCustomers(ctx)
405
if err != nil {
406
log.WithError(err).Errorf("Failed to reconcile stripe customers.")
407
}
408
409
return &v1.ReconcileInvoicesResponse{}, nil
410
}
411
412
func (s *BillingService) ReconcileStripeCustomers(ctx context.Context) error {
413
log.Info("Reconciling stripe customers")
414
var costCenters []db.CostCenter
415
result := s.conn.Raw("SELECT * from d_b_cost_center where creationTime in (SELECT max(creationTime) from d_b_cost_center group by id) and nextBillingTime < creationTime and billingStrategy='stripe'").Scan(&costCenters)
416
if result.Error != nil {
417
return result.Error
418
}
419
420
log.Infof("Found %d cost centers to reconcile", len(costCenters))
421
422
for _, costCenter := range costCenters {
423
log.Infof("Reconciling stripe invoices for cost center %s", costCenter.ID)
424
err := s.reconcileStripeInvoices(ctx, costCenter.ID)
425
if err != nil {
426
log.WithError(err).Errorf("Failed to reconcile stripe invoices for cost center %s", costCenter.ID)
427
continue
428
}
429
_, err = s.ccManager.IncrementBillingCycle(ctx, costCenter.ID)
430
if err != nil {
431
// we are just logging at this point, so that we don't see the event again as the usage has been recorded.
432
log.WithError(err).Errorf("Failed to increment billing cycle.")
433
}
434
}
435
return nil
436
}
437
438
func (s *BillingService) reconcileStripeInvoices(ctx context.Context, id db.AttributionID) error {
439
cust, err := s.stripeClient.GetCustomerByAttributionID(ctx, string(id))
440
if err != nil {
441
return err
442
}
443
invoices, err := s.stripeClient.ListInvoices(ctx, cust.ID)
444
if err != nil {
445
return err
446
}
447
for _, invoice := range invoices {
448
if invoice.Status == "paid" {
449
usage, err := InternalComputeInvoiceUsage(ctx, invoice, cust)
450
if err != nil {
451
return err
452
}
453
// check if a usage entry exists for this invoice
454
var existingUsage db.Usage
455
result := s.conn.First(&existingUsage, "description = ?", usage.Description)
456
if result.Error != nil {
457
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
458
log.Infof("No usage entry found for invoice %s. Inserting one now.", invoice.ID)
459
err = db.InsertUsage(ctx, s.conn, usage)
460
if err != nil {
461
return err
462
}
463
} else {
464
return result.Error
465
}
466
}
467
}
468
}
469
return nil
470
}
471
472
func (s *BillingService) FinalizeInvoice(ctx context.Context, in *v1.FinalizeInvoiceRequest) (*v1.FinalizeInvoiceResponse, error) {
473
logger := log.WithField("invoice_id", in.GetInvoiceId())
474
logger.Info("Invoice finalized. Recording usage.")
475
476
if in.GetInvoiceId() == "" {
477
return nil, status.Errorf(codes.InvalidArgument, "Missing InvoiceID")
478
}
479
480
invoice, err := s.stripeClient.GetInvoiceWithCustomer(ctx, in.GetInvoiceId())
481
if err != nil {
482
logger.WithError(err).Error("Failed to retrieve invoice from Stripe.")
483
return nil, status.Errorf(codes.NotFound, "Failed to get invoice with ID %s: %s", in.GetInvoiceId(), err.Error())
484
}
485
usage, err := InternalComputeInvoiceUsage(ctx, invoice, invoice.Customer)
486
if err != nil {
487
return nil, err
488
}
489
err = db.InsertUsage(ctx, s.conn, usage)
490
if err != nil {
491
logger.WithError(err).Errorf("Failed to insert Invoice usage record into the db.")
492
return nil, status.Errorf(codes.Internal, "Failed to insert Invoice into usage records.")
493
}
494
logger.WithField("usage_id", usage.ID).Infof("Inserted usage record into database for %f credits against %s attribution", usage.CreditCents.ToCredits(), usage.AttributionID)
495
496
_, err = s.ccManager.IncrementBillingCycle(ctx, usage.AttributionID)
497
if err != nil {
498
// we are just logging at this point, so that we don't see the event again as the usage has been recorded.
499
logger.WithError(err).Errorf("Failed to increment billing cycle.")
500
}
501
502
// update stripe with current usage immediately, so that invoices created between now and the next reconcile are correct.
503
newBalance, err := db.GetBalance(ctx, s.conn, usage.AttributionID)
504
if err != nil {
505
// we are just logging at this point, so that we don't see the event again as the usage has been recorded.
506
logger.WithError(err).Errorf("Failed to compute new balance.")
507
return &v1.FinalizeInvoiceResponse{}, nil
508
}
509
err = s.stripeClient.UpdateUsage(ctx, map[db.AttributionID]int64{
510
usage.AttributionID: int64(math.Ceil(newBalance.ToCredits())),
511
})
512
if err != nil {
513
// we are just logging at this point, so that we don't see the event again as the usage has been recorded.
514
log.WithError(err).Errorf("Failed to udpate usage in stripe after receiving invoive.finalized.")
515
}
516
return &v1.FinalizeInvoiceResponse{}, nil
517
}
518
519
func InternalComputeInvoiceUsage(ctx context.Context, invoice *stripe_api.Invoice, customer *stripe_api.Customer) (db.Usage, error) {
520
logger := log.WithField("invoice_id", invoice.ID)
521
attributionID, err := stripe.GetAttributionID(ctx, customer)
522
if err != nil {
523
return db.Usage{}, err
524
}
525
logger = logger.WithField("attributionID", attributionID)
526
finalizedAt := time.Unix(invoice.StatusTransitions.FinalizedAt, 0)
527
528
logger = logger.
529
WithField("attribution_id", attributionID).
530
WithField("invoice_finalized_at", finalizedAt)
531
532
if invoice.Lines == nil || len(invoice.Lines.Data) == 0 {
533
logger.Errorf("Invoice %s did not contain any lines so we cannot extract quantity to reflect it in usage.", invoice.ID)
534
return db.Usage{}, status.Errorf(codes.Internal, "Invoice did not contain any lines.")
535
}
536
537
lines := invoice.Lines.Data
538
var creditsOnInvoice int64
539
for _, line := range lines {
540
creditsOnInvoice += line.Quantity
541
}
542
543
return db.Usage{
544
ID: uuid.New(),
545
AttributionID: attributionID,
546
Description: fmt.Sprintf("Invoice %s finalized in Stripe", invoice.ID),
547
// Apply negative value of credits to reduce accrued credit usage
548
CreditCents: db.NewCreditCents(float64(-creditsOnInvoice)),
549
EffectiveTime: db.NewVarCharTime(finalizedAt),
550
Kind: db.InvoiceUsageKind,
551
Draft: false,
552
Metadata: nil,
553
}, nil
554
}
555
556
func (s *BillingService) CancelSubscription(ctx context.Context, in *v1.CancelSubscriptionRequest) (*v1.CancelSubscriptionResponse, error) {
557
logger := log.WithField("subscription_id", in.GetSubscriptionId())
558
logger.Infof("Subscription ended. Setting cost center back to free.")
559
if in.GetSubscriptionId() == "" {
560
return nil, status.Errorf(codes.InvalidArgument, "subscriptionId is required")
561
}
562
563
subscription, err := s.stripeClient.GetSubscriptionWithCustomer(ctx, in.GetSubscriptionId())
564
if err != nil {
565
return nil, err
566
}
567
568
attributionID, err := stripe.GetAttributionID(ctx, subscription.Customer)
569
if err != nil {
570
return nil, err
571
}
572
573
costCenter, err := s.ccManager.GetOrCreateCostCenter(ctx, attributionID)
574
if err != nil {
575
return nil, err
576
}
577
578
costCenter.BillingStrategy = db.CostCenter_Other
579
_, err = s.ccManager.UpdateCostCenter(ctx, costCenter)
580
if err != nil {
581
return nil, err
582
}
583
return &v1.CancelSubscriptionResponse{}, nil
584
}
585
586
func (s *BillingService) OnChargeDispute(ctx context.Context, req *v1.OnChargeDisputeRequest) (*v1.OnChargeDisputeResponse, error) {
587
if req.DisputeId == "" {
588
return nil, status.Errorf(codes.InvalidArgument, "dispute ID is required")
589
}
590
591
logger := log.WithContext(ctx).WithField("disputeId", req.DisputeId)
592
593
dispute, err := s.stripeClient.GetDispute(ctx, req.DisputeId)
594
if err != nil {
595
return nil, status.Errorf(codes.Internal, "failed to retrieve dispute ID %s from stripe", req.DisputeId)
596
}
597
598
if dispute.PaymentIntent == nil || dispute.PaymentIntent.Customer == nil {
599
return nil, status.Errorf(codes.Internal, "dispute did not contain customer of payment intent in expanded fields")
600
}
601
602
customer := dispute.PaymentIntent.Customer
603
logger = logger.WithField("customerId", customer.ID)
604
605
attributionIDValue, ok := customer.Metadata[stripe.AttributionIDMetadataKey]
606
if !ok {
607
return nil, status.Errorf(codes.Internal, "Customer %s object did not contain attribution ID in metadata", customer.ID)
608
}
609
610
logger = logger.WithField("attributionId", attributionIDValue)
611
612
attributionID, err := db.ParseAttributionID(attributionIDValue)
613
if err != nil {
614
log.WithError(err).Errorf("Failed to parse attribution ID from customer metadata.")
615
return nil, status.Errorf(codes.Internal, "failed to parse attribution ID from customer metadata")
616
}
617
618
var userIDsToBlock []string
619
_, id := attributionID.Values()
620
team, err := s.teamsService.GetTeam(ctx, connect.NewRequest(&experimental_v1.GetTeamRequest{
621
TeamId: id,
622
}))
623
if err != nil {
624
return nil, status.Errorf(codes.Internal, "failed to lookup team details for team ID: %s", id)
625
}
626
627
for _, member := range team.Msg.GetTeam().GetMembers() {
628
if member.GetRole() != experimental_v1.TeamRole_TEAM_ROLE_OWNER {
629
continue
630
}
631
userIDsToBlock = append(userIDsToBlock, member.GetUserId())
632
}
633
634
logger = logger.WithField("teamOwners", userIDsToBlock)
635
636
logger.Infof("Identified %d users to block based on charge dispute", len(userIDsToBlock))
637
var errs []error
638
for _, userToBlock := range userIDsToBlock {
639
_, err := s.userService.BlockUser(ctx, connect.NewRequest(&experimental_v1.BlockUserRequest{
640
UserId: userToBlock,
641
Reason: fmt.Sprintf("User has created a Stripe dispute ID: %s", req.GetDisputeId()),
642
}))
643
if err != nil {
644
errs = append(errs, fmt.Errorf("failed to block user %s: %w", userToBlock, err))
645
}
646
}
647
648
if len(errs) > 0 {
649
return nil, status.Errorf(codes.Internal, "failed to block users: %v", errs)
650
}
651
652
return &v1.OnChargeDisputeResponse{}, nil
653
}
654
655
func (s *BillingService) getPriceId(ctx context.Context, attributionId string) string {
656
defaultPriceId := s.stripePrices.TeamUsagePriceIDs.USD
657
attributionID, err := db.ParseAttributionID(attributionId)
658
if err != nil {
659
log.Errorf("Failed to parse attribution ID %s: %s", attributionId, err.Error())
660
return defaultPriceId
661
}
662
663
customer, err := s.GetStripeCustomer(ctx, &v1.GetStripeCustomerRequest{
664
Identifier: &v1.GetStripeCustomerRequest_AttributionId{
665
AttributionId: string(attributionID),
666
},
667
})
668
if err != nil {
669
if status.Code(err) != codes.NotFound {
670
log.Errorf("Failed to get stripe customer for attribution ID %s: %s", attributionId, err.Error())
671
}
672
return defaultPriceId
673
}
674
675
stripeCustomer, err := s.stripeClient.GetCustomer(ctx, customer.Customer.Id)
676
if err != nil {
677
log.Errorf("Failed to get customer infromation from stripe for customer ID %s: %s", customer.Customer.Id, err.Error())
678
return defaultPriceId
679
}
680
681
// if the customer has an active subscription, return that information
682
for _, subscription := range stripeCustomer.Subscriptions.Data {
683
if subscription.Status != "canceled" {
684
return subscription.Plan.ID
685
}
686
}
687
priceID, err := getPriceIdentifier(attributionID, stripeCustomer, s)
688
if err != nil {
689
log.Errorf("Failed to get price identifier for attribution ID %s: %s", attributionId, err.Error())
690
return defaultPriceId
691
}
692
return priceID
693
}
694
695
func (s *BillingService) GetPriceInformation(ctx context.Context, req *v1.GetPriceInformationRequest) (*v1.GetPriceInformationResponse, error) {
696
_, err := db.ParseAttributionID(req.GetAttributionId())
697
if err != nil {
698
return nil, status.Errorf(codes.InvalidArgument, "Invalid attribution ID %s", req.GetAttributionId())
699
}
700
priceID := s.getPriceId(ctx, req.GetAttributionId())
701
price, err := s.stripeClient.GetPriceInformation(ctx, priceID)
702
if err != nil {
703
return nil, err
704
}
705
information := price.Metadata["human_readable_description"]
706
if information == "" {
707
information = "No information available"
708
}
709
return &v1.GetPriceInformationResponse{
710
HumanReadableDescription: information,
711
}, nil
712
}
713
714
func (s *BillingService) storeStripeCustomer(ctx context.Context, cus *stripe_api.Customer, attributionID db.AttributionID) (*v1.StripeCustomer, error) {
715
var invalidBillingAddress *bool
716
if cus.Tax != nil {
717
invalidBillingAddress = db.BoolPointer(cus.Tax.AutomaticTax == stripe_api.CustomerTaxAutomaticTaxUnrecognizedLocation)
718
}
719
720
err := db.CreateStripeCustomer(ctx, s.conn, db.StripeCustomer{
721
StripeCustomerID: cus.ID,
722
AttributionID: attributionID,
723
Currency: cus.Metadata[stripe.PreferredCurrencyMetadataKey],
724
InvalidBillingAddress: invalidBillingAddress,
725
// We use the original Stripe supplied creation timestamp, this ensures that we stay true to our ordering of customer creation records.
726
CreationTime: db.NewVarCharTime(time.Unix(cus.Created, 0)),
727
})
728
if err != nil {
729
return nil, err
730
}
731
732
return convertStripeCustomer(cus), nil
733
}
734
735
func balancesForStripeCostCenters(ctx context.Context, cm *db.CostCenterManager, balances []db.Balance) ([]db.Balance, error) {
736
var result []db.Balance
737
for _, balance := range balances {
738
// filter out balances for non-stripe attribution IDs
739
costCenter, err := cm.GetOrCreateCostCenter(ctx, balance.AttributionID)
740
if err != nil {
741
return nil, err
742
}
743
744
// We only update Stripe usage when the AttributionID is billed against Stripe (determined through CostCenter)
745
if costCenter.BillingStrategy != db.CostCenter_Stripe {
746
continue
747
}
748
749
result = append(result, balance)
750
}
751
752
return result, nil
753
}
754
755
func convertStripeCustomer(customer *stripe_api.Customer) *v1.StripeCustomer {
756
var invalidBillingAddress bool
757
if customer.Tax != nil {
758
invalidBillingAddress = customer.Tax.AutomaticTax == stripe_api.CustomerTaxAutomaticTaxUnrecognizedLocation
759
}
760
return &v1.StripeCustomer{
761
Id: customer.ID,
762
Currency: customer.Metadata[stripe.PreferredCurrencyMetadataKey],
763
InvalidBillingAddress: invalidBillingAddress,
764
}
765
}
766
767
func convertDBStripeCustomerToResponse(cus db.StripeCustomer) *v1.StripeCustomer {
768
var invalidBillingAddress bool
769
if cus.InvalidBillingAddress != nil {
770
invalidBillingAddress = *cus.InvalidBillingAddress
771
}
772
return &v1.StripeCustomer{
773
Id: cus.StripeCustomerID,
774
Currency: cus.Currency,
775
InvalidBillingAddress: invalidBillingAddress,
776
}
777
}
778
779