Skip to content

Commit e65dc3a

Browse files
committed
[server][dashboard] Fetch and use the actual cost center billing cycle dates in usage-based Billing pages
1 parent 9258ade commit e65dc3a

File tree

8 files changed

+47
-46
lines changed

8 files changed

+47
-46
lines changed

components/dashboard/src/components/UsageBasedBillingConfig.tsx

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,30 +45,34 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) {
4545

4646
const localStorageKey = `pendingStripeSubscriptionFor${attributionId}`;
4747
const now = dayjs().utc(true);
48-
const billingPeriodFrom = now.startOf("month");
49-
const billingPeriodTo = now.endOf("month");
48+
const [billingCycleFrom, setBillingCycleFrom] = useState<dayjs.Dayjs>(now.startOf("month"));
49+
const [billingCycleTo, setBillingCycleTo] = useState<dayjs.Dayjs>(now.endOf("month"));
50+
51+
const refreshSubscriptionDetails = async (attributionId: string) => {
52+
setStripeSubscriptionId(undefined);
53+
setIsLoadingStripeSubscription(true);
54+
try {
55+
const [subscriptionId, costCenter] = await Promise.all([
56+
getGitpodService().server.findStripeSubscriptionId(attributionId),
57+
getGitpodService().server.getCostCenter(attributionId),
58+
]);
59+
setStripeSubscriptionId(subscriptionId);
60+
setUsageLimit(costCenter?.spendingLimit);
61+
setBillingCycleFrom(dayjs(costCenter?.billingCycleStart || now.startOf("month")).utc(true));
62+
setBillingCycleTo(dayjs(costCenter?.nextBillingTime || now.endOf("month")).utc(true));
63+
} catch (error) {
64+
console.error("Could not get Stripe subscription details.", error);
65+
setErrorMessage(`Could not get Stripe subscription details. ${error?.message || String(error)}`);
66+
} finally {
67+
setIsLoadingStripeSubscription(false);
68+
}
69+
};
5070

5171
useEffect(() => {
5272
if (!attributionId) {
5373
return;
5474
}
55-
(async () => {
56-
setStripeSubscriptionId(undefined);
57-
setIsLoadingStripeSubscription(true);
58-
try {
59-
const [subscriptionId, limit] = await Promise.all([
60-
getGitpodService().server.findStripeSubscriptionId(attributionId),
61-
getGitpodService().server.getUsageLimit(attributionId),
62-
]);
63-
setStripeSubscriptionId(subscriptionId);
64-
setUsageLimit(limit);
65-
} catch (error) {
66-
console.error("Could not get Stripe subscription details.", error);
67-
setErrorMessage(`Could not get Stripe subscription details. ${error?.message || String(error)}`);
68-
} finally {
69-
setIsLoadingStripeSubscription(false);
70-
}
71-
})();
75+
refreshSubscriptionDetails(attributionId);
7276
}, [attributionId]);
7377

7478
useEffect(() => {
@@ -156,8 +160,7 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) {
156160
if (!pollStripeSubscriptionTimeout) {
157161
// Refresh Stripe subscription in 5 seconds in order to poll for upgrade confirmation
158162
const timeout = setTimeout(async () => {
159-
const subscriptionId = await getGitpodService().server.findStripeSubscriptionId(attributionId);
160-
setStripeSubscriptionId(subscriptionId);
163+
await refreshSubscriptionDetails(attributionId);
161164
setPollStripeSubscriptionTimeout(undefined);
162165
}, 5000);
163166
setPollStripeSubscriptionTimeout(timeout);
@@ -178,12 +181,12 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) {
178181
const response = await getGitpodService().server.listUsage({
179182
attributionId,
180183
order: Ordering.ORDERING_DESCENDING,
181-
from: billingPeriodFrom.toDate().getTime(),
184+
from: billingCycleFrom.toDate().getTime(),
182185
to: Date.now(),
183186
});
184187
setCurrentUsage(response.creditsUsed);
185188
})();
186-
}, [attributionId]);
189+
}, [attributionId, billingCycleFrom]);
187190

