diff --git a/components/server/ee/src/billing/billing-service.ts b/components/server/ee/src/billing/billing-service.ts index 1294fa8844a79e..e82753e1260167 100644 --- a/components/server/ee/src/billing/billing-service.ts +++ b/components/server/ee/src/billing/billing-service.ts @@ -7,9 +7,14 @@ import { CostCenterDB } from "@gitpod/gitpod-db/lib"; import { User } from "@gitpod/gitpod-protocol"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; -import { BillableSession, BillableSessionRequest, SortOrder } from "@gitpod/gitpod-protocol/lib/usage"; +import { BillableSession, BillableSessionRequest } from "@gitpod/gitpod-protocol/lib/usage"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; -import { CachingUsageServiceClientProvider, UsageService } from "@gitpod/usage-api/lib/usage/v1/sugar"; +import { GetLatestInvoiceResponse } from "@gitpod/usage-api/lib/usage/v1/billing_pb"; +import { + CachingUsageServiceClientProvider, + UsageService, + CachingBillingServiceClientProvider, +} from "@gitpod/usage-api/lib/usage/v1/sugar"; import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; import { inject, injectable } from "inversify"; import { UserService } from "../../../src/user/user-service"; @@ -26,6 +31,8 @@ export class BillingService { @inject(CostCenterDB) protected readonly costCenterDB: CostCenterDB; @inject(CachingUsageServiceClientProvider) protected readonly usageServiceClientProvider: CachingUsageServiceClientProvider; + @inject(CachingBillingServiceClientProvider) + protected readonly billingServiceClientProvider: CachingBillingServiceClientProvider; async checkSpendingLimitReached(user: User): Promise { const attributionId = await this.userService.getWorkspaceUsageAttributionId(user); @@ -40,23 +47,27 @@ export class BillingService { }; } - const allSessions = await this.listBilledUsage({ - attributionId: AttributionId.render(attributionId), - startedTimeOrder: SortOrder.Descending, - }); - const totalUsage = allSessions.map((s) => s.credits).reduce((a, b) => a + b, 0); - if (totalUsage >= costCenter.spendingLimit) { - return { - reached: true, - attributionId, - }; - } else if (totalUsage > costCenter.spendingLimit * 0.8) { - return { - reached: false, - almostReached: true, - attributionId, - }; + if (attributionId.kind === "team") { + const latestInvoice = await this.getLatestInvoice(attributionId.teamId); + const currentUsage = latestInvoice.getCredits(); + if (currentUsage >= costCenter.spendingLimit) { + return { + reached: true, + attributionId, + }; + } else if (currentUsage > costCenter.spendingLimit * 0.8) { + return { + reached: false, + almostReached: true, + attributionId, + }; + } + } + + if (attributionId.kind === "user") { + // TODO } + return { reached: false, attributionId, @@ -86,4 +97,9 @@ export class BillingService { const sessions = response.getSessionsList().map((s) => UsageService.mapBilledSession(s)); return sessions; } + + async getLatestInvoice(teamId: string): Promise { + const response = await this.billingServiceClientProvider.getDefault().getLatestInvoice(teamId); + return response; + } } diff --git a/components/usage-api/go/v1/billing.pb.go b/components/usage-api/go/v1/billing.pb.go index 2005e39075e3ad..3e6155b34f21ca 100644 --- a/components/usage-api/go/v1/billing.pb.go +++ b/components/usage-api/go/v1/billing.pb.go @@ -181,7 +181,6 @@ type GetLatestInvoiceRequest struct { unknownFields protoimpl.UnknownFields // Types that are assignable to Identifier: - // // *GetLatestInvoiceRequest_TeamId // *GetLatestInvoiceRequest_UserId Identifier isGetLatestInvoiceRequest_Identifier `protobuf_oneof:"identifier"` @@ -264,6 +263,7 @@ type GetLatestInvoiceResponse struct { InvoiceId string `protobuf:"bytes,1,opt,name=invoice_id,json=invoiceId,proto3" json:"invoice_id,omitempty"` Currency string `protobuf:"bytes,2,opt,name=currency,proto3" json:"currency,omitempty"` Amount float64 `protobuf:"fixed64,3,opt,name=amount,proto3" json:"amount,omitempty"` + Credits int64 `protobuf:"varint,4,opt,name=credits,proto3" json:"credits,omitempty"` } func (x *GetLatestInvoiceResponse) Reset() { @@ -319,6 +319,13 @@ func (x *GetLatestInvoiceResponse) GetAmount() float64 { return 0 } +func (x *GetLatestInvoiceResponse) GetCredits() int64 { + if x != nil { + return x.Credits + } + return 0 +} + type FinalizeInvoiceRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -535,62 +542,63 @@ var file_usage_v1_billing_proto_rawDesc = []byte{ 0x48, 0x00, 0x52, 0x06, 0x74, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x22, 0x6d, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, - 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x1d, 0x0a, 0x0a, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, - 0x0a, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x22, 0x37, 0x0a, 0x16, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x49, 0x6e, - 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x49, 0x64, 0x22, 0x19, 0x0a, 0x17, 0x46, - 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x94, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x42, 0x69, - 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x66, - 0x72, 0x6f, 0x6d, 0x12, 0x28, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, - 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x1a, 0x0a, - 0x18, 0x53, 0x65, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x45, 0x0a, 0x06, 0x53, 0x79, 0x73, - 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x55, 0x4e, - 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x59, 0x53, 0x54, 0x45, - 0x4d, 0x5f, 0x43, 0x48, 0x41, 0x52, 0x47, 0x45, 0x42, 0x45, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, - 0x0d, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x53, 0x54, 0x52, 0x49, 0x50, 0x45, 0x10, 0x02, - 0x32, 0xfb, 0x02, 0x0a, 0x0e, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x76, - 0x6f, 0x69, 0x63, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x10, 0x47, 0x65, - 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x21, - 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, - 0x65, 0x73, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x22, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x0f, 0x46, 0x69, 0x6e, 0x61, 0x6c, - 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x20, 0x2e, 0x75, 0x73, 0x61, - 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x49, 0x6e, - 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x75, - 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, - 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x5b, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x53, 0x65, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x2a, - 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, - 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x75, 0x73, - 0x61, 0x67, 0x65, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x69, 0x65, 0x72, 0x22, 0x87, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, + 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, + 0x1a, 0x0a, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x61, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x61, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x22, 0x37, 0x0a, + 0x16, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6e, 0x76, + 0x6f, 0x69, 0x63, 0x65, 0x49, 0x64, 0x22, 0x19, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, + 0x7a, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x94, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, + 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2e, + 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x28, + 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, + 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x42, + 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x45, 0x0a, 0x06, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x12, + 0x0a, 0x0e, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x43, 0x48, 0x41, + 0x52, 0x47, 0x45, 0x42, 0x45, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x59, 0x53, 0x54, + 0x45, 0x4d, 0x5f, 0x53, 0x54, 0x52, 0x49, 0x50, 0x45, 0x10, 0x02, 0x32, 0xfb, 0x02, 0x0a, 0x0e, + 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x55, + 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, + 0x12, 0x1f, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x20, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, + 0x73, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x21, 0x2e, 0x75, 0x73, 0x61, 0x67, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x49, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x75, + 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, + 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x58, 0x0a, 0x0f, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x49, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x20, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x10, + 0x53, 0x65, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x21, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x42, + 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x65, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, + 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2d, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/components/usage-api/typescript/src/usage/v1/billing_pb.d.ts b/components/usage-api/typescript/src/usage/v1/billing_pb.d.ts index 38e41d0600ba71..c4c2cb86c0ace5 100644 --- a/components/usage-api/typescript/src/usage/v1/billing_pb.d.ts +++ b/components/usage-api/typescript/src/usage/v1/billing_pb.d.ts @@ -110,6 +110,8 @@ export class GetLatestInvoiceResponse extends jspb.Message { setCurrency(value: string): GetLatestInvoiceResponse; getAmount(): number; setAmount(value: number): GetLatestInvoiceResponse; + getCredits(): number; + setCredits(value: number): GetLatestInvoiceResponse; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): GetLatestInvoiceResponse.AsObject; @@ -126,6 +128,7 @@ export namespace GetLatestInvoiceResponse { invoiceId: string, currency: string, amount: number, + credits: number, } } diff --git a/components/usage-api/typescript/src/usage/v1/billing_pb.js b/components/usage-api/typescript/src/usage/v1/billing_pb.js index 6e5a40c593c1f6..cc5deec63f0302 100644 --- a/components/usage-api/typescript/src/usage/v1/billing_pb.js +++ b/components/usage-api/typescript/src/usage/v1/billing_pb.js @@ -822,7 +822,8 @@ proto.usage.v1.GetLatestInvoiceResponse.toObject = function(includeInstance, msg var f, obj = { invoiceId: jspb.Message.getFieldWithDefault(msg, 1, ""), currency: jspb.Message.getFieldWithDefault(msg, 2, ""), - amount: jspb.Message.getFloatingPointFieldWithDefault(msg, 3, 0.0) + amount: jspb.Message.getFloatingPointFieldWithDefault(msg, 3, 0.0), + credits: jspb.Message.getFieldWithDefault(msg, 4, 0) }; if (includeInstance) { @@ -871,6 +872,10 @@ proto.usage.v1.GetLatestInvoiceResponse.deserializeBinaryFromReader = function(m var value = /** @type {number} */ (reader.readDouble()); msg.setAmount(value); break; + case 4: + var value = /** @type {number} */ (reader.readInt64()); + msg.setCredits(value); + break; default: reader.skipField(); break; @@ -921,6 +926,13 @@ proto.usage.v1.GetLatestInvoiceResponse.serializeBinaryToWriter = function(messa f ); } + f = message.getCredits(); + if (f !== 0) { + writer.writeInt64( + 4, + f + ); + } }; @@ -978,6 +990,24 @@ proto.usage.v1.GetLatestInvoiceResponse.prototype.setAmount = function(value) { }; +/** + * optional int64 credits = 4; + * @return {number} + */ +proto.usage.v1.GetLatestInvoiceResponse.prototype.getCredits = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.usage.v1.GetLatestInvoiceResponse} returns this + */ +proto.usage.v1.GetLatestInvoiceResponse.prototype.setCredits = function(value) { + return jspb.Message.setProto3IntField(this, 4, value); +}; + + diff --git a/components/usage-api/typescript/src/usage/v1/sugar.ts b/components/usage-api/typescript/src/usage/v1/sugar.ts index 4750c7e5f61999..494a663834f635 100644 --- a/components/usage-api/typescript/src/usage/v1/sugar.ts +++ b/components/usage-api/typescript/src/usage/v1/sugar.ts @@ -10,7 +10,13 @@ import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import * as opentracing from "opentracing"; import { Metadata } from "@grpc/grpc-js"; import { BilledSession, ListBilledUsageRequest, ListBilledUsageResponse } from "./usage_pb"; -import { SetBilledSessionRequest, SetBilledSessionResponse, System } from "./billing_pb"; +import { + GetLatestInvoiceRequest, + GetLatestInvoiceResponse, + SetBilledSessionRequest, + SetBilledSessionResponse, + System, +} from "./billing_pb"; import { injectable, inject, optional } from "inversify"; import { createClientCallMetricsInterceptor, IClientCallMetrics } from "@gitpod/gitpod-protocol/lib/util/grpc"; import * as grpc from "@grpc/grpc-js"; @@ -254,4 +260,20 @@ export class PromisifiedBillingServiceClient { interceptors: this.interceptor, }; } + + public async getLatestInvoice(teamId: string) { + const req = new GetLatestInvoiceRequest(); + req.setTeamId(teamId); + + const response = await new Promise((resolve, reject) => { + this.client.getLatestInvoice(req, (err: grpc.ServiceError | null, response: GetLatestInvoiceResponse) => { + if (err) { + reject(err); + return; + } + resolve(response); + }); + }); + return response; + } } diff --git a/components/usage-api/usage/v1/billing.proto b/components/usage-api/usage/v1/billing.proto index 7ad003f076d9f6..7959d971ce520c 100644 --- a/components/usage-api/usage/v1/billing.proto +++ b/components/usage-api/usage/v1/billing.proto @@ -45,6 +45,7 @@ message GetLatestInvoiceResponse { string invoice_id = 1; string currency = 2; double amount = 3; + int64 credits = 4; } message FinalizeInvoiceRequest { diff --git a/components/usage/pkg/apiv1/billing.go b/components/usage/pkg/apiv1/billing.go index e33c01760a7162..2fba431f9ca4ef 100644 --- a/components/usage/pkg/apiv1/billing.go +++ b/components/usage/pkg/apiv1/billing.go @@ -58,6 +58,21 @@ func (s *BillingService) FinalizeInvoice(ctx context.Context, in *v1.FinalizeInv return &v1.FinalizeInvoiceResponse{}, nil } +func (s *BillingService) GetLatestInvoice(ctx context.Context, in *v1.GetLatestInvoiceRequest) (*v1.GetLatestInvoiceResponse, error) { + invoice, err := s.stripeClient.GetLatestInvoice(ctx, in.GetTeamId()) + if err != nil { + log.Log.WithError(err).Errorf("Failed to fetch upcoming invoice from stripe.") + return nil, status.Errorf(codes.Internal, "failed to fetcht upcoming invoice from stripe") + } + + return &v1.GetLatestInvoiceResponse{ + InvoiceId: invoice.ID, + Currency: invoice.Currency, + Amount: float64(invoice.Amount), + Credits: invoice.Credits, + }, nil +} + func (s *BillingService) creditSummaryForTeams(sessions []*v1.BilledSession) (map[string]int64, error) { creditsPerTeamID := map[string]float64{} diff --git a/components/usage/pkg/apiv1/billing_test.go b/components/usage/pkg/apiv1/billing_test.go index cde6094b5aaff1..5710de769d571b 100644 --- a/components/usage/pkg/apiv1/billing_test.go +++ b/components/usage/pkg/apiv1/billing_test.go @@ -5,7 +5,6 @@ package apiv1 import ( - "context" "testing" "time" @@ -117,11 +116,3 @@ func TestCreditSummaryForTeams(t *testing.T) { }) } } - -func TestFinalizeInvoice(t *testing.T) { - b := BillingService{} - - _, err := b.FinalizeInvoice(context.Background(), &v1.FinalizeInvoiceRequest{}) - - require.NoError(t, err) -} diff --git a/components/usage/pkg/server/server.go b/components/usage/pkg/server/server.go index ce7759e2625ece..c6c09cc65538a9 100644 --- a/components/usage/pkg/server/server.go +++ b/components/usage/pkg/server/server.go @@ -6,11 +6,12 @@ package server import ( "fmt" - grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" "net" "os" "time" + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" diff --git a/components/usage/pkg/stripe/stripe.go b/components/usage/pkg/stripe/stripe.go index 4c11661c8d399d..88556fc3e13944 100644 --- a/components/usage/pkg/stripe/stripe.go +++ b/components/usage/pkg/stripe/stripe.go @@ -50,6 +50,14 @@ type UsageRecord struct { Quantity int64 } +type StripeInvoice struct { + ID string + SubscriptionID string + Amount int64 + Currency string + Credits int64 +} + // UpdateUsage updates teams' Stripe subscriptions with usage data // `usageForTeam` is a map from team name to total workspace seconds used within a billing period. func (c *Client) UpdateUsage(ctx context.Context, creditsPerTeam map[string]int64) error { @@ -122,6 +130,38 @@ func (c *Client) updateUsageForCustomer(ctx context.Context, customer *stripe.Cu }, nil } +// GetLatestInvoice fetches the upcoming invoice for the given team +func (c *Client) GetLatestInvoice(ctx context.Context, teamId string) (*StripeInvoice, error) { + query := fmt.Sprintf("metadata['teamId']:'%s'", teamId) + searchParams := &stripe.CustomerSearchParams{ + SearchParams: stripe.SearchParams{ + Query: query, + Expand: []*string{stripe.String("data.subscriptions")}, + Context: ctx, + }, + } + iter := c.sc.Customers.Search(searchParams) + if !iter.Next() { + return nil, fmt.Errorf("failed to find customer data for team %s", teamId) + } + customer := iter.Customer() + invoiceParams := &stripe.InvoiceParams{ + Customer: stripe.String(customer.ID), + } + invoice, err := c.sc.Invoices.GetNext(invoiceParams) + if err != nil { + return nil, fmt.Errorf("failed to fetch the upcoming invoice for customer %s", customer.ID) + } + + return &StripeInvoice{ + ID: invoice.ID, + SubscriptionID: invoice.Subscription.ID, + Amount: invoice.AmountRemaining, + Currency: string(invoice.Currency), + Credits: invoice.Lines.Data[0].Quantity, + }, nil +} + // queriesForCustomersWithTeamIds constructs Stripe query strings to find the Stripe Customer for each teamId // It returns multiple queries, each being a big disjunction of subclauses so that we can process multiple teamIds in one query. // `clausesPerQuery` is a limit enforced by the Stripe API. diff --git a/components/usage/pkg/stripe/stripe_test.go b/components/usage/pkg/stripe/stripe_test.go index c628e372577a59..5e1a9e6b495da1 100644 --- a/components/usage/pkg/stripe/stripe_test.go +++ b/components/usage/pkg/stripe/stripe_test.go @@ -5,6 +5,7 @@ package stripe import ( + "context" "fmt" "testing" @@ -83,3 +84,18 @@ func TestCustomerQueriesForTeamIds_MultipleQueries(t *testing.T) { }) } } + +func TestGetLatestInvoice(t *testing.T) { + config := ClientConfig{ + SecretKey: "", + } + c, err := New(config) + if err != nil { + t.Error(err) + } + invoice, err := c.GetLatestInvoice(context.Background(), "sub_1LRu1YGadRXm50o3WFaXl2Pg") + if err != nil { + t.Error(err) + } + t.Log(invoice.Credits) +}