Skip to content

Commit f4acfbf

Browse files
author
Laurie T. Malau
committed
[usage] Implement CreateStripeSubscription
1 parent 3ac8f43 commit f4acfbf

File tree

9 files changed

+253
-81
lines changed

9 files changed

+253
-81
lines changed

components/server/ee/src/workspace/gitpod-server-impl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2203,9 +2203,13 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
22032203
throw new Error(`No Stripe customer profile for '${attributionId}'`);
22042204
}
22052205

2206+
//TODO: feature flag with below
2207+
// await this.billingService.createStripeSubscription({attributionId,customerId,setupIntentId,usageLimit});
2208+
22062209
await this.stripeService.setDefaultPaymentMethodForCustomer(customerId, setupIntentId);
22072210
await this.stripeService.createSubscriptionForCustomer(customerId, attributionId);
22082211

2212+
// TODO maybe: Also move this call to setCostCenter to billingService.createStripeSubscription
22092213
// Creating a cost center for this customer
22102214
const { costCenter } = await this.usageService.setCostCenter({
22112215
costCenter: {

components/usage-api/go/v1/billing.pb.go

Lines changed: 83 additions & 73 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/usage-api/typescript/src/usage/v1/billing.pb.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface CreateStripeCustomerResponse {
5959

6060
export interface CreateStripeSubscriptionRequest {
6161
attributionId: string;
62+
customerId: string;
6263
setupIntentId: string;
6364
usageLimit: number;
6465
}
@@ -623,19 +624,22 @@ export const CreateStripeCustomerResponse = {
623624
};
624625

625626
function createBaseCreateStripeSubscriptionRequest(): CreateStripeSubscriptionRequest {
626-
return { attributionId: "", setupIntentId: "", usageLimit: 0 };
627+
return { attributionId: "", customerId: "", setupIntentId: "", usageLimit: 0 };
627628
}
628629

629630
export const CreateStripeSubscriptionRequest = {
630631
encode(message: CreateStripeSubscriptionRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
631632
if (message.attributionId !== "") {
632633
writer.uint32(10).string(message.attributionId);
633634
}
635+
if (message.customerId !== "") {
636+
writer.uint32(18).string(message.customerId);
637+
}
634638
if (message.setupIntentId !== "") {
635-
writer.uint32(18).string(message.setupIntentId);
639+
writer.uint32(26).string(message.setupIntentId);
636640
}
637641
if (message.usageLimit !== 0) {
638-
writer.uint32(24).int64(message.usageLimit);
642+
writer.uint32(32).int64(message.usageLimit);
639643
}
640644
return writer;
641645
},
@@ -651,9 +655,12 @@ export const CreateStripeSubscriptionRequest = {
651655
message.attributionId = reader.string();
652656
break;
653657
case 2:
654-
message.setupIntentId = reader.string();
658+
message.customerId = reader.string();
655659
break;
656660
case 3:
661+
message.setupIntentId = reader.string();
662+
break;
663+
case 4:
657664
message.usageLimit = longToNumber(reader.int64() as Long);
658665
break;
659666
default:
@@ -667,6 +674,7 @@ export const CreateStripeSubscriptionRequest = {
667674
fromJSON(object: any): CreateStripeSubscriptionRequest {
668675
return {
669676
attributionId: isSet(object.attributionId) ? String(object.attributionId) : "",
677+
customerId: isSet(object.customerId) ? String(object.customerId) : "",
670678
setupIntentId: isSet(object.setupIntentId) ? String(object.setupIntentId) : "",
671679
usageLimit: isSet(object.usageLimit) ? Number(object.usageLimit) : 0,
672680
};
@@ -675,6 +683,7 @@ export const CreateStripeSubscriptionRequest = {
675683
toJSON(message: CreateStripeSubscriptionRequest): unknown {
676684
const obj: any = {};
677685
message.attributionId !== undefined && (obj.attributionId = message.attributionId);
686+
message.customerId !== undefined && (obj.customerId = message.customerId);
678687
message.setupIntentId !== undefined && (obj.setupIntentId = message.setupIntentId);
679688
message.usageLimit !== undefined && (obj.usageLimit = Math.round(message.usageLimit));
680689
return obj;
@@ -683,6 +692,7 @@ export const CreateStripeSubscriptionRequest = {
683692
fromPartial(object: DeepPartial<CreateStripeSubscriptionRequest>): CreateStripeSubscriptionRequest {
684693
const message = createBaseCreateStripeSubscriptionRequest();
685694
message.attributionId = object.attributionId ?? "";
695+
message.customerId = object.customerId ?? "";
686696
message.setupIntentId = object.setupIntentId ?? "";
687697
message.usageLimit = object.usageLimit ?? 0;
688698
return message;

components/usage-api/usage/v1/billing.proto

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ message CreateStripeCustomerResponse {
7878

7979
message CreateStripeSubscriptionRequest {
8080
string attribution_id = 1;
81-
string setup_intent_id = 2;
82-
int64 usage_limit = 3;
81+
string customer_id = 2;
82+
string setup_intent_id = 3;
83+
int64 usage_limit = 4;
8384
}
8485

8586
message CreateStripeSubscriptionResponse {

components/usage/pkg/apiv1/billing.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,30 @@ import (
2323
"gorm.io/gorm"
2424
)
2525

26-
func NewBillingService(stripeClient *stripe.Client, conn *gorm.DB, ccManager *db.CostCenterManager) *BillingService {
26+
type PriceConfig struct {
27+
EUR string `json:"eur"`
28+
USD string `json:"usd"`
29+
}
30+
31+
type StripePrices struct {
32+
IndividualUsagePriceIDs PriceConfig `json:"individualUsagePriceIds"`
33+
TeamUsagePriceIDs PriceConfig `json:"teamUsagePriceIds"`
34+
}
35+
36+
func NewBillingService(stripeClient *stripe.Client, conn *gorm.DB, ccManager *db.CostCenterManager, stripePrices StripePrices) *BillingService {
2737
return &BillingService{
2838
stripeClient: stripeClient,
2939
conn: conn,
3040
ccManager: ccManager,
41+
stripePrices: stripePrices,
3142
}
3243
}
3344

3445
type BillingService struct {
3546
conn *gorm.DB
3647
stripeClient *stripe.Client
3748
ccManager *db.CostCenterManager
49+
stripePrices StripePrices
3850

3951
v1.UnimplementedBillingServiceServer
4052
}
@@ -165,6 +177,61 @@ func (s *BillingService) CreateStripeCustomer(ctx context.Context, req *v1.Creat
165177
}, nil
166178
}
167179

180+
func (s *BillingService) CreateStripeSubscription(ctx context.Context, req *v1.CreateStripeSubscriptionRequest) (*v1.CreateStripeSubscriptionResponse, error) {
181+
attributionID, err := db.ParseAttributionID(req.GetAttributionId())
182+
if err != nil {
183+
return nil, status.Errorf(codes.InvalidArgument, "Invalid attribution ID %s", attributionID)
184+
}
185+
186+
if req.CustomerId == "" {
187+
return nil, status.Error(codes.InvalidArgument, "Invalid customer ID")
188+
}
189+
190+
_, err = s.stripeClient.SetDefaultPaymentForCustomer(ctx, req.CustomerId, req.SetupIntentId)
191+
if err != nil {
192+
return nil, fmt.Errorf("Failed to set default payment for customer ID %s", req.CustomerId)
193+
}
194+
195+
stripeCustomer, err := s.stripeClient.GetCustomer(ctx, req.CustomerId)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
priceIdentifier := getPriceIdentifier(attributionID, stripeCustomer, s)
201+
202+
subscription, err := s.stripeClient.CreateSubscription(ctx, req.CustomerId, priceIdentifier)
203+
if err != nil {
204+
return nil, status.Errorf(codes.Aborted, "Failed to create subscription with customer ID %s", req.CustomerId)
205+
}
206+
207+
return &v1.CreateStripeSubscriptionResponse{
208+
Subscription: &v1.StripeSubscription{
209+
Id: subscription.ID,
210+
},
211+
}, nil
212+
}
213+
214+
func getPriceIdentifier(attributionID db.AttributionID, stripeCustomer *stripe_api.Customer, s *BillingService) string {
215+
priceIdentifier := ""
216+
if attributionID.IsEntity("team") {
217+
if stripeCustomer.Currency == "EUR" {
218+
priceIdentifier = s.stripePrices.TeamUsagePriceIDs.EUR
219+
}
220+
if stripeCustomer.Currency == "USD" {
221+
priceIdentifier = s.stripePrices.TeamUsagePriceIDs.USD
222+
}
223+
}
224+
if attributionID.IsEntity("user") {
225+
if stripeCustomer.Currency == "EUR" {
226+
priceIdentifier = s.stripePrices.IndividualUsagePriceIDs.EUR
227+
}
228+
if stripeCustomer.Currency == "USD" {
229+
priceIdentifier = s.stripePrices.IndividualUsagePriceIDs.USD
230+
}
231+
}
232+
return priceIdentifier
233+
}
234+
168235
func (s *BillingService) ReconcileInvoices(ctx context.Context, in *v1.ReconcileInvoicesRequest) (*v1.ReconcileInvoicesResponse, error) {
169236
balances, err := db.ListBalance(ctx, s.conn)
170237
if err != nil {

components/usage/pkg/server/server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type Config struct {
3838
Server *baseserver.Configuration `json:"server,omitempty"`
3939

4040
DefaultSpendingLimit db.DefaultSpendingLimit `json:"defaultSpendingLimit"`
41+
42+
StripePrices apiv1.StripePrices `json:"stripePrices"`
4143
}
4244

4345
func Start(cfg Config, version string) error {
@@ -158,7 +160,7 @@ func registerGRPCServices(srv *baseserver.Server, conn *gorm.DB, stripeClient *s
158160
if stripeClient == nil {
159161
v1.RegisterBillingServiceServer(srv.GRPC(), &apiv1.BillingServiceNoop{})
160162
} else {
161-
v1.RegisterBillingServiceServer(srv.GRPC(), apiv1.NewBillingService(stripeClient, conn, ccManager))
163+
v1.RegisterBillingServiceServer(srv.GRPC(), apiv1.NewBillingService(stripeClient, conn, ccManager, cfg.StripePrices))
162164
}
163165
return nil
164166
}

components/usage/pkg/stripe/stripe.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,62 @@ func (c *Client) GetSubscriptionWithCustomer(ctx context.Context, subscriptionID
320320
return subscription, nil
321321
}
322322

323+
func (c *Client) CreateSubscription(ctx context.Context, customerID string, priceID string) (*stripe.Subscription, error) {
324+
if customerID == "" {
325+
return nil, fmt.Errorf("no customerID specified")
326+
}
327+
if priceID == "" {
328+
return nil, fmt.Errorf("no priceID specified")
329+
}
330+
331+
params := &stripe.SubscriptionParams{
332+
Customer: &customerID,
333+
Items: []*stripe.SubscriptionItemsParams{
334+
{
335+
Price: &priceID,
336+
},
337+
},
338+
}
339+
340+
subscription, error := c.sc.Subscriptions.New(params)
341+
if error != nil {
342+
return nil, fmt.Errorf("failed to get subscription with customer ID %s", customerID)
343+
}
344+
345+
return subscription, error
346+
}
347+
348+
func (c *Client) SetDefaultPaymentForCustomer(ctx context.Context, customerID string, setupIntentId string) (*stripe.Customer, error) {
349+
if customerID == "" {
350+
return nil, fmt.Errorf("no customerID specified")
351+
}
352+
353+
if setupIntentId == "" {
354+
return nil, fmt.Errorf("no setupIntentID specified")
355+
}
356+
357+
setupIntent, err := c.sc.SetupIntents.Get(setupIntentId, &stripe.SetupIntentParams{
358+
Params: stripe.Params{
359+
Context: ctx,
360+
},
361+
})
362+
if err != nil {
363+
return nil, fmt.Errorf("Failed to retrieve setup intent with id %s", setupIntentId)
364+
}
365+
366+
paymentMethod, err := c.sc.PaymentMethods.Attach(setupIntent.PaymentMethod.ID, &stripe.PaymentMethodAttachParams{Customer: &customerID})
367+
if err != nil {
368+
return nil, fmt.Errorf("Failed to attach payment method to setup intent ID %s", setupIntentId)
369+
}
370+
371+
customer, _ := c.sc.Customers.Update(customerID, &stripe.CustomerParams{InvoiceSettings: &stripe.CustomerInvoiceSettingsParams{DefaultPaymentMethod: &paymentMethod.ID}, Address: &stripe.AddressParams{Line1: &paymentMethod.BillingDetails.Address.Line1, Country: &paymentMethod.BillingDetails.Address.Country}})
372+
if err != nil {
373+
return nil, fmt.Errorf("Failed to update customer with id %s", customerID)
374+
}
375+
376+
return customer, nil
377+
}
378+
323379
func GetAttributionID(ctx context.Context, customer *stripe.Customer) (db.AttributionID, error) {
324380
if customer == nil {
325381
log.Error("No customer information available for invoice.")

install/installer/pkg/components/usage/configmap.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88

99
"github.com/gitpod-io/gitpod/common-go/baseserver"
10+
"github.com/gitpod-io/gitpod/usage/pkg/apiv1"
1011
"github.com/gitpod-io/gitpod/usage/pkg/db"
1112
"github.com/gitpod-io/gitpod/usage/pkg/server"
1213

@@ -33,6 +34,17 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
3334
ForUsers: 1_000_000_000,
3435
MinForUsersOnStripe: 0,
3536
},
37+
// TODO: Remove hardcoded IDs
38+
StripePrices: apiv1.StripePrices{
39+
IndividualUsagePriceIDs: apiv1.PriceConfig{
40+
EUR: "price_1LmY7GAyBDPbWrhaf8Lav5yd",
41+
USD: "price_1LmY86AyBDPbWrhahYRNNSK1",
42+
},
43+
TeamUsagePriceIDs: apiv1.PriceConfig{
44+
EUR: "price_1LiJKtAyBDPbWrhaJ2rUVEBb",
45+
USD: "price_1LiJLDAyBDPbWrhaOeWsP7Yr",
46+
},
47+
},
3648
}
3749
expConfig := getExperimentalConfig(ctx)
3850

install/installer/pkg/components/usage/configmap_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ func TestConfigMap_ContainsSchedule(t *testing.T) {
2929
"forTeams": 1000000000,
3030
"minForUsersOnStripe": 0
3131
},
32+
"stripePrices": {
33+
"individualUsagePriceIds": {
34+
"eur": "price_1LmY7GAyBDPbWrhaf8Lav5yd",
35+
"usd": "price_1LmY86AyBDPbWrhahYRNNSK1"
36+
},
37+
"teamUsagePriceIds": {
38+
"eur": "price_1LiJKtAyBDPbWrhaJ2rUVEBb",
39+
"usd": "price_1LiJLDAyBDPbWrhaOeWsP7Yr"
40+
}
41+
},
3242
"server": {
3343
"services": {
3444
"grpc": {

0 commit comments

Comments
 (0)