188191
const showSpinner = !attributionId || isLoadingStripeSubscription || !!pendingStripeSubscription;
189192
const showBalance = !showSpinner && !(AttributionId.parse(attributionId)?.kind === "team" && !stripeSubscriptionId);
@@ -255,8 +258,15 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) {
255258
<div className="flex-grow">
256259
<div className="uppercase text-sm text-gray-400 dark:text-gray-500">Current Period</div>
257260
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">
258-
<span className="font-semibold">{`${billingPeriodFrom.format("MMMM YYYY")}`}</span>{" "}
259-
{`(${billingPeriodFrom.format("MMM D")}` + ` - ${billingPeriodTo.format("MMM D")})`}
261+
<span className="font-semibold">{`${billingCycleFrom.format("MMMM YYYY")}`}</span> (
262+
<span title={billingCycleFrom.toDate().toUTCString().replace("GMT", "UTC")}>
263+
{billingCycleFrom.format("MMM D")}
264+
</span>{" "}
265+
-{" "}
266+
<span title={billingCycleTo.toDate().toUTCString().replace("GMT", "UTC")}>
267+
{billingCycleTo.format("MMM D")}
268+
</span>
269+
)
260270
</div>
261271
</div>
262272
<div>

components/gitpod-protocol/BUILD.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ packages:
55
- :lib
66
- components/gitpod-protocol/go:lib
77
- components/gitpod-protocol/java:lib
8+
- components/usage-api/typescript:lib
89
- name: lib
910
type: yarn
1011
srcs:

components/gitpod-protocol/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"watch": "leeway exec --package .:lib --transitive-dependencies --filter-type yarn --components --parallel -- tsc -w --preserveWatchOutput"
4242
},
4343
"dependencies": {
44+
"@gitpod/usage-api": "0.1.5",
4445
"@types/react": "17.0.32",
4546
"abort-controller-x": "^0.4.0",
4647
"ajv": "^6.5.4",

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { InstallationAdminSettings, TelemetryData } from "./installation-admin-p
6262
import { ListUsageRequest, ListUsageResponse } from "./usage";
6363
import { SupportedWorkspaceClass } from "./workspace-class";
6464
import { BillingMode } from "./billing-mode";
65+
import { CostCenter } from "@gitpod/usage-api/lib/usage/v1/usage.pb";
6566

6667
export interface GitpodClient {
6768
onInstanceUpdate(instance: WorkspaceInstance): void;
@@ -280,7 +281,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
280281
createStripeCustomerIfNeeded(attributionId: string, currency: string): Promise<void>;
281282
subscribeToStripe(attributionId: string, setupIntentId: string, usageLimit: number): Promise<number | undefined>;
282283
getStripePortalUrl(attributionId: string): Promise<string>;
283-
getUsageLimit(attributionId: string): Promise<number | undefined>;
284+
getCostCenter(attributionId: string): Promise<CostCenter | undefined>;
284285
setUsageLimit(attributionId: string, usageLimit: number): Promise<void>;
285286

286287
listUsage(req: ListUsageRequest): Promise<ListUsageResponse>;

components/gitpod-protocol/src/protocol.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { WorkspaceInstance, PortVisibility } from "./workspace-instance";
88
import { RoleOrPermission } from "./permission";
99
import { Project } from "./teams-projects-protocol";
1010
import { createHash } from "crypto";
11-
import { AttributionId } from "./attribution";
1211

1312
export interface UserInfo {
1413
name?: string;
@@ -1518,13 +1517,3 @@ export interface StripeConfig {
15181517
individualUsagePriceIds: { [currency: string]: string };
15191518
teamUsagePriceIds: { [currency: string]: string };
15201519
}
1521-
1522-
export type BillingStrategy = "other" | "stripe";
1523-
export interface CostCenter {
1524-
readonly id: AttributionId;
1525-
/**
1526-
* Unit: credits
1527-
*/
1528-
spendingLimit: number;
1529-
billingStrategy: BillingStrategy;
1530-
}

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import { AccountStatementProvider } from "../user/account-statement-provider";
7373
import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol";
7474
import { ListUsageRequest, ListUsageResponse } from "@gitpod/gitpod-protocol/lib/usage";
7575
import {
76+
CostCenter,
7677
CostCenter_BillingStrategy,
7778
ListUsageRequest_Ordering,
7879
UsageServiceClient,
@@ -2261,21 +2262,18 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
22612262
return url;
22622263
}
22632264

2264-
async getUsageLimit(ctx: TraceContext, attributionId: string): Promise<number | undefined> {
2265+
async getCostCenter(ctx: TraceContext, attributionId: string): Promise<CostCenter | undefined> {
22652266
const attrId = AttributionId.parse(attributionId);
22662267
if (attrId === undefined) {
22672268
log.error(`Invalid attribution id: ${attributionId}`);
22682269
throw new ResponseError(ErrorCodes.BAD_REQUEST, `Invalid attibution id: ${attributionId}`);
22692270
}
22702271

2271-
const user = this.checkAndBlockUser("getUsageLimit");
2272+
const user = this.checkAndBlockUser("getCostCenter");
22722273
await this.guardCostCenterAccess(ctx, user.id, attrId, "get");
22732274

2274-
const costCenter = await this.usageService.getCostCenter({ attributionId });
2275-
if (costCenter?.costCenter) {
2276-
return costCenter.costCenter.spendingLimit;
2277-
}
2278-
return undefined;
2275+
const { costCenter } = await this.usageService.getCostCenter({ attributionId });
2276+
return costCenter;
22792277
}
22802278

22812279
async setUsageLimit(ctx: TraceContext, attributionId: string, usageLimit: number): Promise<void> {

components/server/src/auth/rate-limiter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ const defaultFunctions: FunctionsConfig = {
214214
getPrebuildEvents: { group: "default", points: 1 },
215215
setUsageAttribution: { group: "default", points: 1 },
216216
listAvailableUsageAttributionIds: { group: "default", points: 1 },
217-
getUsageLimit: { group: "default", points: 1 },
217+
getCostCenter: { group: "default", points: 1 },
218218
setUsageLimit: { group: "default", points: 1 },
219219
getNotifications: { group: "default", points: 1 },
220220
getSupportedWorkspaceClasses: { group: "default", points: 1 },

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ import { MessageBusIntegration } from "./messagebus-integration";
181181
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
182182
import * as grpc from "@grpc/grpc-js";
183183
import { CachingBlobServiceClientProvider } from "../util/content-service-sugar";
184+
import { CostCenter } from "@gitpod/usage-api/lib/usage/v1/usage.pb";
184185

185186
// shortcut
186187
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
@@ -3157,7 +3158,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
31573158
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
31583159
}
31593160

3160-
async getUsageLimit(ctx: TraceContext, attributionId: string): Promise<number | undefined> {
3161+
async getCostCenter(ctx: TraceContext, attributionId: string): Promise<CostCenter | undefined> {
31613162
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
31623163
}
31633164

0 commit comments

Comments
 (0)