Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/usage/pkg/stripe/stripe.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 stripe
6
7
import (
8
"context"
9
"encoding/json"
10
"fmt"
11
"net/http"
12
"os"
13
"sort"
14
"strings"
15
"time"
16
17
"github.com/gitpod-io/gitpod/common-go/log"
18
db "github.com/gitpod-io/gitpod/components/gitpod-db/go"
19
"github.com/stripe/stripe-go/v72"
20
"github.com/stripe/stripe-go/v72/client"
21
"google.golang.org/grpc/codes"
22
"google.golang.org/grpc/status"
23
)
24
25
const (
26
// Metadata keys are used for storing additional context in Stripe
27
AttributionIDMetadataKey = "attributionId"
28
PreferredCurrencyMetadataKey = "preferredCurrency"
29
BillingCreaterUserIDMetadataKey = "billingCreatorUserId"
30
)
31
32
type Client struct {
33
sc *client.API
34
}
35
36
type ClientConfig struct {
37
PublishableKey string `json:"publishableKey"`
38
SecretKey string `json:"secretKey"`
39
}
40
41
type PriceConfig struct {
42
EUR string `json:"eur"`
43
USD string `json:"usd"`
44
}
45
46
type StripePrices struct {
47
IndividualUsagePriceIDs PriceConfig `json:"individualUsagePriceIds"`
48
TeamUsagePriceIDs PriceConfig `json:"teamUsagePriceIds"`
49
}
50
51
func ReadConfigFromFile(path string) (ClientConfig, error) {
52
bytes, err := os.ReadFile(path)
53
if err != nil {
54
return ClientConfig{}, fmt.Errorf("failed to read stripe client config: %w", err)
55
}
56
57
var config ClientConfig
58
err = json.Unmarshal(bytes, &config)
59
if err != nil {
60
return ClientConfig{}, fmt.Errorf("failed to unmarshal Stripe Client config: %w", err)
61
}
62
63
return config, nil
64
}
65
66
// New authenticates a Stripe client using the provided config
67
func New(config ClientConfig) (*Client, error) {
68
return NewWithHTTPClient(config, &http.Client{
69
Transport: http.DefaultTransport,
70
Timeout: 10 * time.Second,
71
})
72
}
73
74
func NewWithHTTPClient(config ClientConfig, c *http.Client) (*Client, error) {
75
sc := &client.API{}
76
77
sc.Init(config.SecretKey, stripe.NewBackends(c))
78
79
return &Client{sc: sc}, nil
80
}
81
82
type UsageRecord struct {
83
SubscriptionItemID string
84
Quantity int64
85
}
86
87
type Invoice struct {
88
ID string
89
SubscriptionID string
90
Amount int64
91
Currency string
92
Credits int64
93
}
94
95
// UpdateUsage updates teams' Stripe subscriptions with usage data
96
// `usageForTeam` is a map from team name to total workspace seconds used within a billing period.
97
func (c *Client) UpdateUsage(ctx context.Context, creditsPerAttributionID map[db.AttributionID]int64) error {
98
queries := queriesForCustomersWithAttributionIDs(creditsPerAttributionID)
99
100
for _, query := range queries {
101
logger := log.WithField("stripe_query", query)
102
103
customers, err := c.findCustomers(ctx, query)
104
if err != nil {
105
return fmt.Errorf("failed to find customers: %w", err)
106
}
107
108
for _, customer := range customers {
109
attributionIDRaw := customer.Metadata[AttributionIDMetadataKey]
110
logger.Infof("Found customer %q for attribution ID %q", customer.Name, attributionIDRaw)
111
112
attributionID, err := db.ParseAttributionID(attributionIDRaw)
113
if err != nil {
114
logger.WithError(err).Error("Failed to parse attribution ID from Stripe metadata.")
115
continue
116
}
117
118
lgr := logger.
119
WithField("customer_id", customer.ID).
120
WithField("customer_name", customer.Name).
121
WithField("subscriptions", customer.Subscriptions).
122
WithField("attribution_id", attributionID)
123
124
err = c.updateUsageForCustomer(ctx, customer, creditsPerAttributionID[attributionID])
125
if err != nil {
126
lgr.WithError(err).Errorf("Failed to update usage.")
127
}
128
reportStripeUsageUpdate(err)
129
}
130
}
131
return nil
132
}
133
134
func (c *Client) findCustomers(ctx context.Context, query string) (customers []*stripe.Customer, err error) {
135
now := time.Now()
136
reportStripeRequestStarted("customers_search")
137
defer func() {
138
reportStripeRequestCompleted("customers_search", err, time.Since(now))
139
}()
140
141
params := &stripe.CustomerSearchParams{
142
SearchParams: stripe.SearchParams{
143
Query: query,
144
Expand: []*string{stripe.String("data.tax"), stripe.String("data.subscriptions")},
145
Context: ctx,
146
},
147
}
148
iter := c.sc.Customers.Search(params)
149
150
for iter.Next() {
151
customers = append(customers, iter.Customer())
152
}
153
if iter.Err() != nil {
154
return nil, fmt.Errorf("failed to search for customers: %w", iter.Err())
155
}
156
157
return customers, nil
158
}
159
160
func (c *Client) updateUsageForCustomer(ctx context.Context, customer *stripe.Customer, credits int64) (err error) {
161
logger := log.
162
WithField("customer_id", customer.ID).
163
WithField("customer_name", customer.Name).
164
WithField("credits", credits)
165
if credits < 0 {
166
logger.Infof("Received request to update customer %s usage to negative value, updating to 0 instead.", customer.ID)
167
168
// nullify any existing usage, but do not set it to negative value - negative invoice doesn't make sense...
169
credits = 0
170
}
171
172
subscriptions := customer.Subscriptions.Data
173
if len(subscriptions) == 0 {
174
logger.Info("Customer does not have a valid (one) subscription. This happens when a customer has cancelled their subscription but still exist as a Customer in stripe.")
175
return nil
176
}
177
if len(subscriptions) > 1 {
178
return fmt.Errorf("customer has more than 1 subscription (got: %d), this is a consistency error and requires a manual intervention", len(subscriptions))
179
}
180
181
subscription := customer.Subscriptions.Data[0]
182
183
log.Infof("Customer has subscription: %q", subscription.ID)
184
if len(subscription.Items.Data) != 1 {
185
return fmt.Errorf("subscription %s has an unexpected number of subscriptionItems (expected 1, got %d)", subscription.ID, len(subscription.Items.Data))
186
}
187
188
subscriptionItemId := subscription.Items.Data[0].ID
189
log.Infof("Registering usage against subscriptionItem %q", subscriptionItemId)
190
191
reportStripeRequestStarted("usage_record_update")
192
now := time.Now()
193
defer func() {
194
reportStripeRequestCompleted("usage_record_update", err, time.Since(now))
195
}()
196
_, err = c.sc.UsageRecords.New(&stripe.UsageRecordParams{
197
Params: stripe.Params{
198
Context: ctx,
199
},
200
SubscriptionItem: stripe.String(subscriptionItemId),
201
Quantity: stripe.Int64(credits),
202
})
203
if err != nil {
204
return fmt.Errorf("failed to register usage for customer %q on subscription item %s", customer.Name, subscriptionItemId)
205
}
206
207
return nil
208
}
209
210
func (c *Client) GetCustomerByAttributionID(ctx context.Context, attributionID string) (*stripe.Customer, error) {
211
customers, err := c.findCustomers(ctx, fmt.Sprintf("metadata['attributionId']:'%s'", attributionID))
212
if err != nil {
213
return nil, status.Errorf(codes.Internal, "failed to find customers: %v", err)
214
}
215
216
if len(customers) == 0 {
217
return nil, status.Errorf(codes.NotFound, "no customer found for attribution_id: %s", attributionID)
218
}
219
if len(customers) > 1 {
220
return nil, status.Errorf(codes.FailedPrecondition, "found multiple customers for attributiuon_id: %s", attributionID)
221
}
222
223
return customers[0], nil
224
}
225
226
func (c *Client) GetCustomer(ctx context.Context, customerID string) (customer *stripe.Customer, err error) {
227
now := time.Now()
228
reportStripeRequestStarted("customer_get")
229
defer func() {
230
reportStripeRequestCompleted("customer_get", err, time.Since(now))
231
}()
232
233
customer, err = c.sc.Customers.Get(customerID, &stripe.CustomerParams{
234
Params: stripe.Params{
235
Context: ctx,
236
Expand: []*string{stripe.String("tax"), stripe.String("subscriptions")},
237
},
238
})
239
if err != nil {
240
if stripeErr, ok := err.(*stripe.Error); ok {
241
switch stripeErr.Code {
242
case stripe.ErrorCodeMissing:
243
return nil, status.Errorf(codes.NotFound, "customer %s does not exist in stripe", customerID)
244
}
245
}
246
247
return nil, fmt.Errorf("failed to get customer by customer ID %s", customerID)
248
}
249
250
return customer, nil
251
}
252
253
func (c *Client) GetPriceInformation(ctx context.Context, priceID string) (price *stripe.Price, err error) {
254
now := time.Now()
255
reportStripeRequestStarted("prices_get")
256
defer func() {
257
reportStripeRequestCompleted("prices_get", err, time.Since(now))
258
}()
259
260
price, err = c.sc.Prices.Get(priceID, &stripe.PriceParams{
261
Params: stripe.Params{
262
Context: ctx,
263
},
264
})
265
if err != nil {
266
if stripeErr, ok := err.(*stripe.Error); ok {
267
switch stripeErr.Code {
268
case stripe.ErrorCodeMissing:
269
return nil, status.Errorf(codes.NotFound, "price %s does not exist in stripe", priceID)
270
}
271
}
272
273
return nil, fmt.Errorf("failed to get price by price ID %s", priceID)
274
}
275
276
return price, nil
277
}
278
279
type CreateCustomerParams struct {
280
AttributionID string
281
Currency string
282
Email string
283
Name string
284
BillingCreatorUserID string
285
}
286
287
func (c *Client) CreateCustomer(ctx context.Context, params CreateCustomerParams) (customer *stripe.Customer, err error) {
288
now := time.Now()
289
reportStripeRequestStarted("customers_create")
290
defer func() {
291
reportStripeRequestCompleted("customers_create", err, time.Since(now))
292
}()
293
294
customer, err = c.sc.Customers.New(&stripe.CustomerParams{
295
Params: stripe.Params{
296
Context: ctx,
297
Metadata: map[string]string{
298
// We set the preferred currency on the metadata such that we can later retreive it when we're creating a Subscription
299
// This is also done to propagate the preference into the Customer such that we can inform them when their
300
// new subscription would use a different currency to the previous one
301
PreferredCurrencyMetadataKey: params.Currency,
302
AttributionIDMetadataKey: params.AttributionID,
303
BillingCreaterUserIDMetadataKey: params.BillingCreatorUserID,
304
},
305
},
306
Email: stripe.String(params.Email),
307
Name: stripe.String(params.Name),
308
})
309
if err != nil {
310
return nil, fmt.Errorf("failed to create Stripe customer: %w", err)
311
}
312
return customer, nil
313
}
314
315
func (c *Client) GetInvoiceWithCustomer(ctx context.Context, invoiceID string) (invoice *stripe.Invoice, err error) {
316
if invoiceID == "" {
317
return nil, fmt.Errorf("no invoice ID specified")
318
}
319
320
now := time.Now()
321
reportStripeRequestStarted("invoice_get")
322
defer func() {
323
reportStripeRequestCompleted("invoice_get", err, time.Since(now))
324
}()
325
326
invoice, err = c.sc.Invoices.Get(invoiceID, &stripe.InvoiceParams{
327
Params: stripe.Params{
328
Context: ctx,
329
Expand: []*string{stripe.String("customer")},
330
},
331
})
332
if err != nil {
333
return nil, fmt.Errorf("failed to get invoice %s: %w", invoiceID, err)
334
}
335
return invoice, nil
336
}
337
338
func (c *Client) ListInvoices(ctx context.Context, customerId string) (invoices []*stripe.Invoice, err error) {
339
if customerId == "" {
340
return nil, fmt.Errorf("no customer ID specified")
341
}
342
343
now := time.Now()
344
reportStripeRequestStarted("invoice_list")
345
defer func() {
346
reportStripeRequestCompleted("invoice_list", err, time.Since(now))
347
}()
348
349
invoicesResponse := c.sc.Invoices.List(&stripe.InvoiceListParams{
350
Customer: stripe.String(customerId),
351
})
352
if invoicesResponse.Err() != nil {
353
return nil, fmt.Errorf("failed to get invoices for customer %s: %w", customerId, invoicesResponse.Err())
354
}
355
return invoicesResponse.InvoiceList().Data, nil
356
}
357
358
func (c *Client) GetSubscriptionWithCustomer(ctx context.Context, subscriptionID string) (subscription *stripe.Subscription, err error) {
359
if subscriptionID == "" {
360
return nil, fmt.Errorf("no subscriptionID specified")
361
}
362
363
now := time.Now()
364
reportStripeRequestStarted("subscription_get")
365
defer func() {
366
reportStripeRequestCompleted("subscription_get", err, time.Since(now))
367
}()
368
369
subscription, err = c.sc.Subscriptions.Get(subscriptionID, &stripe.SubscriptionParams{
370
Params: stripe.Params{
371
Expand: []*string{stripe.String("customer")},
372
},
373
})
374
if err != nil {
375
return nil, fmt.Errorf("failed to get subscription %s: %w", subscriptionID, err)
376
}
377
return subscription, nil
378
}
379
380
func (c *Client) CreateSubscription(ctx context.Context, customerID string, priceID string, isAutomaticTaxSupported bool) (*stripe.Subscription, error) {
381
if customerID == "" {
382
return nil, fmt.Errorf("no customerID specified")
383
}
384
if priceID == "" {
385
return nil, fmt.Errorf("no priceID specified")
386
}
387
388
params := &stripe.SubscriptionParams{
389
Customer: stripe.String(customerID),
390
Items: []*stripe.SubscriptionItemsParams{
391
{
392
Price: stripe.String(priceID),
393
},
394
},
395
AutomaticTax: &stripe.SubscriptionAutomaticTaxParams{
396
Enabled: stripe.Bool(isAutomaticTaxSupported),
397
},
398
}
399
400
subscription, err := c.sc.Subscriptions.New(params)
401
if err != nil {
402
return nil, fmt.Errorf("failed to get subscription with customer ID %s", customerID)
403
}
404
405
return subscription, err
406
}
407
408
func (c *Client) UpdateSubscriptionAutomaticTax(ctx context.Context, subscriptionID string, isAutomaticTaxSupported bool) (*stripe.Subscription, error) {
409
if subscriptionID == "" {
410
return nil, fmt.Errorf("no subscriptionID specified")
411
}
412
413
params := &stripe.SubscriptionParams{
414
AutomaticTax: &stripe.SubscriptionAutomaticTaxParams{
415
Enabled: stripe.Bool(isAutomaticTaxSupported),
416
},
417
}
418
419
subscription, err := c.sc.Subscriptions.Update(subscriptionID, params)
420
if err != nil {
421
return nil, fmt.Errorf("failed to update subscription with subscription ID %s", subscriptionID)
422
}
423
424
return subscription, err
425
}
426
427
func (c *Client) SetDefaultPaymentForCustomer(ctx context.Context, customerID string, paymentIntentId string) (*stripe.Customer, error) {
428
if customerID == "" {
429
return nil, fmt.Errorf("no customerID specified")
430
}
431
432
if paymentIntentId == "" {
433
return nil, fmt.Errorf("no paymentIntentId specified")
434
}
435
436
paymentIntent, err := c.sc.PaymentIntents.Get(paymentIntentId, &stripe.PaymentIntentParams{
437
Params: stripe.Params{
438
Context: ctx,
439
},
440
})
441
if err != nil {
442
return nil, fmt.Errorf("Failed to retrieve payment intent with id %s", paymentIntentId)
443
}
444
445
paymentMethod, err := c.sc.PaymentMethods.Attach(paymentIntent.PaymentMethod.ID, &stripe.PaymentMethodAttachParams{Customer: &customerID})
446
if err != nil {
447
return nil, fmt.Errorf("Failed to attach payment method to payment intent ID %s", paymentIntentId)
448
}
449
450
customer, _ := c.sc.Customers.Update(customerID, &stripe.CustomerParams{
451
InvoiceSettings: &stripe.CustomerInvoiceSettingsParams{
452
DefaultPaymentMethod: stripe.String(paymentMethod.ID)},
453
Address: &stripe.AddressParams{
454
Line1: stripe.String(paymentMethod.BillingDetails.Address.Line1),
455
Line2: stripe.String(paymentMethod.BillingDetails.Address.Line2),
456
City: stripe.String(paymentMethod.BillingDetails.Address.City),
457
PostalCode: stripe.String(paymentMethod.BillingDetails.Address.PostalCode),
458
Country: stripe.String(paymentMethod.BillingDetails.Address.Country),
459
State: stripe.String(paymentMethod.BillingDetails.Address.State),
460
}})
461
if err != nil {
462
return nil, fmt.Errorf("Failed to update customer with id %s", customerID)
463
}
464
465
return customer, nil
466
}
467
468
func (c *Client) CreateHoldPaymentIntent(ctx context.Context, customer *stripe.Customer, amountInCents int) (*stripe.PaymentIntent, error) {
469
if customer == nil {
470
return nil, fmt.Errorf("no customer specified")
471
}
472
473
currency := customer.Metadata["preferredCurrency"]
474
if currency == "" {
475
currency = string(stripe.CurrencyUSD)
476
}
477
478
// We create a payment intent with the amount we want to hold
479
paymentIntent, err := c.sc.PaymentIntents.New(&stripe.PaymentIntentParams{
480
Amount: stripe.Int64(int64(amountInCents)),
481
Currency: stripe.String(currency),
482
Customer: stripe.String(customer.ID),
483
PaymentMethodTypes: stripe.StringSlice([]string{
484
"card",
485
}),
486
// Place a hold on the funds when the customer authorizes the payment
487
CaptureMethod: stripe.String(string(stripe.PaymentIntentCaptureMethodManual)),
488
// This allows us to use this payment method for subscription payments
489
SetupFutureUsage: stripe.String(string(stripe.PaymentIntentSetupFutureUsageOffSession)),
490
})
491
if err != nil {
492
return nil, fmt.Errorf("failed to create hold payment intent: %w", err)
493
}
494
495
return paymentIntent, nil
496
}
497
498
func (c *Client) GetDispute(ctx context.Context, disputeID string) (dispute *stripe.Dispute, err error) {
499
now := time.Now()
500
reportStripeRequestStarted("dispute_get")
501
defer func() {
502
reportStripeRequestCompleted("dispute_get", err, time.Since(now))
503
}()
504
params := &stripe.DisputeParams{
505
Params: stripe.Params{
506
Context: ctx,
507
},
508
}
509
params.AddExpand("payment_intent.customer")
510
511
dispute, err = c.sc.Disputes.Get(disputeID, params)
512
if err != nil {
513
return nil, fmt.Errorf("failed to retrieve dispute ID: %s", disputeID)
514
}
515
516
return dispute, nil
517
}
518
519
type PaymentHoldResult string
520
521
// List of values that PaymentIntentStatus can take
522
const (
523
PaymentHoldResultRequiresAction PaymentHoldResult = "requires_action"
524
PaymentHoldResultRequiresConfirmation PaymentHoldResult = "requires_confirmation"
525
PaymentHoldResultRequiresPaymentMethod PaymentHoldResult = "requires_payment_method"
526
PaymentHoldResultSucceeded PaymentHoldResult = "succeeded"
527
PaymentHoldResultFailed PaymentHoldResult = "failed"
528
)
529
530
// TODO: We can replace this with TryHoldAmountForPaymentIntent once we use payment intents for the subscription flow
531
func (c *Client) TryHoldAmount(ctx context.Context, customer *stripe.Customer, amountInCents int) (PaymentHoldResult, error) {
532
if customer == nil {
533
return PaymentHoldResultFailed, fmt.Errorf("no customer specified")
534
}
535
536
if amountInCents <= 0 {
537
return PaymentHoldResultFailed, fmt.Errorf("amountInCents must be greater than 0")
538
}
539
540
currency := customer.Metadata["preferredCurrency"]
541
if currency == "" {
542
currency = string(stripe.CurrencyUSD)
543
}
544
545
// we create a payment intent with the amount we want to hold
546
// and then cancel it immediately
547
paymentIntent, err := c.sc.PaymentIntents.New(&stripe.PaymentIntentParams{
548
Amount: stripe.Int64(int64(amountInCents)),
549
Currency: stripe.String(currency),
550
Customer: stripe.String(customer.ID),
551
PaymentMethodTypes: stripe.StringSlice([]string{
552
"card",
553
}),
554
PaymentMethod: stripe.String(customer.InvoiceSettings.DefaultPaymentMethod.ID),
555
CaptureMethod: stripe.String(string(stripe.PaymentIntentCaptureMethodManual)),
556
Confirm: stripe.Bool(true),
557
})
558
if err != nil {
559
return PaymentHoldResultFailed, fmt.Errorf("failed to confirm payment intent: %w", err)
560
}
561
if paymentIntent.Status != stripe.PaymentIntentStatusRequiresCapture {
562
result := PaymentHoldResultFailed
563
if paymentIntent.Status == stripe.PaymentIntentStatusRequiresAction {
564
result = PaymentHoldResultRequiresAction
565
} else if paymentIntent.Status == stripe.PaymentIntentStatusRequiresConfirmation {
566
result = PaymentHoldResultRequiresConfirmation
567
} else if paymentIntent.Status == stripe.PaymentIntentStatusRequiresPaymentMethod {
568
result = PaymentHoldResultRequiresPaymentMethod
569
}
570
return result, fmt.Errorf("Couldn't put a hold on the card: %s", paymentIntent.Status)
571
}
572
paymentIntent, err = c.sc.PaymentIntents.Cancel(paymentIntent.ID, nil)
573
if err != nil {
574
log.Errorf("Failed to cancel payment intent: %v", err)
575
return PaymentHoldResultSucceeded, nil
576
}
577
if paymentIntent.Status != stripe.PaymentIntentStatusCanceled {
578
log.Errorf("Failed to cancel payment intent: %v", err)
579
return PaymentHoldResultSucceeded, nil
580
}
581
log.Info("Successfully put a hold on the card. Payment intent canceled.", customer.ID)
582
return PaymentHoldResultSucceeded, nil
583
}
584
585
func (c *Client) TryHoldAmountForPaymentIntent(ctx context.Context, customer *stripe.Customer, holdPaymentIntentId string) (PaymentHoldResult, error) {
586
if customer == nil {
587
return PaymentHoldResultFailed, fmt.Errorf("no customer specified")
588
}
589
590
if holdPaymentIntentId == "" {
591
return PaymentHoldResultFailed, fmt.Errorf("no payment intent specified for hold")
592
}
593
// ensure we cancel the payment intent so we remove the hold
594
defer func(holdPaymentIntentId string) {
595
paymentIntent, err := c.sc.PaymentIntents.Cancel(holdPaymentIntentId, nil)
596
if err != nil {
597
log.Errorf("Failed to cancel payment intent: %v", err)
598
return
599
}
600
if paymentIntent.Status != stripe.PaymentIntentStatusCanceled {
601
log.Errorf("Failed to cancel payment intent: %v", err)
602
return
603
}
604
log.Debugf("Successfully cancelled payment intent %s", holdPaymentIntentId)
605
}(holdPaymentIntentId)
606
607
paymentIntent, err := c.sc.PaymentIntents.Get(holdPaymentIntentId, nil)
608
if err != nil {
609
return "", fmt.Errorf("failed to retrieve payment intent: %w", err)
610
}
611
612
if paymentIntent.Status != stripe.PaymentIntentStatusRequiresCapture {
613
result := PaymentHoldResultFailed
614
if paymentIntent.Status == stripe.PaymentIntentStatusRequiresAction {
615
result = PaymentHoldResultRequiresAction
616
} else if paymentIntent.Status == stripe.PaymentIntentStatusRequiresConfirmation {
617
result = PaymentHoldResultRequiresConfirmation
618
} else if paymentIntent.Status == stripe.PaymentIntentStatusRequiresPaymentMethod {
619
result = PaymentHoldResultRequiresPaymentMethod
620
}
621
return result, fmt.Errorf("Couldn't put a hold on the card: %s", paymentIntent.Status)
622
}
623
624
log.Info("Successfully put a hold on the card.", customer.ID)
625
return PaymentHoldResultSucceeded, nil
626
}
627
628
func GetAttributionID(ctx context.Context, customer *stripe.Customer) (db.AttributionID, error) {
629
if customer == nil {
630
log.Error("No customer information available for invoice.")
631
return "", status.Errorf(codes.Internal, "Failed to retrieve customer details from invoice.")
632
}
633
return db.ParseAttributionID(customer.Metadata[AttributionIDMetadataKey])
634
}
635
636
// queriesForCustomersWithAttributionIDs constructs Stripe query strings to find the Stripe Customer for each teamId
637
// It returns multiple queries, each being a big disjunction of subclauses so that we can process multiple teamIds in one query.
638
// `clausesPerQuery` is a limit enforced by the Stripe API.
639
func queriesForCustomersWithAttributionIDs(creditsByAttributionID map[db.AttributionID]int64) []string {
640
attributionIDs := make([]string, 0, len(creditsByAttributionID))
641
for k := range creditsByAttributionID {
642
attributionIDs = append(attributionIDs, string(k))
643
}
644
sort.Strings(attributionIDs)
645
646
const clausesPerQuery = 10
647
var queries []string
648
sb := strings.Builder{}
649
650
for i := 0; i < len(attributionIDs); i += clausesPerQuery {
651
sb.Reset()
652
for j := 0; j < clausesPerQuery && i+j < len(attributionIDs); j++ {
653
sb.WriteString(fmt.Sprintf("metadata['%s']:'%s'", AttributionIDMetadataKey, attributionIDs[i+j]))
654
if j < clausesPerQuery-1 && i+j < len(attributionIDs)-1 {
655
sb.WriteString(" OR ")
656
}
657
}
658
queries = append(queries, sb.String())
659
}
660
661
return queries
662
}
663
664