diff --git a/components/dashboard/src/components/UsageBasedBillingConfig.tsx b/components/dashboard/src/components/UsageBasedBillingConfig.tsx index 31a7022330f713..16f9181e95977f 100644 --- a/components/dashboard/src/components/UsageBasedBillingConfig.tsx +++ b/components/dashboard/src/components/UsageBasedBillingConfig.tsx @@ -45,30 +45,34 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) { const localStorageKey = `pendingStripeSubscriptionFor${attributionId}`; const now = dayjs().utc(true); - const billingPeriodFrom = now.startOf("month"); - const billingPeriodTo = now.endOf("month"); + const [billingCycleFrom, setBillingCycleFrom] = useState(now.startOf("month")); + const [billingCycleTo, setBillingCycleTo] = useState(now.endOf("month")); + + const refreshSubscriptionDetails = async (attributionId: string) => { + setStripeSubscriptionId(undefined); + setIsLoadingStripeSubscription(true); + try { + const [subscriptionId, costCenter] = await Promise.all([ + getGitpodService().server.findStripeSubscriptionId(attributionId), + getGitpodService().server.getCostCenter(attributionId), + ]); + setStripeSubscriptionId(subscriptionId); + setUsageLimit(costCenter?.spendingLimit); + setBillingCycleFrom(dayjs(costCenter?.billingCycleStart || now.startOf("month")).utc(true)); + setBillingCycleTo(dayjs(costCenter?.nextBillingTime || now.endOf("month")).utc(true)); + } catch (error) { + console.error("Could not get Stripe subscription details.", error); + setErrorMessage(`Could not get Stripe subscription details. ${error?.message || String(error)}`); + } finally { + setIsLoadingStripeSubscription(false); + } + }; useEffect(() => { if (!attributionId) { return; } - (async () => { - setStripeSubscriptionId(undefined); - setIsLoadingStripeSubscription(true); - try { - const [subscriptionId, limit] = await Promise.all([ - getGitpodService().server.findStripeSubscriptionId(attributionId), - getGitpodService().server.getUsageLimit(attributionId), - ]); - setStripeSubscriptionId(subscriptionId); - setUsageLimit(limit); - } catch (error) { - console.error("Could not get Stripe subscription details.", error); - setErrorMessage(`Could not get Stripe subscription details. ${error?.message || String(error)}`); - } finally { - setIsLoadingStripeSubscription(false); - } - })(); + refreshSubscriptionDetails(attributionId); }, [attributionId]); useEffect(() => { @@ -156,8 +160,7 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) { if (!pollStripeSubscriptionTimeout) { // Refresh Stripe subscription in 5 seconds in order to poll for upgrade confirmation const timeout = setTimeout(async () => { - const subscriptionId = await getGitpodService().server.findStripeSubscriptionId(attributionId); - setStripeSubscriptionId(subscriptionId); + await refreshSubscriptionDetails(attributionId); setPollStripeSubscriptionTimeout(undefined); }, 5000); setPollStripeSubscriptionTimeout(timeout); @@ -178,12 +181,12 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) { const response = await getGitpodService().server.listUsage({ attributionId, order: Ordering.ORDERING_DESCENDING, - from: billingPeriodFrom.toDate().getTime(), + from: billingCycleFrom.toDate().getTime(), to: Date.now(), }); setCurrentUsage(response.creditsUsed); })(); - }, [attributionId]); + }, [attributionId, billingCycleFrom]); const showSpinner = !attributionId || isLoadingStripeSubscription || !!pendingStripeSubscription; const showBalance = !showSpinner && !(AttributionId.parse(attributionId)?.kind === "team" && !stripeSubscriptionId); @@ -255,8 +258,15 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) {
Current Period
- {`${billingPeriodFrom.format("MMMM YYYY")}`}{" "} - {`(${billingPeriodFrom.format("MMM D")}` + ` - ${billingPeriodTo.format("MMM D")})`} + {`${billingCycleFrom.format("MMMM YYYY")}`} ( + + {billingCycleFrom.format("MMM D")} + {" "} + -{" "} + + {billingCycleTo.format("MMM D")} + + )
diff --git a/components/gitpod-db/src/typeorm/migration/1667919924081-CostCenterBillingCycleStart.ts b/components/gitpod-db/src/typeorm/migration/1667919924081-CostCenterBillingCycleStart.ts new file mode 100644 index 00000000000000..e6cac7bae8a96e --- /dev/null +++ b/components/gitpod-db/src/typeorm/migration/1667919924081-CostCenterBillingCycleStart.ts @@ -0,0 +1,26 @@ +/** + * 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_BILLING_CYCLE_START = "billingCycleStart"; + +export class CostCenterBillingCycleStart1667919924081 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + if (!(await columnExists(queryRunner, D_B_COST_CENTER, COL_BILLING_CYCLE_START))) { + await queryRunner.query( + `ALTER TABLE ${D_B_COST_CENTER} ADD COLUMN ${COL_BILLING_CYCLE_START} varchar(30) NOT NULL, ALGORITHM=INPLACE, LOCK=NONE `, + ); + await queryRunner.query( + `ALTER TABLE ${D_B_COST_CENTER} ADD INDEX(${COL_BILLING_CYCLE_START}), ALGORITHM=INPLACE, LOCK=NONE `, + ); + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/components/gitpod-protocol/BUILD.yaml b/components/gitpod-protocol/BUILD.yaml index 135c9c12a6f1cb..27cd9f478b5a0f 100644 --- a/components/gitpod-protocol/BUILD.yaml +++ b/components/gitpod-protocol/BUILD.yaml @@ -5,6 +5,7 @@ packages: - :lib - components/gitpod-protocol/go:lib - components/gitpod-protocol/java:lib + - components/usage-api/typescript:lib - name: lib type: yarn srcs: diff --git a/components/gitpod-protocol/package.json b/components/gitpod-protocol/package.json index 68fb90cc638cff..7b15d6160676bf 100644 --- a/components/gitpod-protocol/package.json +++ b/components/gitpod-protocol/package.json @@ -41,6 +41,7 @@ "watch": "leeway exec --package .:lib --transitive-dependencies --filter-type yarn --components --parallel -- tsc -w --preserveWatchOutput" }, "dependencies": { + "@gitpod/usage-api": "0.1.5", "@types/react": "17.0.32", "abort-controller-x": "^0.4.0", "ajv": "^6.5.4", diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 84667f96438806..13f2cd92c1966f 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -62,6 +62,7 @@ import { InstallationAdminSettings, TelemetryData } from "./installation-admin-p import { ListUsageRequest, ListUsageResponse } from "./usage"; import { SupportedWorkspaceClass } from "./workspace-class"; import { BillingMode } from "./billing-mode"; +import { CostCenter } from "@gitpod/usage-api/lib/usage/v1/usage.pb"; export interface GitpodClient { onInstanceUpdate(instance: WorkspaceInstance): void; @@ -280,7 +281,7 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, createStripeCustomerIfNeeded(attributionId: string, currency: string): Promise; subscribeToStripe(attributionId: string, setupIntentId: string, usageLimit: number): Promise; getStripePortalUrl(attributionId: string): Promise; - getUsageLimit(attributionId: string): Promise; + getCostCenter(attributionId: string): Promise; setUsageLimit(attributionId: string, usageLimit: number): Promise; listUsage(req: ListUsageRequest): Promise; diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 6425966e5a3d21..61d7fb4072cbfb 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -8,7 +8,6 @@ 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; @@ -1518,13 +1517,3 @@ export interface StripeConfig { individualUsagePriceIds: { [currency: string]: string }; teamUsagePriceIds: { [currency: string]: string }; } - -export type BillingStrategy = "other" | "stripe"; -export interface CostCenter { - readonly id: AttributionId; - /** - * Unit: credits - */ - spendingLimit: number; - billingStrategy: BillingStrategy; -} diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 69dbd79716e2bb..87b48625a7ced8 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -73,6 +73,7 @@ import { AccountStatementProvider } from "../user/account-statement-provider"; import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol"; import { ListUsageRequest, ListUsageResponse } from "@gitpod/gitpod-protocol/lib/usage"; import { + CostCenter, CostCenter_BillingStrategy, ListUsageRequest_Ordering, UsageServiceClient, @@ -2261,21 +2262,18 @@ export class GitpodServerEEImpl extends GitpodServerImpl { return url; } - async getUsageLimit(ctx: TraceContext, attributionId: string): Promise { + async getCostCenter(ctx: TraceContext, attributionId: string): Promise { const attrId = AttributionId.parse(attributionId); if (attrId === undefined) { log.error(`Invalid attribution id: ${attributionId}`); throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`); } - const user = this.checkAndBlockUser("getUsageLimit"); + const user = this.checkAndBlockUser("getCostCenter"); await this.guardCostCenterAccess(ctx, user.id, attrId, "get"); - const costCenter = await this.usageService.getCostCenter({ attributionId }); - if (costCenter?.costCenter) { - return costCenter.costCenter.spendingLimit; - } - return undefined; + const { costCenter } = await this.usageService.getCostCenter({ attributionId }); + return costCenter; } async setUsageLimit(ctx: TraceContext, attributionId: string, usageLimit: number): Promise { diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index ac7c5ea6d5ebcc..b6c886d60646a8 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -214,7 +214,7 @@ const defaultFunctions: FunctionsConfig = { getPrebuildEvents: { group: "default", points: 1 }, setUsageAttribution: { group: "default", points: 1 }, listAvailableUsageAttributionIds: { group: "default", points: 1 }, - getUsageLimit: { group: "default", points: 1 }, + getCostCenter: { group: "default", points: 1 }, setUsageLimit: { group: "default", points: 1 }, getNotifications: { group: "default", points: 1 }, getSupportedWorkspaceClasses: { group: "default", points: 1 }, diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 9e90d100c1ac6c..bb6a644965f66b 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -181,6 +181,7 @@ import { MessageBusIntegration } from "./messagebus-integration"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; import * as grpc from "@grpc/grpc-js"; import { CachingBlobServiceClientProvider } from "../util/content-service-sugar"; +import { CostCenter } from "@gitpod/usage-api/lib/usage/v1/usage.pb"; // shortcut export const traceWI = (ctx: TraceContext, wi: Omit) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager @@ -3157,7 +3158,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } - async getUsageLimit(ctx: TraceContext, attributionId: string): Promise { + async getCostCenter(ctx: TraceContext, attributionId: string): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } diff --git a/components/usage-api/go/v1/usage.pb.go b/components/usage-api/go/v1/usage.pb.go index df15f2aa6b7b6e..a5e17324537982 100644 --- a/components/usage-api/go/v1/usage.pb.go +++ b/components/usage-api/go/v1/usage.pb.go @@ -933,7 +933,8 @@ type CostCenter struct { 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"` // next_billing_time specifies when the next billing cycle happens. Only set when billing strategy is 'other'. This property is readonly. - NextBillingTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=next_billing_time,json=nextBillingTime,proto3" json:"next_billing_time,omitempty"` + NextBillingTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=next_billing_time,json=nextBillingTime,proto3" json:"next_billing_time,omitempty"` + BillingCycleStart *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=billing_cycle_start,json=billingCycleStart,proto3" json:"billing_cycle_start,omitempty"` } func (x *CostCenter) Reset() { @@ -996,6 +997,13 @@ func (x *CostCenter) GetNextBillingTime() *timestamppb.Timestamp { return nil } +func (x *CostCenter) GetBillingCycleStart() *timestamppb.Timestamp { + if x != nil { + return x.BillingCycleStart + } + return nil +} + type ResetUsageRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1181,7 +1189,7 @@ var file_usage_v1_usage_proto_rawDesc = []byte{ 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, 0xbf, 0x02, 0x0a, 0x0a, 0x43, 0x6f, 0x73, 0x74, + 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x22, 0x8b, 0x03, 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, @@ -1196,49 +1204,54 @@ var file_usage_v1_usage_proto_rawDesc = []byte{ 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 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, 0x0f, 0x6e, 0x65, - 0x78, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x54, 0x69, 0x6d, 0x65, 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, 0x22, 0x13, 0x0a, 0x11, 0x52, 0x65, 0x73, - 0x65, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x14, - 0x0a, 0x12, 0x52, 0x65, 0x73, 0x65, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xeb, 0x03, 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, + 0x78, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4a, 0x0a, + 0x13, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x5f, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x18, 0x05, 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, 0x11, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x43, + 0x79, 0x63, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 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, 0x22, 0x13, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x65, 0x74, 0x55, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x14, 0x0a, 0x12, 0x52, 0x65, + 0x73, 0x65, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x32, 0xeb, 0x03, 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, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x43, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x52, + 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, 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, 0x55, 0x0a, - 0x0e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x1f, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, - 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x20, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, - 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x49, 0x0a, 0x0a, 0x52, 0x65, 0x73, 0x65, 0x74, 0x55, 0x73, 0x61, - 0x67, 0x65, 0x12, 0x1b, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x73, 0x65, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1c, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, - 0x55, 0x73, 0x61, 0x67, 0x65, 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, 0x12, 0x49, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1b, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, - 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 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, + 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, 0x55, 0x0a, 0x0e, 0x52, 0x65, 0x63, + 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, 0x75, 0x73, + 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x75, + 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, + 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x49, 0x0a, 0x0a, 0x52, 0x65, 0x73, 0x65, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1b, + 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x75, 0x73, + 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x55, 0x73, 0x61, 0x67, + 0x65, 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, 0x12, 0x49, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x12, 0x1b, 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, + 0x2e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, + 0x61, 0x6e, 0x63, 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 ( @@ -1293,23 +1306,24 @@ var file_usage_v1_usage_proto_depIdxs = []int32{ 16, // 12: usage.v1.GetCostCenterResponse.cost_center:type_name -> usage.v1.CostCenter 2, // 13: usage.v1.CostCenter.billing_strategy:type_name -> usage.v1.CostCenter.BillingStrategy 19, // 14: usage.v1.CostCenter.next_billing_time:type_name -> google.protobuf.Timestamp - 14, // 15: usage.v1.UsageService.GetCostCenter:input_type -> usage.v1.GetCostCenterRequest - 10, // 16: usage.v1.UsageService.SetCostCenter:input_type -> usage.v1.SetCostCenterRequest - 3, // 17: usage.v1.UsageService.ReconcileUsage:input_type -> usage.v1.ReconcileUsageRequest - 17, // 18: usage.v1.UsageService.ResetUsage:input_type -> usage.v1.ResetUsageRequest - 7, // 19: usage.v1.UsageService.ListUsage:input_type -> usage.v1.ListUsageRequest - 12, // 20: usage.v1.UsageService.GetBalance:input_type -> usage.v1.GetBalanceRequest - 15, // 21: usage.v1.UsageService.GetCostCenter:output_type -> usage.v1.GetCostCenterResponse - 11, // 22: usage.v1.UsageService.SetCostCenter:output_type -> usage.v1.SetCostCenterResponse - 4, // 23: usage.v1.UsageService.ReconcileUsage:output_type -> usage.v1.ReconcileUsageResponse - 18, // 24: usage.v1.UsageService.ResetUsage:output_type -> usage.v1.ResetUsageResponse - 8, // 25: usage.v1.UsageService.ListUsage:output_type -> usage.v1.ListUsageResponse - 13, // 26: usage.v1.UsageService.GetBalance:output_type -> usage.v1.GetBalanceResponse - 21, // [21:27] is the sub-list for method output_type - 15, // [15:21] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 19, // 15: usage.v1.CostCenter.billing_cycle_start:type_name -> google.protobuf.Timestamp + 14, // 16: usage.v1.UsageService.GetCostCenter:input_type -> usage.v1.GetCostCenterRequest + 10, // 17: usage.v1.UsageService.SetCostCenter:input_type -> usage.v1.SetCostCenterRequest + 3, // 18: usage.v1.UsageService.ReconcileUsage:input_type -> usage.v1.ReconcileUsageRequest + 17, // 19: usage.v1.UsageService.ResetUsage:input_type -> usage.v1.ResetUsageRequest + 7, // 20: usage.v1.UsageService.ListUsage:input_type -> usage.v1.ListUsageRequest + 12, // 21: usage.v1.UsageService.GetBalance:input_type -> usage.v1.GetBalanceRequest + 15, // 22: usage.v1.UsageService.GetCostCenter:output_type -> usage.v1.GetCostCenterResponse + 11, // 23: usage.v1.UsageService.SetCostCenter:output_type -> usage.v1.SetCostCenterResponse + 4, // 24: usage.v1.UsageService.ReconcileUsage:output_type -> usage.v1.ReconcileUsageResponse + 18, // 25: usage.v1.UsageService.ResetUsage:output_type -> usage.v1.ResetUsageResponse + 8, // 26: usage.v1.UsageService.ListUsage:output_type -> usage.v1.ListUsageResponse + 13, // 27: usage.v1.UsageService.GetBalance:output_type -> usage.v1.GetBalanceResponse + 22, // [22:28] is the sub-list for method output_type + 16, // [16:22] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name } func init() { file_usage_v1_usage_proto_init() } diff --git a/components/usage-api/typescript/src/usage/v1/usage.pb.ts b/components/usage-api/typescript/src/usage/v1/usage.pb.ts index 0faf9d4262f5ab..1837258da9555c 100644 --- a/components/usage-api/typescript/src/usage/v1/usage.pb.ts +++ b/components/usage-api/typescript/src/usage/v1/usage.pb.ts @@ -195,6 +195,7 @@ export interface CostCenter { billingStrategy: CostCenter_BillingStrategy; /** next_billing_time specifies when the next billing cycle happens. Only set when billing strategy is 'other'. This property is readonly. */ nextBillingTime: Date | undefined; + billingCycleStart: Date | undefined; } export enum CostCenter_BillingStrategy { @@ -1077,6 +1078,7 @@ function createBaseCostCenter(): CostCenter { spendingLimit: 0, billingStrategy: CostCenter_BillingStrategy.BILLING_STRATEGY_STRIPE, nextBillingTime: undefined, + billingCycleStart: undefined, }; } @@ -1094,6 +1096,9 @@ export const CostCenter = { if (message.nextBillingTime !== undefined) { Timestamp.encode(toTimestamp(message.nextBillingTime), writer.uint32(34).fork()).ldelim(); } + if (message.billingCycleStart !== undefined) { + Timestamp.encode(toTimestamp(message.billingCycleStart), writer.uint32(42).fork()).ldelim(); + } return writer; }, @@ -1116,6 +1121,9 @@ export const CostCenter = { case 4: message.nextBillingTime = fromTimestamp(Timestamp.decode(reader, reader.uint32())); break; + case 5: + message.billingCycleStart = fromTimestamp(Timestamp.decode(reader, reader.uint32())); + break; default: reader.skipType(tag & 7); break; @@ -1132,6 +1140,7 @@ export const CostCenter = { ? costCenter_BillingStrategyFromJSON(object.billingStrategy) : CostCenter_BillingStrategy.BILLING_STRATEGY_STRIPE, nextBillingTime: isSet(object.nextBillingTime) ? fromJsonTimestamp(object.nextBillingTime) : undefined, + billingCycleStart: isSet(object.billingCycleStart) ? fromJsonTimestamp(object.billingCycleStart) : undefined, }; }, @@ -1142,6 +1151,7 @@ export const CostCenter = { message.billingStrategy !== undefined && (obj.billingStrategy = costCenter_BillingStrategyToJSON(message.billingStrategy)); message.nextBillingTime !== undefined && (obj.nextBillingTime = message.nextBillingTime.toISOString()); + message.billingCycleStart !== undefined && (obj.billingCycleStart = message.billingCycleStart.toISOString()); return obj; }, @@ -1151,6 +1161,7 @@ export const CostCenter = { message.spendingLimit = object.spendingLimit ?? 0; message.billingStrategy = object.billingStrategy ?? CostCenter_BillingStrategy.BILLING_STRATEGY_STRIPE; message.nextBillingTime = object.nextBillingTime ?? undefined; + message.billingCycleStart = object.billingCycleStart ?? undefined; return message; }, }; diff --git a/components/usage-api/usage/v1/usage.proto b/components/usage-api/usage/v1/usage.proto index 7bf0c20f52293a..a1bd26d721f547 100644 --- a/components/usage-api/usage/v1/usage.proto +++ b/components/usage-api/usage/v1/usage.proto @@ -129,6 +129,7 @@ message CostCenter { // next_billing_time specifies when the next billing cycle happens. Only set when billing strategy is 'other'. This property is readonly. google.protobuf.Timestamp next_billing_time = 4; + google.protobuf.Timestamp billing_cycle_start = 5; } message ResetUsageRequest {} diff --git a/components/usage/pkg/apiv1/usage.go b/components/usage/pkg/apiv1/usage.go index e6679a06cadb9b..8899a740879efa 100644 --- a/components/usage/pkg/apiv1/usage.go +++ b/components/usage/pkg/apiv1/usage.go @@ -190,11 +190,20 @@ func (s *UsageService) GetCostCenter(ctx context.Context, in *v1.GetCostCenterRe } func dbCostCenterToAPI(c db.CostCenter) *v1.CostCenter { + NextBillingTime := timestamppb.New(c.NextBillingTime.Time()) + if !c.NextBillingTime.IsSet() { + NextBillingTime = nil + } + BillingCycleStart := timestamppb.New(c.BillingCycleStart.Time()) + if !c.BillingCycleStart.IsSet() { + BillingCycleStart = nil + } return &v1.CostCenter{ - AttributionId: string(c.ID), - SpendingLimit: c.SpendingLimit, - BillingStrategy: convertBillingStrategyToAPI(c.BillingStrategy), - NextBillingTime: timestamppb.New(c.NextBillingTime.Time()), + AttributionId: string(c.ID), + SpendingLimit: c.SpendingLimit, + BillingStrategy: convertBillingStrategyToAPI(c.BillingStrategy), + NextBillingTime: NextBillingTime, + BillingCycleStart: BillingCycleStart, } } diff --git a/components/usage/pkg/db/cost_center.go b/components/usage/pkg/db/cost_center.go index fe056ff8c21a8f..1e3e99c42c9caa 100644 --- a/components/usage/pkg/db/cost_center.go +++ b/components/usage/pkg/db/cost_center.go @@ -27,12 +27,13 @@ const ( ) type CostCenter struct { - 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"` - NextBillingTime VarcharTime `gorm:"column:nextBillingTime;type:varchar;size:255;" json:"nextBillingTime"` - 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"` + BillingCycleStart VarcharTime `gorm:"column:billingCycleStart;type:varchar;size:255;" json:"billingCycleStart"` + NextBillingTime VarcharTime `gorm:"column:nextBillingTime;type:varchar;size:255;" json:"nextBillingTime"` + LastModified time.Time `gorm:"->;column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"` } // TableName sets the insert table name for this struct type @@ -81,11 +82,12 @@ func (c *CostCenterManager) GetOrCreateCostCenter(ctx context.Context, attributi defaultSpendingLimit = c.cfg.ForTeams } result = CostCenter{ - ID: attributionID, - CreationTime: NewVarcharTime(now), - BillingStrategy: CostCenter_Other, - SpendingLimit: defaultSpendingLimit, - NextBillingTime: NewVarcharTime(now.AddDate(0, 1, 0)), + ID: attributionID, + CreationTime: NewVarcharTime(now), + BillingStrategy: CostCenter_Other, + SpendingLimit: defaultSpendingLimit, + BillingCycleStart: NewVarcharTime(now), + NextBillingTime: NewVarcharTime(now.AddDate(0, 1, 0)), } err := c.conn.Save(&result).Error if err != nil { @@ -145,7 +147,8 @@ func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, newCC CostCent // we always update the creationTime newCC.CreationTime = NewVarcharTime(now) - // we don't allow setting the nextBillingTime from outside + // we don't allow setting billingCycleStart or nextBillingTime from outside + newCC.BillingCycleStart = existingCC.BillingCycleStart newCC.NextBillingTime = existingCC.NextBillingTime isTeam := attributionID.IsEntity(AttributionEntity_Team) @@ -169,6 +172,7 @@ func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, newCC CostCent // Downgrading from stripe if existingCC.BillingStrategy == CostCenter_Stripe && newCC.BillingStrategy == CostCenter_Other { newCC.SpendingLimit = c.cfg.ForUsers + newCC.BillingCycleStart = NewVarcharTime(now) // see you next month newCC.NextBillingTime = NewVarcharTime(now.AddDate(0, 1, 0)) } @@ -180,6 +184,7 @@ func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, newCC CostCent return CostCenter{}, err } + newCC.BillingCycleStart = NewVarcharTime(now) // we don't manage stripe billing cycle newCC.NextBillingTime = VarcharTime{} } @@ -195,6 +200,7 @@ func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, newCC CostCent // Downgrading from stripe if existingCC.BillingStrategy == CostCenter_Stripe && newCC.BillingStrategy == CostCenter_Other { newCC.SpendingLimit = c.cfg.ForTeams + newCC.BillingCycleStart = NewVarcharTime(now) // see you next month newCC.NextBillingTime = NewVarcharTime(now.AddDate(0, 1, 0)) } @@ -206,6 +212,7 @@ func (c *CostCenterManager) UpdateCostCenter(ctx context.Context, newCC CostCent return CostCenter{}, err } + newCC.BillingCycleStart = NewVarcharTime(now) // we don't manage stripe billing cycle newCC.NextBillingTime = VarcharTime{} } @@ -311,6 +318,9 @@ func (c *CostCenterManager) ResetUsage(ctx context.Context, cc CostCenter) (Cost now := time.Now().UTC() + // Resetting the usage always resets the billing cycle start time + billingCycleStart := now + // Default to 1 month from now, if there's no nextBillingTime set on the record. nextBillingTime := now.AddDate(0, 1, 0) if cc.NextBillingTime.IsSet() { @@ -325,11 +335,12 @@ func (c *CostCenterManager) ResetUsage(ctx context.Context, cc CostCenter) (Cost // All fields on the new cost center remain the same, except for CreationTime and NextBillingTime newCostCenter := CostCenter{ - ID: cc.ID, - SpendingLimit: spendingLimit, - BillingStrategy: cc.BillingStrategy, - NextBillingTime: NewVarcharTime(nextBillingTime), - CreationTime: NewVarcharTime(now), + ID: cc.ID, + SpendingLimit: spendingLimit, + BillingStrategy: cc.BillingStrategy, + BillingCycleStart: NewVarcharTime(billingCycleStart), + NextBillingTime: NewVarcharTime(nextBillingTime), + CreationTime: NewVarcharTime(now), } err = c.conn.Save(&newCostCenter).Error if err != nil { diff --git a/components/usage/pkg/db/cost_center_test.go b/components/usage/pkg/db/cost_center_test.go index 81e62ffb490c46..6d2a544092fb89 100644 --- a/components/usage/pkg/db/cost_center_test.go +++ b/components/usage/pkg/db/cost_center_test.go @@ -71,26 +71,29 @@ func TestCostCenterManager_GetOrCreateCostCenter_ResetsExpired(t *testing.T) { unexpired := ts.Add(1 * time.Minute) expiredCC := db.CostCenter{ - ID: db.NewTeamAttributionID(uuid.New().String()), - CreationTime: db.NewVarcharTime(now), - SpendingLimit: 0, - BillingStrategy: db.CostCenter_Other, - NextBillingTime: db.NewVarcharTime(expired), + ID: db.NewTeamAttributionID(uuid.New().String()), + CreationTime: db.NewVarcharTime(now), + SpendingLimit: 0, + BillingStrategy: db.CostCenter_Other, + NextBillingTime: db.NewVarcharTime(expired), + BillingCycleStart: db.NewVarcharTime(now), } unexpiredCC := db.CostCenter{ - ID: db.NewUserAttributionID(uuid.New().String()), - CreationTime: db.NewVarcharTime(now), - SpendingLimit: 500, - BillingStrategy: db.CostCenter_Other, - NextBillingTime: db.NewVarcharTime(unexpired), + ID: db.NewUserAttributionID(uuid.New().String()), + CreationTime: db.NewVarcharTime(now), + SpendingLimit: 500, + BillingStrategy: db.CostCenter_Other, + NextBillingTime: db.NewVarcharTime(unexpired), + BillingCycleStart: db.NewVarcharTime(now), } // Stripe billing strategy should not be reset stripeCC := db.CostCenter{ - ID: db.NewUserAttributionID(uuid.New().String()), - CreationTime: db.NewVarcharTime(now), - SpendingLimit: 0, - BillingStrategy: db.CostCenter_Stripe, - NextBillingTime: db.VarcharTime{}, + ID: db.NewUserAttributionID(uuid.New().String()), + CreationTime: db.NewVarcharTime(now), + SpendingLimit: 0, + BillingStrategy: db.CostCenter_Stripe, + NextBillingTime: db.VarcharTime{}, + BillingCycleStart: db.NewVarcharTime(now), } dbtest.CreateCostCenters(t, conn, diff --git a/components/usage/pkg/db/dbtest/cost_center.go b/components/usage/pkg/db/dbtest/cost_center.go index 1ce81c479d77f4..378b7c2d2e6434 100644 --- a/components/usage/pkg/db/dbtest/cost_center.go +++ b/components/usage/pkg/db/dbtest/cost_center.go @@ -18,11 +18,12 @@ func NewCostCenter(t *testing.T, record db.CostCenter) db.CostCenter { t.Helper() result := db.CostCenter{ - ID: db.NewUserAttributionID(uuid.New().String()), - CreationTime: db.NewVarcharTime(time.Now()), - SpendingLimit: 100, - BillingStrategy: db.CostCenter_Stripe, - NextBillingTime: db.NewVarcharTime(time.Now().Add(10 * time.Hour)), + ID: db.NewUserAttributionID(uuid.New().String()), + CreationTime: db.NewVarcharTime(time.Now()), + SpendingLimit: 100, + BillingStrategy: db.CostCenter_Stripe, + BillingCycleStart: db.NewVarcharTime(time.Now()), + NextBillingTime: db.NewVarcharTime(time.Now().Add(10 * time.Hour)), } if record.ID != "" { @@ -38,6 +39,7 @@ func NewCostCenter(t *testing.T, record db.CostCenter) db.CostCenter { result.BillingStrategy = record.BillingStrategy } + result.BillingCycleStart = record.BillingCycleStart result.NextBillingTime = record.NextBillingTime return result