Skip to content

Commit 8fd8122

Browse files
committed
[usage] Implement charge dispute handling
1 parent ac9ca55 commit 8fd8122

File tree

4 files changed

+97
-0
lines changed

4 files changed

+97
-0
lines changed

components/usage/BUILD.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ packages:
99
- components/gitpod-db/go:lib
1010
- components/common-go:lib
1111
- components/usage-api/go:lib
12+
- components/public-api/go:lib
1213
- components/content-service-api/go:lib
1314
- components/gitpod-db/go:init-testdb
1415
env:
@@ -24,6 +25,7 @@ packages:
2425
- components/gitpod-db/go:lib
2526
- components/common-go:lib
2627
- components/usage-api/go:lib
28+
- components/public-api/go:lib
2729
- components/content-service-api/go:lib
2830
- components/gitpod-db/go:init-testdb
2931
srcs:

components/usage/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ replace github.com/gitpod-io/gitpod/components/gitpod-db/go => ../gitpod-db/go /
6767

6868
replace github.com/gitpod-io/gitpod/common-go => ../common-go // leeway
6969

70+
replace github.com/gitpod-io/gitpod/components/public-api/go => ../public-api/go // leeway
71+
7072
replace github.com/gitpod-io/gitpod/content-service/api => ../content-service-api/go // leeway
7173

7274
replace github.com/gitpod-io/gitpod/usage-api => ../usage-api/go // leeway

components/usage/pkg/apiv1/billing.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ import (
1212
"math"
1313
"time"
1414

15+
"github.com/bufbuild/connect-go"
1516
"github.com/gitpod-io/gitpod/common-go/log"
1617
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"
1720
v1 "github.com/gitpod-io/gitpod/usage-api/v1"
1821
"github.com/gitpod-io/gitpod/usage/pkg/stripe"
1922
"github.com/google/uuid"
@@ -38,6 +41,8 @@ type BillingService struct {
3841
ccManager *db.CostCenterManager
3942
stripePrices stripe.StripePrices
4043

44+
teamsService v1connect.TeamsServiceClient
45+
4146
v1.UnimplementedBillingServiceServer
4247
}
4348

@@ -408,6 +413,73 @@ func (s *BillingService) CancelSubscription(ctx context.Context, in *v1.CancelSu
408413
return &v1.CancelSubscriptionResponse{}, nil
409414
}
410415

416+
func (s *BillingService) OnChargeDispute(ctx context.Context, req *v1.OnChargeDisputeRequest) (*v1.OnChargeDisputeResponse, error) {
417+
if req.DisputeId == "" {
418+
return nil, status.Errorf(codes.InvalidArgument, "dispute ID is required")
419+
}
420+
421+
logger := log.WithContext(ctx).WithField("disputeId", req.DisputeId)
422+
423+
dispute, err := s.stripeClient.GetDispute(ctx, req.DisputeId)
424+
if err != nil {
425+
return nil, status.Errorf(codes.Internal, "failed to retrieve dispute ID %s from stripe", req.DisputeId)
426+
}
427+
428+
if dispute.PaymentIntent == nil || dispute.PaymentIntent.Customer == nil {
429+
return nil, status.Errorf(codes.Internal, "dispute did not contain customer of payment intent in expanded fields")
430+
}
431+
432+
customer := dispute.PaymentIntent.Customer
433+
logger = logger.WithField("customerId", customer.ID)
434+
435+
attributionIDValue, ok := customer.Metadata[stripe.AttributionIDMetadataKey]
436+
if !ok {
437+
return nil, status.Errorf(codes.Internal, "Customer %s object did not contain attribution ID in metadata", customer.ID)
438+
}
439+
440+
logger = logger.WithField("attributionId", attributionIDValue)
441+
442+
attributionID, err := db.ParseAttributionID(attributionIDValue)
443+
if err != nil {
444+
log.WithError(err).Errorf("Failed to parse attribution ID from customer metadata.")
445+
return nil, status.Errorf(codes.Internal, "failed to parse attribution ID from customer metadata")
446+
}
447+
448+
var userIDsToBlock []string
449+
entity, id := attributionID.Values()
450+
switch entity {
451+
case db.AttributionEntity_User:
452+
// legacy for cases where we've not migrated the user to a team
453+
// because we attribute to the user directly, we can just block the user directly
454+
userIDsToBlock = append(userIDsToBlock, id)
455+
456+
case db.AttributionEntity_Team:
457+
team, err := s.teamsService.GetTeam(ctx, connect.NewRequest(&experimental_v1.GetTeamRequest{
458+
TeamId: id,
459+
}))
460+
if err != nil {
461+
return nil, status.Errorf(codes.Internal, "failed to lookup team details for team ID: %s", id)
462+
}
463+
464+
for _, member := range team.Msg.GetTeam().GetMembers() {
465+
if member.GetRole() != experimental_v1.TeamRole_TEAM_ROLE_OWNER {
466+
continue
467+
}
468+
userIDsToBlock = append(userIDsToBlock, member.GetUserId())
469+
}
470+
471+
default:
472+
return nil, status.Errorf(codes.Internal, "unknown attribution entity for %s", attributionIDValue)
473+
}
474+
475+
logger = logger.WithField("teamOwners", userIDsToBlock)
476+
477+
logger.Infof("Identified %d users to block based on charge dispute", len(userIDsToBlock))
478+
// TODO: actually block users
479+
480+
return &v1.OnChargeDisputeResponse{}, nil
481+
}
482+
411483
func (s *BillingService) getPriceId(ctx context.Context, attributionId string) string {
412484
defaultPriceId := s.stripePrices.TeamUsagePriceIDs.USD
413485
attributionID, err := db.ParseAttributionID(attributionId)

components/usage/pkg/stripe/stripe.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,27 @@ func (c *Client) SetDefaultPaymentForCustomer(ctx context.Context, customerID st
424424
return customer, nil
425425
}
426426

427+
func (c *Client) GetDispute(ctx context.Context, disputeID string) (dispute *stripe.Dispute, err error) {
428+
now := time.Now()
429+
reportStripeRequestStarted("dispute_get")
430+
defer func() {
431+
reportStripeRequestCompleted("dispute_get", err, time.Since(now))
432+
}()
433+
params := &stripe.DisputeParams{
434+
Params: stripe.Params{
435+
Context: ctx,
436+
},
437+
}
438+
params.AddExpand("payment_intent.customer")
439+
440+
dispute, err = c.sc.Disputes.Get(disputeID, params)
441+
if err != nil {
442+
return nil, fmt.Errorf("failed to retrieve dispute ID: %s", disputeID)
443+
}
444+
445+
return dispute, nil
446+
}
447+
427448
type PaymentHoldResult string
428449

429450
// List of values that PaymentIntentStatus can take

0 commit comments

Comments
 (0)