diff --git a/components/gitpod-db/src/container-module.ts b/components/gitpod-db/src/container-module.ts index 87bc10af010b83..e63a7e6dd4dc3c 100644 --- a/components/gitpod-db/src/container-module.ts +++ b/components/gitpod-db/src/container-module.ts @@ -67,8 +67,6 @@ import { TypeORMBlockedRepositoryDBImpl } from "./typeorm/blocked-repository-db- import { BlockedRepositoryDB } from "./blocked-repository-db"; import { WebhookEventDB } from "./webhook-event-db"; import { WebhookEventDBImpl } from "./typeorm/webhook-event-db-impl"; -import { CostCenterDB } from "./cost-center-db"; -import { CostCenterDBImpl } from "./typeorm/cost-center-db-impl"; // THE DB container module that contains all DB implementations export const dbContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { @@ -146,9 +144,6 @@ export const dbContainerModule = new ContainerModule((bind, unbind, isBound, reb bind(WebhookEventDBImpl).toSelf().inSingletonScope(); bind(WebhookEventDB).toService(WebhookEventDBImpl); - bind(CostCenterDBImpl).toSelf().inSingletonScope(); - bind(CostCenterDB).toService(CostCenterDBImpl); - // com concerns bind(AccountingDB).to(TypeORMAccountingDBImpl).inSingletonScope(); bind(TransactionalAccountingDBFactory).toFactory((ctx) => { diff --git a/components/gitpod-db/src/cost-center-db.ts b/components/gitpod-db/src/cost-center-db.ts deleted file mode 100644 index 419ba00f530e64..00000000000000 --- a/components/gitpod-db/src/cost-center-db.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2022 Gitpod GmbH. All rights reserved. - * Licensed under the Gitpod Enterprise Source Code License, - * See License.enterprise.txt in the project root folder. - */ - -import { CostCenter } from "@gitpod/gitpod-protocol"; - -export const CostCenterDB = Symbol("CostCenterDB"); -export interface CostCenterDB { - storeEntry(ts: CostCenter): Promise; - findById(id: string): Promise; -} diff --git a/components/gitpod-db/src/index.ts b/components/gitpod-db/src/index.ts index 785de419ccb6d9..a19b888e7e5652 100644 --- a/components/gitpod-db/src/index.ts +++ b/components/gitpod-db/src/index.ts @@ -39,4 +39,3 @@ export * from "./project-db"; export * from "./team-db"; export * from "./installation-admin-db"; export * from "./webhook-event-db"; -export * from "./cost-center-db"; diff --git a/components/gitpod-db/src/typeorm/cost-center-db-impl.ts b/components/gitpod-db/src/typeorm/cost-center-db-impl.ts deleted file mode 100644 index 9c7df6b1421b01..00000000000000 --- a/components/gitpod-db/src/typeorm/cost-center-db-impl.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2022 Gitpod GmbH. All rights reserved. - * Licensed under the Gitpod Enterprise Source Code License, - * See License.enterprise.txt in the project root folder. - */ - -import { injectable, inject } from "inversify"; -import { Repository } from "typeorm"; - -import { CostCenter } from "@gitpod/gitpod-protocol"; - -import { CostCenterDB } from "../cost-center-db"; -import { DBCostCenter } from "./entity/db-cost-center"; -import { TypeORM } from "./typeorm"; - -@injectable() -export class CostCenterDBImpl implements CostCenterDB { - @inject(TypeORM) protected readonly typeORM: TypeORM; - - protected async getEntityManager() { - return (await this.typeORM.getConnection()).manager; - } - - protected async getRepo(): Promise> { - return (await this.getEntityManager()).getRepository(DBCostCenter); - } - - async storeEntry(ts: CostCenter): Promise { - const repo = await this.getRepo(); - await repo.save(ts); - } - - async findById(id: string): Promise { - const repo = await this.getRepo(); - return repo.findOne(id); - } -} diff --git a/components/gitpod-db/src/typeorm/entity/db-cost-center.ts b/components/gitpod-db/src/typeorm/entity/db-cost-center.ts deleted file mode 100644 index 9457972eb9c879..00000000000000 --- a/components/gitpod-db/src/typeorm/entity/db-cost-center.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2022 Gitpod GmbH. All rights reserved. - * Licensed under the Gitpod Enterprise Source Code License, - * See License.enterprise.txt in the project root folder. - */ - -import { Entity, Column, PrimaryColumn } from "typeorm"; -import { CostCenter } from "@gitpod/gitpod-protocol"; - -@Entity() -// on DB but not Typeorm: @Index("ind_lastModified", ["_lastModified"]) // DBSync -export class DBCostCenter implements CostCenter { - @PrimaryColumn() - id: string; - - @Column() - spendingLimit: number; - - // This column triggers the db-sync deletion mechanism. It's not intended for public consumption. - @Column() - deleted: boolean; -} diff --git a/components/gitpod-db/src/typeorm/migration/1662639748206-CostCenterPaymentStrategy.ts b/components/gitpod-db/src/typeorm/migration/1662639748206-CostCenterPaymentStrategy.ts new file mode 100644 index 00000000000000..16bda8da064b63 --- /dev/null +++ b/components/gitpod-db/src/typeorm/migration/1662639748206-CostCenterPaymentStrategy.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { MigrationInterface, QueryRunner } from "typeorm"; +import { columnExists } from "./helper/helper"; + +const D_B_COST_CENTER = "d_b_cost_center"; +const COL_CREATION_TIME = "creationTime"; +const COL_BILLING_STRATEGY = "billingStrategy"; + +export class CostCenterPaymentStrategy1662639748206 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + if (!(await columnExists(queryRunner, D_B_COST_CENTER, COL_CREATION_TIME))) { + await queryRunner.query( + `ALTER TABLE ${D_B_COST_CENTER} ADD COLUMN ${COL_CREATION_TIME} varchar(30) NOT NULL, ALGORITHM=INPLACE, LOCK=NONE `, + ); + await queryRunner.query( + `ALTER TABLE ${D_B_COST_CENTER} ADD COLUMN ${COL_BILLING_STRATEGY} varchar(255) NOT NULL DEFAULT 'other', ALGORITHM=INPLACE, LOCK=NONE `, + ); + await queryRunner.query( + `ALTER TABLE ${D_B_COST_CENTER} DROP PRIMARY KEY, ADD PRIMARY KEY(id, ${COL_CREATION_TIME}), ALGORITHM=INPLACE, LOCK=NONE `, + ); + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index df8765be1db48a..71b279d38975ec 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -8,6 +8,7 @@ import { WorkspaceInstance, PortVisibility } from "./workspace-instance"; import { RoleOrPermission } from "./permission"; import { Project } from "./teams-projects-protocol"; import { createHash } from "crypto"; +import { AttributionId } from "./attribution"; export interface UserInfo { name?: string; @@ -1503,10 +1504,12 @@ export interface Terms { readonly formElements?: object; } +export type BillingStrategy = "other" | "stripe"; export interface CostCenter { - readonly id: string; + readonly id: AttributionId; /** * Unit: credits */ spendingLimit: number; + billingStrategy: BillingStrategy; } diff --git a/components/server/ee/src/billing/billing-service.ts b/components/server/ee/src/billing/billing-service.ts index 7a5bdff61b0271..19a37c5edbf418 100644 --- a/components/server/ee/src/billing/billing-service.ts +++ b/components/server/ee/src/billing/billing-service.ts @@ -4,7 +4,6 @@ * See License.enterprise.txt in the project root folder. */ -import { CostCenterDB } from "@gitpod/gitpod-db/lib"; import { User } from "@gitpod/gitpod-protocol"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; @@ -25,7 +24,6 @@ export interface UsageLimitReachedResult { @injectable() export class BillingService { @inject(UserService) protected readonly userService: UserService; - @inject(CostCenterDB) protected readonly costCenterDB: CostCenterDB; @inject(CachingUsageServiceClientProvider) protected readonly usageServiceClientProvider: CachingUsageServiceClientProvider; @inject(CachingBillingServiceClientProvider) @@ -33,11 +31,11 @@ export class BillingService { async checkUsageLimitReached(user: User): Promise { const attributionId = await this.userService.getWorkspaceUsageAttributionId(user); - const costCenter = await this.costCenterDB.findById(AttributionId.render(attributionId)); + const costCenter = await this.usageServiceClientProvider.getDefault().getCostCenter(attributionId); if (!costCenter) { const err = new Error("No CostCenter found"); log.error({ userId: user.id }, err.message, err, { attributionId }); - // Technially we do not have any spending limit set, yet. But sending users down the "reached" path will fix this issues as well. + // Technically we do not have any spending limit set, yet. But sending users down the "reached" path will fix this issues as well. return { reached: true, attributionId, diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 4eee094bf9906b..597e03140761a8 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -64,7 +64,7 @@ import { LicenseKeySource } from "@gitpod/licensor/lib"; import { Feature } from "@gitpod/licensor/lib/api"; import { LicenseValidationResult, LicenseFeature } from "@gitpod/gitpod-protocol/lib/license-protocol"; import { PrebuildManager } from "../prebuilds/prebuild-manager"; -import { CostCenterDB, LicenseDB } from "@gitpod/gitpod-db/lib"; +import { LicenseDB } from "@gitpod/gitpod-db/lib"; import { GuardedCostCenter, ResourceAccessGuard, ResourceAccessOp } from "../../../src/auth/resource-access"; import { AccountStatement, CreditAlert, Subscription } from "@gitpod/gitpod-protocol/lib/accounting-protocol"; import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositories-protocol"; @@ -156,7 +156,6 @@ export class GitpodServerEEImpl extends GitpodServerImpl { @inject(CachingUsageServiceClientProvider) protected readonly usageServiceClientProvider: CachingUsageServiceClientProvider; - @inject(CostCenterDB) protected readonly costCenterDB: CostCenterDB; @inject(EntitlementService) protected readonly entitlementService: EntitlementService; @inject(BillingModes) protected readonly billingModes: BillingModes; @@ -2117,12 +2116,13 @@ export class GitpodServerEEImpl extends GitpodServerImpl { await this.stripeService.setDefaultPaymentMethodForCustomer(customer, setupIntentId); await this.stripeService.createSubscriptionForCustomer(customer); - const attributionId = AttributionId.render({ kind: "team", teamId }); + const attributionId: AttributionId = { kind: "team", teamId }; // Creating a cost center for this team - await this.costCenterDB.storeEntry({ + await this.usageServiceClientProvider.getDefault().setCostCenter({ id: attributionId, spendingLimit: this.defaultSpendingLimit, + billingStrategy: "stripe", }); } catch (error) { log.error(`Failed to subscribe team '${teamId}' to Stripe`, error); @@ -2151,10 +2151,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const team = await this.guardTeamOperation(teamId, "get"); await this.ensureStripeApiIsAllowed({ team }); - const attributionId = AttributionId.render({ kind: "team", teamId }); + const attributionId: AttributionId = { kind: "team", teamId }; await this.guardCostCenterAccess(ctx, user.id, attributionId, "get"); - const costCenter = await this.costCenterDB.findById(attributionId); + const costCenter = await this.usageServiceClientProvider.getDefault().getCostCenter(attributionId); if (costCenter) { return costCenter.spendingLimit; } @@ -2168,12 +2168,14 @@ export class GitpodServerEEImpl extends GitpodServerImpl { if (typeof usageLimit !== "number" || usageLimit < 0) { throw new ResponseError(ErrorCodes.BAD_REQUEST, "Unexpected `usageLimit` value."); } - const attributionId = AttributionId.render({ kind: "team", teamId }); + const attributionId: AttributionId = { kind: "team", teamId }; await this.guardCostCenterAccess(ctx, user.id, attributionId, "update"); - await this.costCenterDB.storeEntry({ - id: AttributionId.render({ kind: "team", teamId }), + const costCenter = await this.usageServiceClientProvider.getDefault().getCostCenter(attributionId); + await this.usageServiceClientProvider.getDefault().setCostCenter({ + id: attributionId, spendingLimit: usageLimit, + billingStrategy: costCenter?.billingStrategy || "other", }); } @@ -2184,23 +2186,25 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const billingMode = await this.billingModes.getBillingModeForUser(user, new Date()); if (billingMode.mode === "usage-based") { const limit = await this.billingService.checkUsageLimitReached(user); - const costCenter = await this.costCenterDB.findById(AttributionId.render(limit.attributionId)); - if (costCenter) { - if (limit.reached) { - result.unshift("The usage limit is reached."); - } else if (limit.almostReached) { - result.unshift("The usage limit is almost reached."); - } + if (limit.reached) { + result.unshift("The usage limit is reached."); + } else if (limit.almostReached) { + result.unshift("The usage limit is almost reached."); } } return result; } async listUsage(ctx: TraceContext, req: ListUsageRequest): Promise { - const { attributionId, from, to } = req; + const { from, to } = req; + const attributionId = AttributionId.parse(req.attributionId); + if (!attributionId) { + throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "Bad attribution ID", { + attributionId: req.attributionId, + }); + } traceAPIParams(ctx, { attributionId }); const user = this.checkAndBlockUser("listUsage"); - await this.guardCostCenterAccess(ctx, user.id, attributionId, "get"); const timestampFrom = from ? Timestamp.fromDate(new Date(from)) : undefined; @@ -2208,7 +2212,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const usageClient = this.usageServiceClientProvider.getDefault(); const request = new usage_grpc.ListUsageRequest(); - request.setAttributionId(attributionId); + request.setAttributionId(AttributionId.render(attributionId)); request.setFrom(timestampFrom); if (to) { request.setTo(timestampTo); @@ -2253,23 +2257,15 @@ export class GitpodServerEEImpl extends GitpodServerImpl { protected async guardCostCenterAccess( ctx: TraceContext, userId: string, - attributionId: string, + attributionId: AttributionId, operation: ResourceAccessOp, ): Promise { traceAPIParams(ctx, { userId, attributionId }); - // TODO(gpl) We need a CostCenter entity (with a strong connection to Team or User) to properly to authorize access to these reports - // const costCenter = await this.costCenterDB.findByAttributionId(attributionId); - const parsedId = AttributionId.parse(attributionId); - if (parsedId === undefined) { - log.warn({ userId }, "Unable to parse attributionId", { attributionId }); - throw new ResponseError(ErrorCodes.BAD_REQUEST, "Unable to parse attributionId"); - } - let owner: GuardedCostCenter["owner"]; - switch (parsedId.kind) { + switch (attributionId.kind) { case "team": - const team = await this.teamDB.findTeamById(parsedId.teamId); + const team = await this.teamDB.findTeamById(attributionId.teamId); if (!team) { throw new ResponseError(ErrorCodes.NOT_FOUND, "Team not found"); } diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index 28a32c0b76b41d..2028dadd1bc98f 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -17,7 +17,7 @@ import { WORKSPACE_TIMEOUT_EXTENDED_ALT, Team, } from "@gitpod/gitpod-protocol"; -import { CostCenterDB, ProjectDB, TeamDB, TermsAcceptanceDB, UserDB } from "@gitpod/gitpod-db/lib"; +import { ProjectDB, TeamDB, TermsAcceptanceDB, UserDB } from "@gitpod/gitpod-db/lib"; import { HostContextProvider } from "../auth/host-context-provider"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; import { Config } from "../config"; @@ -70,7 +70,6 @@ export class UserService { @inject(TermsAcceptanceDB) protected readonly termsAcceptanceDb: TermsAcceptanceDB; @inject(TermsProvider) protected readonly termsProvider: TermsProvider; @inject(ProjectDB) protected readonly projectDb: ProjectDB; - @inject(CostCenterDB) protected readonly costCenterDb: CostCenterDB; @inject(TeamDB) protected readonly teamDB: TeamDB; @inject(StripeService) protected readonly stripeService: StripeService; @inject(VerificationService) protected readonly verificationService: VerificationService; diff --git a/components/usage-api/go/v1/usage.pb.go b/components/usage-api/go/v1/usage.pb.go index e1d1997e44b545..3258204bfc609f 100644 --- a/components/usage-api/go/v1/usage.pb.go +++ b/components/usage-api/go/v1/usage.pb.go @@ -117,6 +117,52 @@ func (Usage_Kind) EnumDescriptor() ([]byte, []int) { return file_usage_v1_usage_proto_rawDescGZIP(), []int{6, 0} } +type CostCenter_BillingStrategy int32 + +const ( + CostCenter_BILLING_STRATEGY_STRIPE CostCenter_BillingStrategy = 0 + CostCenter_BILLING_STRATEGY_OTHER CostCenter_BillingStrategy = 1 +) + +// Enum value maps for CostCenter_BillingStrategy. +var ( + CostCenter_BillingStrategy_name = map[int32]string{ + 0: "BILLING_STRATEGY_STRIPE", + 1: "BILLING_STRATEGY_OTHER", + } + CostCenter_BillingStrategy_value = map[string]int32{ + "BILLING_STRATEGY_STRIPE": 0, + "BILLING_STRATEGY_OTHER": 1, + } +) + +func (x CostCenter_BillingStrategy) Enum() *CostCenter_BillingStrategy { + p := new(CostCenter_BillingStrategy) + *p = x + return p +} + +func (x CostCenter_BillingStrategy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CostCenter_BillingStrategy) Descriptor() protoreflect.EnumDescriptor { + return file_usage_v1_usage_proto_enumTypes[2].Descriptor() +} + +func (CostCenter_BillingStrategy) Type() protoreflect.EnumType { + return &file_usage_v1_usage_proto_enumTypes[2] +} + +func (x CostCenter_BillingStrategy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CostCenter_BillingStrategy.Descriptor instead. +func (CostCenter_BillingStrategy) EnumDescriptor() ([]byte, []int) { + return file_usage_v1_usage_proto_rawDescGZIP(), []int{11, 0} +} + type ReconcileUsageWithLedgerRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -605,6 +651,91 @@ func (x *Usage) GetMetadata() string { return "" } +type SetCostCenterRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CostCenter *CostCenter `protobuf:"bytes,1,opt,name=cost_center,json=costCenter,proto3" json:"cost_center,omitempty"` +} + +func (x *SetCostCenterRequest) Reset() { + *x = SetCostCenterRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_usage_v1_usage_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetCostCenterRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetCostCenterRequest) ProtoMessage() {} + +func (x *SetCostCenterRequest) ProtoReflect() protoreflect.Message { + mi := &file_usage_v1_usage_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetCostCenterRequest.ProtoReflect.Descriptor instead. +func (*SetCostCenterRequest) Descriptor() ([]byte, []int) { + return file_usage_v1_usage_proto_rawDescGZIP(), []int{7} +} + +func (x *SetCostCenterRequest) GetCostCenter() *CostCenter { + if x != nil { + return x.CostCenter + } + return nil +} + +type SetCostCenterResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SetCostCenterResponse) Reset() { + *x = SetCostCenterResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_usage_v1_usage_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetCostCenterResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetCostCenterResponse) ProtoMessage() {} + +func (x *SetCostCenterResponse) ProtoReflect() protoreflect.Message { + mi := &file_usage_v1_usage_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetCostCenterResponse.ProtoReflect.Descriptor instead. +func (*SetCostCenterResponse) Descriptor() ([]byte, []int) { + return file_usage_v1_usage_proto_rawDescGZIP(), []int{8} +} + type GetCostCenterRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -616,7 +747,7 @@ type GetCostCenterRequest struct { func (x *GetCostCenterRequest) Reset() { *x = GetCostCenterRequest{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[7] + mi := &file_usage_v1_usage_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -629,7 +760,7 @@ func (x *GetCostCenterRequest) String() string { func (*GetCostCenterRequest) ProtoMessage() {} func (x *GetCostCenterRequest) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[7] + mi := &file_usage_v1_usage_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -642,7 +773,7 @@ func (x *GetCostCenterRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCostCenterRequest.ProtoReflect.Descriptor instead. func (*GetCostCenterRequest) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{7} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{9} } func (x *GetCostCenterRequest) GetAttributionId() string { @@ -663,7 +794,7 @@ type GetCostCenterResponse struct { func (x *GetCostCenterResponse) Reset() { *x = GetCostCenterResponse{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[8] + mi := &file_usage_v1_usage_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -676,7 +807,7 @@ func (x *GetCostCenterResponse) String() string { func (*GetCostCenterResponse) ProtoMessage() {} func (x *GetCostCenterResponse) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[8] + mi := &file_usage_v1_usage_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -689,7 +820,7 @@ func (x *GetCostCenterResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCostCenterResponse.ProtoReflect.Descriptor instead. func (*GetCostCenterResponse) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{8} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{10} } func (x *GetCostCenterResponse) GetCostCenter() *CostCenter { @@ -704,14 +835,15 @@ type CostCenter struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - AttributionId string `protobuf:"bytes,1,opt,name=attribution_id,json=attributionId,proto3" json:"attribution_id,omitempty"` - SpendingLimit int32 `protobuf:"varint,2,opt,name=spending_limit,json=spendingLimit,proto3" json:"spending_limit,omitempty"` + AttributionId string `protobuf:"bytes,1,opt,name=attribution_id,json=attributionId,proto3" json:"attribution_id,omitempty"` + SpendingLimit int32 `protobuf:"varint,2,opt,name=spending_limit,json=spendingLimit,proto3" json:"spending_limit,omitempty"` + BillingStrategy CostCenter_BillingStrategy `protobuf:"varint,3,opt,name=billing_strategy,json=billingStrategy,proto3,enum=usage.v1.CostCenter_BillingStrategy" json:"billing_strategy,omitempty"` } func (x *CostCenter) Reset() { *x = CostCenter{} if protoimpl.UnsafeEnabled { - mi := &file_usage_v1_usage_proto_msgTypes[9] + mi := &file_usage_v1_usage_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -724,7 +856,7 @@ func (x *CostCenter) String() string { func (*CostCenter) ProtoMessage() {} func (x *CostCenter) ProtoReflect() protoreflect.Message { - mi := &file_usage_v1_usage_proto_msgTypes[9] + mi := &file_usage_v1_usage_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -737,7 +869,7 @@ func (x *CostCenter) ProtoReflect() protoreflect.Message { // Deprecated: Use CostCenter.ProtoReflect.Descriptor instead. func (*CostCenter) Descriptor() ([]byte, []int) { - return file_usage_v1_usage_proto_rawDescGZIP(), []int{9} + return file_usage_v1_usage_proto_rawDescGZIP(), []int{11} } func (x *CostCenter) GetAttributionId() string { @@ -754,6 +886,13 @@ func (x *CostCenter) GetSpendingLimit() int32 { return 0 } +func (x *CostCenter) GetBillingStrategy() CostCenter_BillingStrategy { + if x != nil { + return x.BillingStrategy + } + return CostCenter_BILLING_STRATEGY_STRIPE +} + var File_usage_v1_usage_proto protoreflect.FileDescriptor var file_usage_v1_usage_proto_rawDesc = []byte{ @@ -843,43 +982,65 @@ var file_usage_v1_usage_proto_rawDesc = []byte{ 0x61, 0x22, 0x35, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x17, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x57, 0x4f, 0x52, 0x4b, 0x53, 0x50, 0x41, 0x43, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x49, - 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x10, 0x01, 0x22, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, + 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x10, 0x01, 0x22, 0x4d, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x4e, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x6f, - 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x63, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x73, - 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x22, 0x5a, 0x0a, 0x0a, 0x43, 0x6f, 0x73, 0x74, 0x43, - 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, - 0x73, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4c, 0x69, - 0x6d, 0x69, 0x74, 0x32, 0x9f, 0x02, 0x0a, 0x0c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, - 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1e, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x73, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, - 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x57, 0x69, 0x74, 0x68, 0x4c, 0x65, - 0x64, 0x67, 0x65, 0x72, 0x12, 0x29, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x57, 0x69, - 0x74, 0x68, 0x4c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2a, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, - 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x57, 0x69, 0x74, 0x68, 0x4c, 0x65, 0x64, - 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, - 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x2e, 0x75, 0x73, 0x61, - 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 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, + 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x22, 0x17, 0x0a, 0x15, 0x53, 0x65, 0x74, 0x43, 0x6f, + 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, + 0x4e, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x63, 0x6f, 0x73, 0x74, + 0x5f, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, + 0x74, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x22, + 0xf7, 0x01, 0x0a, 0x0a, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x25, + 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, + 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x4f, 0x0a, 0x10, + 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x2e, 0x42, 0x69, 0x6c, + 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0f, 0x62, 0x69, + 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x22, 0x4a, 0x0a, + 0x0f, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x12, 0x1b, 0x0a, 0x17, 0x42, 0x49, 0x4c, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x52, 0x41, + 0x54, 0x45, 0x47, 0x59, 0x5f, 0x53, 0x54, 0x52, 0x49, 0x50, 0x45, 0x10, 0x00, 0x12, 0x1a, 0x0a, + 0x16, 0x42, 0x49, 0x4c, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, + 0x59, 0x5f, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x10, 0x01, 0x32, 0xf3, 0x02, 0x0a, 0x0c, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x0d, 0x47, 0x65, + 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x1e, 0x2e, 0x75, 0x73, + 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, + 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x75, 0x73, + 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, + 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x52, + 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, + 0x1e, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x43, 0x6f, + 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1f, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x43, 0x6f, + 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x73, 0x0a, 0x18, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x57, 0x69, 0x74, 0x68, 0x4c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x12, 0x29, + 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, + 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x57, 0x69, 0x74, 0x68, 0x4c, 0x65, 0x64, 0x67, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x75, 0x73, 0x61, 0x67, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x57, 0x69, 0x74, 0x68, 0x4c, 0x65, 0x64, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1b, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x55, 0x73, 0x61, 0x67, 0x65, 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 ( @@ -894,46 +1055,53 @@ func file_usage_v1_usage_proto_rawDescGZIP() []byte { return file_usage_v1_usage_proto_rawDescData } -var file_usage_v1_usage_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_usage_v1_usage_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_usage_v1_usage_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_usage_v1_usage_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_usage_v1_usage_proto_goTypes = []interface{}{ (ListUsageRequest_Ordering)(0), // 0: usage.v1.ListUsageRequest.Ordering (Usage_Kind)(0), // 1: usage.v1.Usage.Kind - (*ReconcileUsageWithLedgerRequest)(nil), // 2: usage.v1.ReconcileUsageWithLedgerRequest - (*ReconcileUsageWithLedgerResponse)(nil), // 3: usage.v1.ReconcileUsageWithLedgerResponse - (*PaginatedRequest)(nil), // 4: usage.v1.PaginatedRequest - (*PaginatedResponse)(nil), // 5: usage.v1.PaginatedResponse - (*ListUsageRequest)(nil), // 6: usage.v1.ListUsageRequest - (*ListUsageResponse)(nil), // 7: usage.v1.ListUsageResponse - (*Usage)(nil), // 8: usage.v1.Usage - (*GetCostCenterRequest)(nil), // 9: usage.v1.GetCostCenterRequest - (*GetCostCenterResponse)(nil), // 10: usage.v1.GetCostCenterResponse - (*CostCenter)(nil), // 11: usage.v1.CostCenter - (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp + (CostCenter_BillingStrategy)(0), // 2: usage.v1.CostCenter.BillingStrategy + (*ReconcileUsageWithLedgerRequest)(nil), // 3: usage.v1.ReconcileUsageWithLedgerRequest + (*ReconcileUsageWithLedgerResponse)(nil), // 4: usage.v1.ReconcileUsageWithLedgerResponse + (*PaginatedRequest)(nil), // 5: usage.v1.PaginatedRequest + (*PaginatedResponse)(nil), // 6: usage.v1.PaginatedResponse + (*ListUsageRequest)(nil), // 7: usage.v1.ListUsageRequest + (*ListUsageResponse)(nil), // 8: usage.v1.ListUsageResponse + (*Usage)(nil), // 9: usage.v1.Usage + (*SetCostCenterRequest)(nil), // 10: usage.v1.SetCostCenterRequest + (*SetCostCenterResponse)(nil), // 11: usage.v1.SetCostCenterResponse + (*GetCostCenterRequest)(nil), // 12: usage.v1.GetCostCenterRequest + (*GetCostCenterResponse)(nil), // 13: usage.v1.GetCostCenterResponse + (*CostCenter)(nil), // 14: usage.v1.CostCenter + (*timestamppb.Timestamp)(nil), // 15: google.protobuf.Timestamp } var file_usage_v1_usage_proto_depIdxs = []int32{ - 12, // 0: usage.v1.ReconcileUsageWithLedgerRequest.from:type_name -> google.protobuf.Timestamp - 12, // 1: usage.v1.ReconcileUsageWithLedgerRequest.to:type_name -> google.protobuf.Timestamp - 12, // 2: usage.v1.ListUsageRequest.from:type_name -> google.protobuf.Timestamp - 12, // 3: usage.v1.ListUsageRequest.to:type_name -> google.protobuf.Timestamp + 15, // 0: usage.v1.ReconcileUsageWithLedgerRequest.from:type_name -> google.protobuf.Timestamp + 15, // 1: usage.v1.ReconcileUsageWithLedgerRequest.to:type_name -> google.protobuf.Timestamp + 15, // 2: usage.v1.ListUsageRequest.from:type_name -> google.protobuf.Timestamp + 15, // 3: usage.v1.ListUsageRequest.to:type_name -> google.protobuf.Timestamp 0, // 4: usage.v1.ListUsageRequest.order:type_name -> usage.v1.ListUsageRequest.Ordering - 4, // 5: usage.v1.ListUsageRequest.pagination:type_name -> usage.v1.PaginatedRequest - 8, // 6: usage.v1.ListUsageResponse.usage_entries:type_name -> usage.v1.Usage - 5, // 7: usage.v1.ListUsageResponse.pagination:type_name -> usage.v1.PaginatedResponse - 12, // 8: usage.v1.Usage.effective_time:type_name -> google.protobuf.Timestamp + 5, // 5: usage.v1.ListUsageRequest.pagination:type_name -> usage.v1.PaginatedRequest + 9, // 6: usage.v1.ListUsageResponse.usage_entries:type_name -> usage.v1.Usage + 6, // 7: usage.v1.ListUsageResponse.pagination:type_name -> usage.v1.PaginatedResponse + 15, // 8: usage.v1.Usage.effective_time:type_name -> google.protobuf.Timestamp 1, // 9: usage.v1.Usage.kind:type_name -> usage.v1.Usage.Kind - 11, // 10: usage.v1.GetCostCenterResponse.cost_center:type_name -> usage.v1.CostCenter - 9, // 11: usage.v1.UsageService.GetCostCenter:input_type -> usage.v1.GetCostCenterRequest - 2, // 12: usage.v1.UsageService.ReconcileUsageWithLedger:input_type -> usage.v1.ReconcileUsageWithLedgerRequest - 6, // 13: usage.v1.UsageService.ListUsage:input_type -> usage.v1.ListUsageRequest - 10, // 14: usage.v1.UsageService.GetCostCenter:output_type -> usage.v1.GetCostCenterResponse - 3, // 15: usage.v1.UsageService.ReconcileUsageWithLedger:output_type -> usage.v1.ReconcileUsageWithLedgerResponse - 7, // 16: usage.v1.UsageService.ListUsage:output_type -> usage.v1.ListUsageResponse - 14, // [14:17] is the sub-list for method output_type - 11, // [11:14] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 14, // 10: usage.v1.SetCostCenterRequest.cost_center:type_name -> usage.v1.CostCenter + 14, // 11: usage.v1.GetCostCenterResponse.cost_center:type_name -> usage.v1.CostCenter + 2, // 12: usage.v1.CostCenter.billing_strategy:type_name -> usage.v1.CostCenter.BillingStrategy + 12, // 13: usage.v1.UsageService.GetCostCenter:input_type -> usage.v1.GetCostCenterRequest + 10, // 14: usage.v1.UsageService.SetCostCenter:input_type -> usage.v1.SetCostCenterRequest + 3, // 15: usage.v1.UsageService.ReconcileUsageWithLedger:input_type -> usage.v1.ReconcileUsageWithLedgerRequest + 7, // 16: usage.v1.UsageService.ListUsage:input_type -> usage.v1.ListUsageRequest + 13, // 17: usage.v1.UsageService.GetCostCenter:output_type -> usage.v1.GetCostCenterResponse + 11, // 18: usage.v1.UsageService.SetCostCenter:output_type -> usage.v1.SetCostCenterResponse + 4, // 19: usage.v1.UsageService.ReconcileUsageWithLedger:output_type -> usage.v1.ReconcileUsageWithLedgerResponse + 8, // 20: usage.v1.UsageService.ListUsage:output_type -> usage.v1.ListUsageResponse + 17, // [17:21] is the sub-list for method output_type + 13, // [13:17] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_usage_v1_usage_proto_init() } @@ -1027,7 +1195,7 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetCostCenterRequest); i { + switch v := v.(*SetCostCenterRequest); i { case 0: return &v.state case 1: @@ -1039,7 +1207,7 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetCostCenterResponse); i { + switch v := v.(*SetCostCenterResponse); i { case 0: return &v.state case 1: @@ -1051,6 +1219,30 @@ func file_usage_v1_usage_proto_init() { } } file_usage_v1_usage_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetCostCenterRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_usage_v1_usage_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetCostCenterResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_usage_v1_usage_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CostCenter); i { case 0: return &v.state @@ -1068,8 +1260,8 @@ func file_usage_v1_usage_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_usage_v1_usage_proto_rawDesc, - NumEnums: 2, - NumMessages: 10, + NumEnums: 3, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, diff --git a/components/usage-api/go/v1/usage_grpc.pb.go b/components/usage-api/go/v1/usage_grpc.pb.go index a678b468fe4231..00e5bde855ab5c 100644 --- a/components/usage-api/go/v1/usage_grpc.pb.go +++ b/components/usage-api/go/v1/usage_grpc.pb.go @@ -26,8 +26,10 @@ const _ = grpc.SupportPackageIsVersion7 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type UsageServiceClient interface { - // GetCostCenter retrieves the spending limit with its associated attributionID + // GetCostCenter retrieves the active cost center for the given attributionID GetCostCenter(ctx context.Context, in *GetCostCenterRequest, opts ...grpc.CallOption) (*GetCostCenterResponse, error) + // SetCostCenter stores the given cost center + SetCostCenter(ctx context.Context, in *SetCostCenterRequest, opts ...grpc.CallOption) (*SetCostCenterResponse, error) // Triggers reconciliation of usage with ledger implementation. ReconcileUsageWithLedger(ctx context.Context, in *ReconcileUsageWithLedgerRequest, opts ...grpc.CallOption) (*ReconcileUsageWithLedgerResponse, error) // ListUsage retrieves all usage for the specified attributionId and theb given time range @@ -51,6 +53,15 @@ func (c *usageServiceClient) GetCostCenter(ctx context.Context, in *GetCostCente return out, nil } +func (c *usageServiceClient) SetCostCenter(ctx context.Context, in *SetCostCenterRequest, opts ...grpc.CallOption) (*SetCostCenterResponse, error) { + out := new(SetCostCenterResponse) + err := c.cc.Invoke(ctx, "/usage.v1.UsageService/SetCostCenter", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *usageServiceClient) ReconcileUsageWithLedger(ctx context.Context, in *ReconcileUsageWithLedgerRequest, opts ...grpc.CallOption) (*ReconcileUsageWithLedgerResponse, error) { out := new(ReconcileUsageWithLedgerResponse) err := c.cc.Invoke(ctx, "/usage.v1.UsageService/ReconcileUsageWithLedger", in, out, opts...) @@ -73,8 +84,10 @@ func (c *usageServiceClient) ListUsage(ctx context.Context, in *ListUsageRequest // All implementations must embed UnimplementedUsageServiceServer // for forward compatibility type UsageServiceServer interface { - // GetCostCenter retrieves the spending limit with its associated attributionID + // GetCostCenter retrieves the active cost center for the given attributionID GetCostCenter(context.Context, *GetCostCenterRequest) (*GetCostCenterResponse, error) + // SetCostCenter stores the given cost center + SetCostCenter(context.Context, *SetCostCenterRequest) (*SetCostCenterResponse, error) // Triggers reconciliation of usage with ledger implementation. ReconcileUsageWithLedger(context.Context, *ReconcileUsageWithLedgerRequest) (*ReconcileUsageWithLedgerResponse, error) // ListUsage retrieves all usage for the specified attributionId and theb given time range @@ -89,6 +102,9 @@ type UnimplementedUsageServiceServer struct { func (UnimplementedUsageServiceServer) GetCostCenter(context.Context, *GetCostCenterRequest) (*GetCostCenterResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetCostCenter not implemented") } +func (UnimplementedUsageServiceServer) SetCostCenter(context.Context, *SetCostCenterRequest) (*SetCostCenterResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetCostCenter not implemented") +} func (UnimplementedUsageServiceServer) ReconcileUsageWithLedger(context.Context, *ReconcileUsageWithLedgerRequest) (*ReconcileUsageWithLedgerResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ReconcileUsageWithLedger not implemented") } @@ -126,6 +142,24 @@ func _UsageService_GetCostCenter_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _UsageService_SetCostCenter_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetCostCenterRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UsageServiceServer).SetCostCenter(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/usage.v1.UsageService/SetCostCenter", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UsageServiceServer).SetCostCenter(ctx, req.(*SetCostCenterRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _UsageService_ReconcileUsageWithLedger_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ReconcileUsageWithLedgerRequest) if err := dec(in); err != nil { @@ -173,6 +207,10 @@ var UsageService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetCostCenter", Handler: _UsageService_GetCostCenter_Handler, }, + { + MethodName: "SetCostCenter", + Handler: _UsageService_SetCostCenter_Handler, + }, { MethodName: "ReconcileUsageWithLedger", Handler: _UsageService_ReconcileUsageWithLedger_Handler, diff --git a/components/usage-api/typescript/src/usage/v1/sugar.ts b/components/usage-api/typescript/src/usage/v1/sugar.ts index 4be34d591919a4..fcd2a294dd89d7 100644 --- a/components/usage-api/typescript/src/usage/v1/sugar.ts +++ b/components/usage-api/typescript/src/usage/v1/sugar.ts @@ -9,7 +9,7 @@ import { BillingServiceClient } from "./billing_grpc_pb"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import * as opentracing from "opentracing"; import { Metadata } from "@grpc/grpc-js"; -import { ListUsageRequest, ListUsageResponse } from "./usage_pb"; +import { CostCenter as ProtocolCostCenter, GetCostCenterRequest, GetCostCenterResponse, ListUsageRequest, ListUsageResponse, SetCostCenterRequest, SetCostCenterResponse } from "./usage_pb"; import { GetUpcomingInvoiceRequest, GetUpcomingInvoiceResponse, @@ -22,6 +22,7 @@ import { createClientCallMetricsInterceptor, IClientCallMetrics } from "@gitpod/ import * as grpc from "@grpc/grpc-js"; import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; +import { CostCenter } from "@gitpod/gitpod-protocol"; export const UsageServiceClientProvider = Symbol("UsageServiceClientProvider"); export const BillingServiceClientProvider = Symbol("BillingServiceClientProvider"); @@ -175,6 +176,62 @@ export class PromisifiedUsageServiceClient { } } + public async getCostCenter( + attributionID: AttributionId, + ): Promise { + const request = new GetCostCenterRequest(); + request.setAttributionId(AttributionId.render(attributionID)) + + const response = await new Promise((resolve, reject) => { + this.client.getCostCenter( + request, + (err: grpc.ServiceError | null, response: GetCostCenterResponse) => { + if (err) { + reject(err); + return; + } + resolve(response); + }, + ); + }); + if (!response.hasCostCenter()) { + return undefined; + } + return { + id: AttributionId.parse(response.getCostCenter()!.getAttributionId()), + spendingLimit: response.getCostCenter()!.getSpendingLimit(), + billingStrategy: response.getCostCenter()!.getBillingStrategy() || 'other' + }; + } + + public async setCostCenter( + costCenter: CostCenter, + ): Promise { + const request = new SetCostCenterRequest(); + const cc = new ProtocolCostCenter(); + cc.setAttributionId(AttributionId.render(costCenter.id)); + let billingStrategy = ProtocolCostCenter.BillingStrategy.BILLING_STRATEGY_OTHER; + if (costCenter.billingStrategy == 'stripe') { + billingStrategy = ProtocolCostCenter.BillingStrategy.BILLING_STRATEGY_STRIPE; + } + cc.setBillingStrategy(billingStrategy); + cc.setSpendingLimit(costCenter.spendingLimit); + request.setCostCenter(cc); + + await new Promise((resolve, reject) => { + this.client.setCostCenter( + request, + (err: grpc.ServiceError | null, response: SetCostCenterResponse) => { + if (err) { + reject(err); + return; + } + resolve(response); + }, + ); + }); + } + public dispose() { this.client.close(); } diff --git a/components/usage-api/typescript/src/usage/v1/usage_grpc_pb.d.ts b/components/usage-api/typescript/src/usage/v1/usage_grpc_pb.d.ts index 42817393db0880..8bc2acd06f9fec 100644 --- a/components/usage-api/typescript/src/usage/v1/usage_grpc_pb.d.ts +++ b/components/usage-api/typescript/src/usage/v1/usage_grpc_pb.d.ts @@ -16,6 +16,7 @@ import * as google_protobuf_timestamp_pb from "google-protobuf/google/protobuf/t interface IUsageServiceService extends grpc.ServiceDefinition { getCostCenter: IUsageServiceService_IGetCostCenter; + setCostCenter: IUsageServiceService_ISetCostCenter; reconcileUsageWithLedger: IUsageServiceService_IReconcileUsageWithLedger; listUsage: IUsageServiceService_IListUsage; } @@ -29,6 +30,15 @@ interface IUsageServiceService_IGetCostCenter extends grpc.MethodDefinition; responseDeserialize: grpc.deserialize; } +interface IUsageServiceService_ISetCostCenter extends grpc.MethodDefinition { + path: "/usage.v1.UsageService/SetCostCenter"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} interface IUsageServiceService_IReconcileUsageWithLedger extends grpc.MethodDefinition { path: "/usage.v1.UsageService/ReconcileUsageWithLedger"; requestStream: false; @@ -52,6 +62,7 @@ export const UsageServiceService: IUsageServiceService; export interface IUsageServiceServer extends grpc.UntypedServiceImplementation { getCostCenter: grpc.handleUnaryCall; + setCostCenter: grpc.handleUnaryCall; reconcileUsageWithLedger: grpc.handleUnaryCall; listUsage: grpc.handleUnaryCall; } @@ -60,6 +71,9 @@ export interface IUsageServiceClient { getCostCenter(request: usage_v1_usage_pb.GetCostCenterRequest, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.GetCostCenterResponse) => void): grpc.ClientUnaryCall; getCostCenter(request: usage_v1_usage_pb.GetCostCenterRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.GetCostCenterResponse) => void): grpc.ClientUnaryCall; getCostCenter(request: usage_v1_usage_pb.GetCostCenterRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.GetCostCenterResponse) => void): grpc.ClientUnaryCall; + setCostCenter(request: usage_v1_usage_pb.SetCostCenterRequest, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.SetCostCenterResponse) => void): grpc.ClientUnaryCall; + setCostCenter(request: usage_v1_usage_pb.SetCostCenterRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.SetCostCenterResponse) => void): grpc.ClientUnaryCall; + setCostCenter(request: usage_v1_usage_pb.SetCostCenterRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.SetCostCenterResponse) => void): grpc.ClientUnaryCall; reconcileUsageWithLedger(request: usage_v1_usage_pb.ReconcileUsageWithLedgerRequest, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.ReconcileUsageWithLedgerResponse) => void): grpc.ClientUnaryCall; reconcileUsageWithLedger(request: usage_v1_usage_pb.ReconcileUsageWithLedgerRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.ReconcileUsageWithLedgerResponse) => void): grpc.ClientUnaryCall; reconcileUsageWithLedger(request: usage_v1_usage_pb.ReconcileUsageWithLedgerRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.ReconcileUsageWithLedgerResponse) => void): grpc.ClientUnaryCall; @@ -73,6 +87,9 @@ export class UsageServiceClient extends grpc.Client implements IUsageServiceClie public getCostCenter(request: usage_v1_usage_pb.GetCostCenterRequest, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.GetCostCenterResponse) => void): grpc.ClientUnaryCall; public getCostCenter(request: usage_v1_usage_pb.GetCostCenterRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.GetCostCenterResponse) => void): grpc.ClientUnaryCall; public getCostCenter(request: usage_v1_usage_pb.GetCostCenterRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.GetCostCenterResponse) => void): grpc.ClientUnaryCall; + public setCostCenter(request: usage_v1_usage_pb.SetCostCenterRequest, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.SetCostCenterResponse) => void): grpc.ClientUnaryCall; + public setCostCenter(request: usage_v1_usage_pb.SetCostCenterRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.SetCostCenterResponse) => void): grpc.ClientUnaryCall; + public setCostCenter(request: usage_v1_usage_pb.SetCostCenterRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.SetCostCenterResponse) => void): grpc.ClientUnaryCall; public reconcileUsageWithLedger(request: usage_v1_usage_pb.ReconcileUsageWithLedgerRequest, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.ReconcileUsageWithLedgerResponse) => void): grpc.ClientUnaryCall; public reconcileUsageWithLedger(request: usage_v1_usage_pb.ReconcileUsageWithLedgerRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.ReconcileUsageWithLedgerResponse) => void): grpc.ClientUnaryCall; public reconcileUsageWithLedger(request: usage_v1_usage_pb.ReconcileUsageWithLedgerRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: usage_v1_usage_pb.ReconcileUsageWithLedgerResponse) => void): grpc.ClientUnaryCall; diff --git a/components/usage-api/typescript/src/usage/v1/usage_grpc_pb.js b/components/usage-api/typescript/src/usage/v1/usage_grpc_pb.js index 67b779a33b682a..ebd38f518d090d 100644 --- a/components/usage-api/typescript/src/usage/v1/usage_grpc_pb.js +++ b/components/usage-api/typescript/src/usage/v1/usage_grpc_pb.js @@ -77,9 +77,31 @@ function deserialize_usage_v1_ReconcileUsageWithLedgerResponse(buffer_arg) { return usage_v1_usage_pb.ReconcileUsageWithLedgerResponse.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_usage_v1_SetCostCenterRequest(arg) { + if (!(arg instanceof usage_v1_usage_pb.SetCostCenterRequest)) { + throw new Error('Expected argument of type usage.v1.SetCostCenterRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_usage_v1_SetCostCenterRequest(buffer_arg) { + return usage_v1_usage_pb.SetCostCenterRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_usage_v1_SetCostCenterResponse(arg) { + if (!(arg instanceof usage_v1_usage_pb.SetCostCenterResponse)) { + throw new Error('Expected argument of type usage.v1.SetCostCenterResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_usage_v1_SetCostCenterResponse(buffer_arg) { + return usage_v1_usage_pb.SetCostCenterResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + var UsageServiceService = exports.UsageServiceService = { - // GetCostCenter retrieves the spending limit with its associated attributionID + // GetCostCenter retrieves the active cost center for the given attributionID getCostCenter: { path: '/usage.v1.UsageService/GetCostCenter', requestStream: false, @@ -91,6 +113,18 @@ getCostCenter: { responseSerialize: serialize_usage_v1_GetCostCenterResponse, responseDeserialize: deserialize_usage_v1_GetCostCenterResponse, }, + // SetCostCenter stores the given cost center +setCostCenter: { + path: '/usage.v1.UsageService/SetCostCenter', + requestStream: false, + responseStream: false, + requestType: usage_v1_usage_pb.SetCostCenterRequest, + responseType: usage_v1_usage_pb.SetCostCenterResponse, + requestSerialize: serialize_usage_v1_SetCostCenterRequest, + requestDeserialize: deserialize_usage_v1_SetCostCenterRequest, + responseSerialize: serialize_usage_v1_SetCostCenterResponse, + responseDeserialize: deserialize_usage_v1_SetCostCenterResponse, + }, // Triggers reconciliation of usage with ledger implementation. reconcileUsageWithLedger: { path: '/usage.v1.UsageService/ReconcileUsageWithLedger', diff --git a/components/usage-api/typescript/src/usage/v1/usage_pb.d.ts b/components/usage-api/typescript/src/usage/v1/usage_pb.d.ts index fbb221bef861fb..8d4e4f87a1bc2b 100644 --- a/components/usage-api/typescript/src/usage/v1/usage_pb.d.ts +++ b/components/usage-api/typescript/src/usage/v1/usage_pb.d.ts @@ -245,6 +245,46 @@ export namespace Usage { } +export class SetCostCenterRequest extends jspb.Message { + + hasCostCenter(): boolean; + clearCostCenter(): void; + getCostCenter(): CostCenter | undefined; + setCostCenter(value?: CostCenter): SetCostCenterRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): SetCostCenterRequest.AsObject; + static toObject(includeInstance: boolean, msg: SetCostCenterRequest): SetCostCenterRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: SetCostCenterRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): SetCostCenterRequest; + static deserializeBinaryFromReader(message: SetCostCenterRequest, reader: jspb.BinaryReader): SetCostCenterRequest; +} + +export namespace SetCostCenterRequest { + export type AsObject = { + costCenter?: CostCenter.AsObject, + } +} + +export class SetCostCenterResponse extends jspb.Message { + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): SetCostCenterResponse.AsObject; + static toObject(includeInstance: boolean, msg: SetCostCenterResponse): SetCostCenterResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: SetCostCenterResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): SetCostCenterResponse; + static deserializeBinaryFromReader(message: SetCostCenterResponse, reader: jspb.BinaryReader): SetCostCenterResponse; +} + +export namespace SetCostCenterResponse { + export type AsObject = { + } +} + export class GetCostCenterRequest extends jspb.Message { getAttributionId(): string; setAttributionId(value: string): GetCostCenterRequest; @@ -293,6 +333,8 @@ export class CostCenter extends jspb.Message { setAttributionId(value: string): CostCenter; getSpendingLimit(): number; setSpendingLimit(value: number): CostCenter; + getBillingStrategy(): CostCenter.BillingStrategy; + setBillingStrategy(value: CostCenter.BillingStrategy): CostCenter; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): CostCenter.AsObject; @@ -308,5 +350,12 @@ export namespace CostCenter { export type AsObject = { attributionId: string, spendingLimit: number, + billingStrategy: CostCenter.BillingStrategy, } + + export enum BillingStrategy { + BILLING_STRATEGY_STRIPE = 0, + BILLING_STRATEGY_OTHER = 1, + } + } diff --git a/components/usage-api/typescript/src/usage/v1/usage_pb.js b/components/usage-api/typescript/src/usage/v1/usage_pb.js index 86f06027041074..18ed1a12a3b78b 100644 --- a/components/usage-api/typescript/src/usage/v1/usage_pb.js +++ b/components/usage-api/typescript/src/usage/v1/usage_pb.js @@ -24,6 +24,7 @@ var global = (function() { return this || window || global || self || Function(' var google_protobuf_timestamp_pb = require('google-protobuf/google/protobuf/timestamp_pb.js'); goog.object.extend(proto, google_protobuf_timestamp_pb); goog.exportSymbol('proto.usage.v1.CostCenter', null, global); +goog.exportSymbol('proto.usage.v1.CostCenter.BillingStrategy', null, global); goog.exportSymbol('proto.usage.v1.GetCostCenterRequest', null, global); goog.exportSymbol('proto.usage.v1.GetCostCenterResponse', null, global); goog.exportSymbol('proto.usage.v1.ListUsageRequest', null, global); @@ -33,6 +34,8 @@ goog.exportSymbol('proto.usage.v1.PaginatedRequest', null, global); goog.exportSymbol('proto.usage.v1.PaginatedResponse', null, global); goog.exportSymbol('proto.usage.v1.ReconcileUsageWithLedgerRequest', null, global); goog.exportSymbol('proto.usage.v1.ReconcileUsageWithLedgerResponse', null, global); +goog.exportSymbol('proto.usage.v1.SetCostCenterRequest', null, global); +goog.exportSymbol('proto.usage.v1.SetCostCenterResponse', null, global); goog.exportSymbol('proto.usage.v1.Usage', null, global); goog.exportSymbol('proto.usage.v1.Usage.Kind', null, global); /** @@ -182,6 +185,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.usage.v1.Usage.displayName = 'proto.usage.v1.Usage'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.usage.v1.SetCostCenterRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.usage.v1.SetCostCenterRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.usage.v1.SetCostCenterRequest.displayName = 'proto.usage.v1.SetCostCenterRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.usage.v1.SetCostCenterResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.usage.v1.SetCostCenterResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.usage.v1.SetCostCenterResponse.displayName = 'proto.usage.v1.SetCostCenterResponse'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -1922,6 +1967,258 @@ proto.usage.v1.Usage.prototype.setMetadata = function(value) { +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.usage.v1.SetCostCenterRequest.prototype.toObject = function(opt_includeInstance) { + return proto.usage.v1.SetCostCenterRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.usage.v1.SetCostCenterRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.usage.v1.SetCostCenterRequest.toObject = function(includeInstance, msg) { + var f, obj = { + costCenter: (f = msg.getCostCenter()) && proto.usage.v1.CostCenter.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.usage.v1.SetCostCenterRequest} + */ +proto.usage.v1.SetCostCenterRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.usage.v1.SetCostCenterRequest; + return proto.usage.v1.SetCostCenterRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.usage.v1.SetCostCenterRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.usage.v1.SetCostCenterRequest} + */ +proto.usage.v1.SetCostCenterRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = new proto.usage.v1.CostCenter; + reader.readMessage(value,proto.usage.v1.CostCenter.deserializeBinaryFromReader); + msg.setCostCenter(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.usage.v1.SetCostCenterRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.usage.v1.SetCostCenterRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.usage.v1.SetCostCenterRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.usage.v1.SetCostCenterRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getCostCenter(); + if (f != null) { + writer.writeMessage( + 1, + f, + proto.usage.v1.CostCenter.serializeBinaryToWriter + ); + } +}; + + +/** + * optional CostCenter cost_center = 1; + * @return {?proto.usage.v1.CostCenter} + */ +proto.usage.v1.SetCostCenterRequest.prototype.getCostCenter = function() { + return /** @type{?proto.usage.v1.CostCenter} */ ( + jspb.Message.getWrapperField(this, proto.usage.v1.CostCenter, 1)); +}; + + +/** + * @param {?proto.usage.v1.CostCenter|undefined} value + * @return {!proto.usage.v1.SetCostCenterRequest} returns this +*/ +proto.usage.v1.SetCostCenterRequest.prototype.setCostCenter = function(value) { + return jspb.Message.setWrapperField(this, 1, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.usage.v1.SetCostCenterRequest} returns this + */ +proto.usage.v1.SetCostCenterRequest.prototype.clearCostCenter = function() { + return this.setCostCenter(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.usage.v1.SetCostCenterRequest.prototype.hasCostCenter = function() { + return jspb.Message.getField(this, 1) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.usage.v1.SetCostCenterResponse.prototype.toObject = function(opt_includeInstance) { + return proto.usage.v1.SetCostCenterResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.usage.v1.SetCostCenterResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.usage.v1.SetCostCenterResponse.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.usage.v1.SetCostCenterResponse} + */ +proto.usage.v1.SetCostCenterResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.usage.v1.SetCostCenterResponse; + return proto.usage.v1.SetCostCenterResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.usage.v1.SetCostCenterResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.usage.v1.SetCostCenterResponse} + */ +proto.usage.v1.SetCostCenterResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.usage.v1.SetCostCenterResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.usage.v1.SetCostCenterResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.usage.v1.SetCostCenterResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.usage.v1.SetCostCenterResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** * Creates an object representation of this proto. @@ -2233,7 +2530,8 @@ proto.usage.v1.CostCenter.prototype.toObject = function(opt_includeInstance) { proto.usage.v1.CostCenter.toObject = function(includeInstance, msg) { var f, obj = { attributionId: jspb.Message.getFieldWithDefault(msg, 1, ""), - spendingLimit: jspb.Message.getFieldWithDefault(msg, 2, 0) + spendingLimit: jspb.Message.getFieldWithDefault(msg, 2, 0), + billingStrategy: jspb.Message.getFieldWithDefault(msg, 3, 0) }; if (includeInstance) { @@ -2278,6 +2576,10 @@ proto.usage.v1.CostCenter.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {number} */ (reader.readInt32()); msg.setSpendingLimit(value); break; + case 3: + var value = /** @type {!proto.usage.v1.CostCenter.BillingStrategy} */ (reader.readEnum()); + msg.setBillingStrategy(value); + break; default: reader.skipField(); break; @@ -2321,9 +2623,24 @@ proto.usage.v1.CostCenter.serializeBinaryToWriter = function(message, writer) { f ); } + f = message.getBillingStrategy(); + if (f !== 0.0) { + writer.writeEnum( + 3, + f + ); + } }; +/** + * @enum {number} + */ +proto.usage.v1.CostCenter.BillingStrategy = { + BILLING_STRATEGY_STRIPE: 0, + BILLING_STRATEGY_OTHER: 1 +}; + /** * optional string attribution_id = 1; * @return {string} @@ -2360,4 +2677,22 @@ proto.usage.v1.CostCenter.prototype.setSpendingLimit = function(value) { }; +/** + * optional BillingStrategy billing_strategy = 3; + * @return {!proto.usage.v1.CostCenter.BillingStrategy} + */ +proto.usage.v1.CostCenter.prototype.getBillingStrategy = function() { + return /** @type {!proto.usage.v1.CostCenter.BillingStrategy} */ (jspb.Message.getFieldWithDefault(this, 3, 0)); +}; + + +/** + * @param {!proto.usage.v1.CostCenter.BillingStrategy} value + * @return {!proto.usage.v1.CostCenter} returns this + */ +proto.usage.v1.CostCenter.prototype.setBillingStrategy = function(value) { + return jspb.Message.setProto3EnumField(this, 3, value); +}; + + goog.object.extend(exports, proto.usage.v1); diff --git a/components/usage-api/usage/v1/usage.proto b/components/usage-api/usage/v1/usage.proto index 311c256d05e05c..950ff720a57ea6 100644 --- a/components/usage-api/usage/v1/usage.proto +++ b/components/usage-api/usage/v1/usage.proto @@ -8,9 +8,12 @@ import "google/protobuf/timestamp.proto"; service UsageService { - // GetCostCenter retrieves the spending limit with its associated attributionID + // GetCostCenter retrieves the active cost center for the given attributionID rpc GetCostCenter(GetCostCenterRequest) returns (GetCostCenterResponse) {} + // SetCostCenter stores the given cost center + rpc SetCostCenter(SetCostCenterRequest) returns (SetCostCenterResponse) {} + // Triggers reconciliation of usage with ledger implementation. rpc ReconcileUsageWithLedger(ReconcileUsageWithLedgerRequest) returns (ReconcileUsageWithLedgerResponse) {} @@ -88,6 +91,13 @@ message Usage { string metadata = 9; } +message SetCostCenterRequest { + CostCenter cost_center = 1; +} + +message SetCostCenterResponse { +} + message GetCostCenterRequest { string attribution_id = 1; } @@ -99,4 +109,9 @@ message GetCostCenterResponse { message CostCenter { string attribution_id = 1; int32 spending_limit = 2; + enum BillingStrategy { + BILLING_STRATEGY_STRIPE = 0; + BILLING_STRATEGY_OTHER = 1; + } + BillingStrategy billing_strategy = 3; } diff --git a/components/usage/pkg/apiv1/usage.go b/components/usage/pkg/apiv1/usage.go index 05052bf8e15b05..7836f4bd1d99c4 100644 --- a/components/usage/pkg/apiv1/usage.go +++ b/components/usage/pkg/apiv1/usage.go @@ -170,14 +170,47 @@ func (s *UsageService) GetCostCenter(ctx context.Context, in *v1.GetCostCenterRe return nil, status.Errorf(codes.Internal, "Failed to get cost center %s from DB: %s", in.AttributionId, err.Error()) } + billingStrategy := v1.CostCenter_BILLING_STRATEGY_OTHER + if result.BillingStrategy == db.CostCenter_Stripe { + billingStrategy = v1.CostCenter_BILLING_STRATEGY_STRIPE + } + return &v1.GetCostCenterResponse{ CostCenter: &v1.CostCenter{ - AttributionId: string(attributionId), - SpendingLimit: result.SpendingLimit, + AttributionId: string(attributionId), + SpendingLimit: result.SpendingLimit, + BillingStrategy: billingStrategy, }, }, nil } +func (s *UsageService) SetCostCenter(ctx context.Context, in *v1.SetCostCenterRequest) (*v1.SetCostCenterResponse, error) { + if in.CostCenter == nil { + return nil, status.Errorf(codes.InvalidArgument, "Empty CostCenter") + } + + attributionID, err := db.ParseAttributionID(in.CostCenter.AttributionId) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "Failed to parse attribution ID: %s", err.Error()) + } + + billingStrategy := db.CostCenter_Other + if in.CostCenter.BillingStrategy == v1.CostCenter_BILLING_STRATEGY_STRIPE { + billingStrategy = db.CostCenter_Stripe + } + + _, err = db.SaveCostCenter(ctx, s.conn, &db.CostCenter{ + ID: attributionID, + SpendingLimit: in.CostCenter.SpendingLimit, + BillingStrategy: billingStrategy, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "Failed to save cost center %s: %s", attributionID, err.Error()) + } + + return &v1.SetCostCenterResponse{}, nil +} + func (s *UsageService) ReconcileUsageWithLedger(ctx context.Context, req *v1.ReconcileUsageWithLedgerRequest) (*v1.ReconcileUsageWithLedgerResponse, error) { from := req.GetFrom().AsTime() to := req.GetTo().AsTime() diff --git a/components/usage/pkg/apiv1/usage_test.go b/components/usage/pkg/apiv1/usage_test.go index acc80800842fd1..0c6c659bd9b26b 100644 --- a/components/usage/pkg/apiv1/usage_test.go +++ b/components/usage/pkg/apiv1/usage_test.go @@ -19,6 +19,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/types/known/timestamppb" + "gorm.io/gorm" ) func TestUsageService_ReconcileUsageWithLedger(t *testing.T) { @@ -54,19 +55,9 @@ func TestUsageService_ReconcileUsageWithLedger(t *testing.T) { Draft: true, })) - srv := baseserver.NewForTests(t, - baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), - ) - - v1.RegisterUsageServiceServer(srv.GRPC(), NewUsageService(dbconn, DefaultWorkspacePricer)) - baseserver.StartServerForTests(t, srv) - - conn, err := grpc.Dial(srv.GRPCAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) + client := newUsageService(t, dbconn) - client := v1.NewUsageServiceClient(conn) - - _, err = client.ReconcileUsageWithLedger(context.Background(), &v1.ReconcileUsageWithLedgerRequest{ + _, err := client.ReconcileUsageWithLedger(context.Background(), &v1.ReconcileUsageWithLedgerRequest{ From: timestamppb.New(from), To: timestamppb.New(to), }) @@ -82,6 +73,21 @@ func TestUsageService_ReconcileUsageWithLedger(t *testing.T) { require.Len(t, usage, 1) } +func newUsageService(t *testing.T, dbconn *gorm.DB) v1.UsageServiceClient { + srv := baseserver.NewForTests(t, + baseserver.WithGRPC(baseserver.MustUseRandomLocalAddress(t)), + ) + + v1.RegisterUsageServiceServer(srv.GRPC(), NewUsageService(dbconn, DefaultWorkspacePricer)) + baseserver.StartServerForTests(t, srv) + + conn, err := grpc.Dial(srv.GRPCAddress(), grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + client := v1.NewUsageServiceClient(conn) + return client +} + func TestReconcileWithLedger(t *testing.T) { now := time.Date(2022, 9, 1, 10, 0, 0, 0, time.UTC) pricer, err := NewWorkspacePricer(map[string]float64{ @@ -210,3 +216,46 @@ func TestReconcileWithLedger(t *testing.T) { require.EqualValues(t, expectedUsage, updates[0]) }) } + +func TestGetAndSetCostCenter(t *testing.T) { + conn := dbtest.ConnectForTests(t) + costCenterUpdates := []*v1.CostCenter{ + { + AttributionId: string(db.NewTeamAttributionID(uuid.New().String())), + SpendingLimit: 5000, + BillingStrategy: v1.CostCenter_BILLING_STRATEGY_OTHER, + }, + { + AttributionId: string(db.NewTeamAttributionID(uuid.New().String())), + SpendingLimit: 8000, + BillingStrategy: v1.CostCenter_BILLING_STRATEGY_OTHER, + }, + { + AttributionId: string(db.NewTeamAttributionID(uuid.New().String())), + SpendingLimit: 8000, + BillingStrategy: v1.CostCenter_BILLING_STRATEGY_STRIPE, + }, + { + AttributionId: string(db.NewTeamAttributionID(uuid.New().String())), + SpendingLimit: 5000, + BillingStrategy: v1.CostCenter_BILLING_STRATEGY_OTHER, + }, + } + + service := newUsageService(t, conn) + + for _, costCenter := range costCenterUpdates { + _, err := service.SetCostCenter(context.Background(), &v1.SetCostCenterRequest{ + CostCenter: costCenter, + }) + require.NoError(t, err) + retrieved, err := service.GetCostCenter(context.Background(), &v1.GetCostCenterRequest{ + AttributionId: costCenter.AttributionId, + }) + require.NoError(t, err) + + require.Equal(t, costCenter.SpendingLimit, retrieved.CostCenter.SpendingLimit) + require.Equal(t, costCenter.BillingStrategy, retrieved.CostCenter.BillingStrategy) + } + +} diff --git a/components/usage/pkg/db/cost_center.go b/components/usage/pkg/db/cost_center.go index 5bb8bc3f2d887c..835ee5f596ff52 100644 --- a/components/usage/pkg/db/cost_center.go +++ b/components/usage/pkg/db/cost_center.go @@ -15,13 +15,20 @@ import ( var CostCenterNotFound = errors.New("CostCenter not found") +type BillingStrategy string + +const ( + CostCenter_Stripe BillingStrategy = "stripe" + CostCenter_Other BillingStrategy = "other" +) + type CostCenter struct { - ID AttributionID `gorm:"primary_key;column:id;type:char;size:36;" json:"id"` - SpendingLimit int32 `gorm:"column:spendingLimit;type:int;default:0;" json:"spendingLimit"` - LastModified time.Time `gorm:"->:column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"` + ID AttributionID `gorm:"primary_key;column:id;type:char;size:36;" json:"id"` + CreationTime VarcharTime `gorm:"primary_key;column:creationTime;type:varchar;size:255;" json:"creationTime"` + SpendingLimit int32 `gorm:"column:spendingLimit;type:int;default:0;" json:"spendingLimit"` + BillingStrategy BillingStrategy `gorm:"column:billingStrategy;type:varchar;size:255;" json:"billingStrategy"` - // deleted is restricted for use by db-sync - _ bool `gorm:"column:deleted;type:tinyint;default:0;" json:"deleted"` + LastModified time.Time `gorm:"->:column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"` } // TableName sets the insert table name for this struct type @@ -31,16 +38,25 @@ func (d *CostCenter) TableName() string { func GetCostCenter(ctx context.Context, conn *gorm.DB, attributionId AttributionID) (*CostCenter, error) { db := conn.WithContext(ctx) - var costCenter CostCenter - - result := db.First(&costCenter, attributionId) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil, CostCenterNotFound - } - return nil, fmt.Errorf("failed to get cost center: %w", result.Error) + var results []CostCenter + db = db.Where("id = ?", attributionId).Order("creationTime DESC").Limit(1).Find(&results) + if db.Error != nil { + return nil, fmt.Errorf("failed to get cost center: %w", db.Error) } - + if len(results) == 0 { + return nil, CostCenterNotFound + } + costCenter := results[0] return &costCenter, nil } + +func SaveCostCenter(ctx context.Context, conn *gorm.DB, costCenter *CostCenter) (*CostCenter, error) { + db := conn.WithContext(ctx) + costCenter.CreationTime = NewVarcharTime(time.Now()) + db = db.Save(costCenter) + if db.Error != nil { + return nil, fmt.Errorf("failed to save cost center: %w", db.Error) + } + return costCenter, nil +}