Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/usage/pkg/apiv1/billing_test.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
"encoding/json"
10
"fmt"
11
"testing"
12
13
"github.com/bufbuild/connect-go"
14
db "github.com/gitpod-io/gitpod/components/gitpod-db/go"
15
"github.com/gitpod-io/gitpod/components/gitpod-db/go/dbtest"
16
experimental_v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
17
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
18
v1 "github.com/gitpod-io/gitpod/usage-api/v1"
19
"github.com/gitpod-io/gitpod/usage/pkg/stripe"
20
"github.com/google/uuid"
21
"github.com/stretchr/testify/require"
22
stripe_api "github.com/stripe/stripe-go/v72"
23
"gopkg.in/dnaeon/go-vcr.v3/cassette"
24
"gopkg.in/dnaeon/go-vcr.v3/recorder"
25
)
26
27
func TestBillingService_OnChargeDispute(t *testing.T) {
28
r := NewStripeRecorder(t, "stripe_on_charge_dispute")
29
30
client := r.GetDefaultClient()
31
stripeClient, err := stripe.NewWithHTTPClient(stripe.ClientConfig{
32
SecretKey: "testkey",
33
}, client)
34
require.NoError(t, err)
35
36
stubUserService := &StubUserService{}
37
svc := &BillingService{
38
stripeClient: stripeClient,
39
teamsService: &StubTeamsService{},
40
userService: stubUserService,
41
}
42
43
_, err = svc.OnChargeDispute(context.Background(), &v1.OnChargeDisputeRequest{
44
DisputeId: "dp_1MrLJpAyBDPbWrhawbWHEIDL",
45
})
46
require.NoError(t, err)
47
48
require.Equal(t, stubUserService.blockedUsers, []string{"owner_id"})
49
}
50
51
func NewStripeRecorder(t *testing.T, name string) *recorder.Recorder {
52
t.Helper()
53
54
r, err := recorder.New(fmt.Sprintf("fixtures/%s", name))
55
require.NoError(t, err)
56
57
t.Cleanup(func() {
58
err = r.Stop()
59
})
60
61
// Add a hook which removes Authorization headers from all requests
62
hook := func(i *cassette.Interaction) error {
63
delete(i.Request.Headers, "Authorization")
64
return nil
65
}
66
r.AddHook(hook, recorder.AfterCaptureHook)
67
68
if r.Mode() != recorder.ModeRecordOnce {
69
require.Fail(t, "Recorder should be in ModeRecordOnce")
70
}
71
72
return r
73
}
74
75
type StubTeamsService struct {
76
v1connect.TeamsServiceClient
77
}
78
79
func (s *StubTeamsService) CreateTeam(context.Context, *connect.Request[experimental_v1.CreateTeamRequest]) (*connect.Response[experimental_v1.CreateTeamResponse], error) {
80
return nil, nil
81
}
82
83
func (s *StubTeamsService) GetTeam(ctx context.Context, req *connect.Request[experimental_v1.GetTeamRequest]) (*connect.Response[experimental_v1.GetTeamResponse], error) {
84
// generate a stub which returns a team
85
team := &experimental_v1.Team{
86
Id: req.Msg.GetTeamId(),
87
Members: []*experimental_v1.TeamMember{
88
{
89
UserId: "owner_id",
90
Role: experimental_v1.TeamRole_TEAM_ROLE_OWNER,
91
},
92
{
93
UserId: "non_owner_id",
94
Role: experimental_v1.TeamRole_TEAM_ROLE_MEMBER,
95
},
96
},
97
}
98
99
return connect.NewResponse(&experimental_v1.GetTeamResponse{
100
Team: team,
101
}), nil
102
}
103
104
func (s *StubTeamsService) ListTeams(context.Context, *connect.Request[experimental_v1.ListTeamsRequest]) (*connect.Response[experimental_v1.ListTeamsResponse], error) {
105
return nil, nil
106
}
107
func (s *StubTeamsService) DeleteTeam(context.Context, *connect.Request[experimental_v1.DeleteTeamRequest]) (*connect.Response[experimental_v1.DeleteTeamResponse], error) {
108
return nil, nil
109
}
110
func (s *StubTeamsService) JoinTeam(context.Context, *connect.Request[experimental_v1.JoinTeamRequest]) (*connect.Response[experimental_v1.JoinTeamResponse], error) {
111
return nil, nil
112
}
113
func (s *StubTeamsService) ResetTeamInvitation(context.Context, *connect.Request[experimental_v1.ResetTeamInvitationRequest]) (*connect.Response[experimental_v1.ResetTeamInvitationResponse], error) {
114
return nil, nil
115
}
116
func (s *StubTeamsService) UpdateTeamMember(context.Context, *connect.Request[experimental_v1.UpdateTeamMemberRequest]) (*connect.Response[experimental_v1.UpdateTeamMemberResponse], error) {
117
return nil, nil
118
}
119
func (s *StubTeamsService) DeleteTeamMember(context.Context, *connect.Request[experimental_v1.DeleteTeamMemberRequest]) (*connect.Response[experimental_v1.DeleteTeamMemberResponse], error) {
120
return nil, nil
121
}
122
123
type StubUserService struct {
124
blockedUsers []string
125
}
126
127
func (s *StubUserService) GetAuthenticatedUser(context.Context, *connect.Request[experimental_v1.GetAuthenticatedUserRequest]) (*connect.Response[experimental_v1.GetAuthenticatedUserResponse], error) {
128
return nil, nil
129
}
130
131
// ListSSHKeys lists the public SSH keys.
132
func (s *StubUserService) ListSSHKeys(context.Context, *connect.Request[experimental_v1.ListSSHKeysRequest]) (*connect.Response[experimental_v1.ListSSHKeysResponse], error) {
133
return nil, nil
134
}
135
136
// CreateSSHKey adds a public SSH key.
137
func (s *StubUserService) CreateSSHKey(context.Context, *connect.Request[experimental_v1.CreateSSHKeyRequest]) (*connect.Response[experimental_v1.CreateSSHKeyResponse], error) {
138
return nil, nil
139
}
140
141
// GetSSHKey retrieves an ssh key by ID.
142
func (s *StubUserService) GetSSHKey(context.Context, *connect.Request[experimental_v1.GetSSHKeyRequest]) (*connect.Response[experimental_v1.GetSSHKeyResponse], error) {
143
return nil, nil
144
}
145
146
// DeleteSSHKey removes a public SSH key.
147
func (s *StubUserService) DeleteSSHKey(context.Context, *connect.Request[experimental_v1.DeleteSSHKeyRequest]) (*connect.Response[experimental_v1.DeleteSSHKeyResponse], error) {
148
return nil, nil
149
}
150
func (s *StubUserService) GetGitToken(context.Context, *connect.Request[experimental_v1.GetGitTokenRequest]) (*connect.Response[experimental_v1.GetGitTokenResponse], error) {
151
return nil, nil
152
}
153
func (s *StubUserService) BlockUser(ctx context.Context, req *connect.Request[experimental_v1.BlockUserRequest]) (*connect.Response[experimental_v1.BlockUserResponse], error) {
154
s.blockedUsers = append(s.blockedUsers, req.Msg.GetUserId())
155
return connect.NewResponse(&experimental_v1.BlockUserResponse{}), nil
156
}
157
158
func TestBalancesForStripeCostCenters(t *testing.T) {
159
attributionIDForStripe := db.NewTeamAttributionID(uuid.New().String())
160
attributionIDForOther := db.NewTeamAttributionID(uuid.New().String())
161
dbconn := dbtest.ConnectForTests(t)
162
163
dbtest.CreateCostCenters(t, dbconn,
164
dbtest.NewCostCenter(t, db.CostCenter{
165
ID: attributionIDForStripe,
166
BillingStrategy: db.CostCenter_Stripe,
167
}),
168
dbtest.NewCostCenter(t, db.CostCenter{
169
ID: attributionIDForOther,
170
BillingStrategy: db.CostCenter_Other,
171
}),
172
)
173
174
balances := []db.Balance{
175
{
176
AttributionID: attributionIDForStripe,
177
CreditCents: 100,
178
},
179
{
180
AttributionID: attributionIDForOther,
181
CreditCents: 100,
182
},
183
}
184
185
stripeBalances, err := balancesForStripeCostCenters(context.Background(), db.NewCostCenterManager(dbconn, db.DefaultSpendingLimit{}), balances)
186
require.NoError(t, err)
187
require.Len(t, stripeBalances, 1)
188
require.Equal(t, stripeBalances[0].AttributionID, attributionIDForStripe)
189
}
190
191
func TestFinalizeInvoiceForIndividual(t *testing.T) {
192
invoice := stripe_api.Invoice{}
193
require.NoError(t, json.Unmarshal([]byte(IndiInvoiceTestData), &invoice))
194
usage, err := InternalComputeInvoiceUsage(context.Background(), &invoice, invoice.Customer)
195
require.NoError(t, err)
196
require.Equal(t, usage.CreditCents, db.CreditCents(-103100))
197
}
198
199
var IndiInvoiceTestData = `{
200
"id": "in_1MA0RBAyBDPbWrhabNdJIuhl",
201
"object": "invoice",
202
"account_country": "DE",
203
"account_name": "Gitpod GmbH",
204
"account_tax_ids": null,
205
"amount_due": 1012,
206
"amount_paid": 1012,
207
"amount_remaining": 0,
208
"application": null,
209
"application_fee_amount": null,
210
"attempt_count": 1,
211
"attempted": true,
212
"auto_advance": false,
213
"automatic_tax": {
214
"enabled": false,
215
"status": null
216
},
217
"billing_reason": "subscription_cycle",
218
"charge": "ch_3MA1YjAyBDPbWrha1F1mqyQs",
219
"collection_method": "charge_automatically",
220
"created": 1669853737,
221
"currency": "usd",
222
"custom_fields": null,
223
"customer": {
224
"id": "cus_MoA9ghwDcE2vaA",
225
"object": "customer",
226
"address": {
227
"city": null,
228
"country": "TW",
229
"line1": "",
230
"line2": null,
231
"postal_code": null,
232
"state": null
233
},
234
"balance": 0,
235
"created": 1668552088,
236
"currency": "usd",
237
"default_currency": "usd",
238
"default_source": null,
239
"delinquent": false,
240
"description": null,
241
"discount": null,
242
"email": "[email protected]",
243
"invoice_prefix": "89796AD3",
244
"invoice_settings": {
245
"custom_fields": null,
246
"default_payment_method": "pm_1M4XoqAyBDPbWrhaqEc29Ev0",
247
"footer": null,
248
"rendering_options": null
249
},
250
"livemode": false,
251
"metadata": {
252
"attributionId": "team:12345678-1234-1234-1234-123456789abc",
253
"preferredCurrency": "USD"
254
},
255
"name": "user-name",
256
"phone": null,
257
"preferred_locales": [],
258
"shipping": null,
259
"tax_exempt": "none",
260
"test_clock": null
261
},
262
"customer_address": {
263
"city": null,
264
"country": "TW",
265
"line1": "",
266
"line2": null,
267
"postal_code": null,
268
"state": null
269
},
270
"customer_email": "[email protected]",
271
"customer_name": "user-name",
272
"customer_phone": null,
273
"customer_shipping": null,
274
"customer_tax_exempt": "none",
275
"customer_tax_ids": [],
276
"default_payment_method": null,
277
"default_source": null,
278
"default_tax_rates": [],
279
"description": null,
280
"discount": null,
281
"discounts": [],
282
"due_date": null,
283
"ending_balance": 0,
284
"footer": null,
285
"from_invoice": null,
286
"hosted_invoice_url": "xxxx",
287
"invoice_pdf": "xxxx",
288
"last_finalization_error": null,
289
"latest_revision": null,
290
"lines": {
291
"object": "list",
292
"data": [
293
{
294
"id": "il_1MA0RBAyBDPbWrhaMKGOYdcr",
295
"object": "line_item",
296
"amount": 0,
297
"amount_excluding_tax": 0,
298
"currency": "usd",
299
"description": "1000 credit × Gitpod Usage (Tier 1 at $0.00 / month)",
300
"discount_amounts": [],
301
"discountable": true,
302
"discounts": [],
303
"livemode": false,
304
"metadata": {},
305
"period": {
306
"end": 1669852800,
307
"start": 1668552093
308
},
309
"plan": {
310
"id": "price_1LmYDQAyBDPbWrhaiebWlzVX",
311
"object": "plan",
312
"active": true,
313
"aggregate_usage": "last_during_period",
314
"amount": null,
315
"amount_decimal": null,
316
"billing_scheme": "tiered",
317
"created": 1664263708,
318
"currency": "usd",
319
"interval": "month",
320
"interval_count": 1,
321
"livemode": false,
322
"metadata": {},
323
"nickname": "Individual USD",
324
"product": "prod_MIUT2nUscrEWBA",
325
"tiers_mode": "graduated",
326
"transform_usage": null,
327
"trial_period_days": null,
328
"usage_type": "metered"
329
},
330
"price": {
331
"id": "price_1LmYDQAyBDPbWrhaiebWlzVX",
332
"object": "price",
333
"active": true,
334
"billing_scheme": "tiered",
335
"created": 1664263708,
336
"currency": "usd",
337
"custom_unit_amount": null,
338
"livemode": false,
339
"lookup_key": null,
340
"metadata": {},
341
"nickname": "Individual USD",
342
"product": "prod_MIUT2nUscrEWBA",
343
"recurring": {
344
"aggregate_usage": "last_during_period",
345
"interval": "month",
346
"interval_count": 1,
347
"trial_period_days": null,
348
"usage_type": "metered"
349
},
350
"tax_behavior": "inclusive",
351
"tiers_mode": "graduated",
352
"transform_quantity": null,
353
"type": "recurring",
354
"unit_amount": null,
355
"unit_amount_decimal": null
356
},
357
"proration": false,
358
"proration_details": {
359
"credited_items": null
360
},
361
"quantity": 1000,
362
"subscription": "sub_1M4XovAyBDPbWrhaCnn4gigv",
363
"subscription_item": "si_MoA9zVoSS4gH2G",
364
"tax_amounts": [],
365
"tax_rates": [],
366
"type": "subscription",
367
"unit_amount_excluding_tax": "0"
368
},
369
{
370
"id": "il_1MA0RCAyBDPbWrhaogm8Cw8j",
371
"object": "line_item",
372
"amount": 900,
373
"amount_excluding_tax": 900,
374
"currency": "usd",
375
"description": "Gitpod Usage (Tier 1 at $9.00 / month)",
376
"discount_amounts": [],
377
"discountable": true,
378
"discounts": [],
379
"livemode": false,
380
"metadata": {},
381
"period": {
382
"end": 1669852800,
383
"start": 1668552093
384
},
385
"plan": {
386
"id": "price_1LmYDQAyBDPbWrhaiebWlzVX",
387
"object": "plan",
388
"active": true,
389
"aggregate_usage": "last_during_period",
390
"amount": null,
391
"amount_decimal": null,
392
"billing_scheme": "tiered",
393
"created": 1664263708,
394
"currency": "usd",
395
"interval": "month",
396
"interval_count": 1,
397
"livemode": false,
398
"metadata": {},
399
"nickname": "Individual USD",
400
"product": "prod_MIUT2nUscrEWBA",
401
"tiers_mode": "graduated",
402
"transform_usage": null,
403
"trial_period_days": null,
404
"usage_type": "metered"
405
},
406
"price": {
407
"id": "price_1LmYDQAyBDPbWrhaiebWlzVX",
408
"object": "price",
409
"active": true,
410
"billing_scheme": "tiered",
411
"created": 1664263708,
412
"currency": "usd",
413
"custom_unit_amount": null,
414
"livemode": false,
415
"lookup_key": null,
416
"metadata": {},
417
"nickname": "Individual USD",
418
"product": "prod_MIUT2nUscrEWBA",
419
"recurring": {
420
"aggregate_usage": "last_during_period",
421
"interval": "month",
422
"interval_count": 1,
423
"trial_period_days": null,
424
"usage_type": "metered"
425
},
426
"tax_behavior": "inclusive",
427
"tiers_mode": "graduated",
428
"transform_quantity": null,
429
"type": "recurring",
430
"unit_amount": null,
431
"unit_amount_decimal": null
432
},
433
"proration": false,
434
"proration_details": {
435
"credited_items": null
436
},
437
"quantity": 0,
438
"subscription": "sub_1M4XovAyBDPbWrhaCnn4gigv",
439
"subscription_item": "si_MoA9zVoSS4gH2G",
440
"tax_amounts": [],
441
"tax_rates": [],
442
"type": "subscription",
443
"unit_amount_excluding_tax": null
444
},
445
{
446
"id": "il_1MA0RDAyBDPbWrhaIF5LaBhx",
447
"object": "line_item",
448
"amount": 112,
449
"amount_excluding_tax": 112,
450
"currency": "usd",
451
"description": "31 credit × Gitpod Usage (Tier 2 at $0.036 / month)",
452
"discount_amounts": [],
453
"discountable": true,
454
"discounts": [],
455
"livemode": false,
456
"metadata": {},
457
"period": {
458
"end": 1669852800,
459
"start": 1668552093
460
},
461
"plan": {
462
"id": "price_1LmYDQAyBDPbWrhaiebWlzVX",
463
"object": "plan",
464
"active": true,
465
"aggregate_usage": "last_during_period",
466
"amount": null,
467
"amount_decimal": null,
468
"billing_scheme": "tiered",
469
"created": 1664263708,
470
"currency": "usd",
471
"interval": "month",
472
"interval_count": 1,
473
"livemode": false,
474
"metadata": {},
475
"nickname": "Individual USD",
476
"product": "prod_MIUT2nUscrEWBA",
477
"tiers_mode": "graduated",
478
"transform_usage": null,
479
"trial_period_days": null,
480
"usage_type": "metered"
481
},
482
"price": {
483
"id": "price_1LmYDQAyBDPbWrhaiebWlzVX",
484
"object": "price",
485
"active": true,
486
"billing_scheme": "tiered",
487
"created": 1664263708,
488
"currency": "usd",
489
"custom_unit_amount": null,
490
"livemode": false,
491
"lookup_key": null,
492
"metadata": {},
493
"nickname": "Individual USD",
494
"product": "prod_MIUT2nUscrEWBA",
495
"recurring": {
496
"aggregate_usage": "last_during_period",
497
"interval": "month",
498
"interval_count": 1,
499
"trial_period_days": null,
500
"usage_type": "metered"
501
},
502
"tax_behavior": "inclusive",
503
"tiers_mode": "graduated",
504
"transform_quantity": null,
505
"type": "recurring",
506
"unit_amount": null,
507
"unit_amount_decimal": null
508
},
509
"proration": false,
510
"proration_details": {
511
"credited_items": null
512
},
513
"quantity": 31,
514
"subscription": "sub_1M4XovAyBDPbWrhaCnn4gigv",
515
"subscription_item": "si_MoA9zVoSS4gH2G",
516
"tax_amounts": [],
517
"tax_rates": [],
518
"type": "subscription",
519
"unit_amount_excluding_tax": "4"
520
},
521
{
522
"id": "il_1MA0REAyBDPbWrhaMRhZhiJ6",
523
"object": "line_item",
524
"amount": 0,
525
"amount_excluding_tax": 0,
526
"currency": "usd",
527
"description": "Gitpod Usage (Tier 2 at $0.00 / month)",
528
"discount_amounts": [],
529
"discountable": true,
530
"discounts": [],
531
"livemode": false,
532
"metadata": {},
533
"period": {
534
"end": 1669852800,
535
"start": 1668552093
536
},
537
"plan": {
538
"id": "price_1LmYDQAyBDPbWrhaiebWlzVX",
539
"object": "plan",
540
"active": true,
541
"aggregate_usage": "last_during_period",
542
"amount": null,
543
"amount_decimal": null,
544
"billing_scheme": "tiered",
545
"created": 1664263708,
546
"currency": "usd",
547
"interval": "month",
548
"interval_count": 1,
549
"livemode": false,
550
"metadata": {},
551
"nickname": "Individual USD",
552
"product": "prod_MIUT2nUscrEWBA",
553
"tiers_mode": "graduated",
554
"transform_usage": null,
555
"trial_period_days": null,
556
"usage_type": "metered"
557
},
558
"price": {
559
"id": "price_1LmYDQAyBDPbWrhaiebWlzVX",
560
"object": "price",
561
"active": true,
562
"billing_scheme": "tiered",
563
"created": 1664263708,
564
"currency": "usd",
565
"custom_unit_amount": null,
566
"livemode": false,
567
"lookup_key": null,
568
"metadata": {},
569
"nickname": "Individual USD",
570
"product": "prod_MIUT2nUscrEWBA",
571
"recurring": {
572
"aggregate_usage": "last_during_period",
573
"interval": "month",
574
"interval_count": 1,
575
"trial_period_days": null,
576
"usage_type": "metered"
577
},
578
"tax_behavior": "inclusive",
579
"tiers_mode": "graduated",
580
"transform_quantity": null,
581
"type": "recurring",
582
"unit_amount": null,
583
"unit_amount_decimal": null
584
},
585
"proration": false,
586
"proration_details": {
587
"credited_items": null
588
},
589
"quantity": 0,
590
"subscription": "sub_1M4XovAyBDPbWrhaCnn4gigv",
591
"subscription_item": "si_MoA9zVoSS4gH2G",
592
"tax_amounts": [],
593
"tax_rates": [],
594
"type": "subscription",
595
"unit_amount_excluding_tax": null
596
}
597
],
598
"has_more": false,
599
"total_count": 4,
600
"url": "/v1/invoices/in_1MA0RBAyBDPbWrhabNdJIuhl/lines"
601
},
602
"livemode": false,
603
"metadata": {},
604
"next_payment_attempt": null,
605
"number": "DF67D6F2-0037",
606
"on_behalf_of": null,
607
"paid": true,
608
"paid_out_of_band": false,
609
"payment_intent": "pi_3MA1YjAyBDPbWrha1Tb6pdTW",
610
"payment_settings": {
611
"default_mandate": null,
612
"payment_method_options": null,
613
"payment_method_types": [
614
"card",
615
"link"
616
]
617
},
618
"period_end": 1669852800,
619
"period_start": 1668552093,
620
"post_payment_credit_notes_amount": 0,
621
"pre_payment_credit_notes_amount": 0,
622
"quote": null,
623
"receipt_number": "2061-6831",
624
"rendering_options": null,
625
"starting_balance": 0,
626
"statement_descriptor": null,
627
"status": "paid",
628
"status_transitions": {
629
"finalized_at": 1669858049,
630
"marked_uncollectible_at": null,
631
"paid_at": 1669948594,
632
"voided_at": null
633
},
634
"subscription": "sub_1M4XovAyBDPbWrhaCnn4gigv",
635
"subtotal": 1012,
636
"subtotal_excluding_tax": 1012,
637
"tax": null,
638
"test_clock": null,
639
"total": 1012,
640
"total_discount_amounts": [],
641
"total_excluding_tax": 1012,
642
"total_tax_amounts": [],
643
"transfer_data": null,
644
"webhooks_delivered_at": 1669853737
645
}`
646
647