Skip to content

[Usage-based] When payment is enabled, attribute all workspace instance usage to an explicitly selected "billing account" #10893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 75 additions & 9 deletions components/dashboard/src/settings/Billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,48 @@
* See License-AGPL.txt in the project root for license information.
*/

import { useContext } from "react";
import { Team } from "@gitpod/gitpod-protocol";
import { useContext, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import getSettingsMenu from "./settings-menu";
import { ReactComponent as Spinner } from "../icons/Spinner.svg";
import DropDown from "../components/DropDown";
import { PageWithSubMenu } from "../components/PageWithSubMenu";
import { PaymentContext } from "../payment-context";
import getSettingsMenu from "./settings-menu";
import { getGitpodService } from "../service/service";
import { TeamsContext } from "../teams/teams-context";
import { UserContext } from "../user-context";

export default function Billing() {
const { user } = useContext(UserContext);
const { showPaymentUI, showUsageBasedUI } = useContext(PaymentContext);
const { teams } = useContext(TeamsContext);
const [teamsWithBillingEnabled, setTeamsWithBillingEnabled] = useState<Team[] | undefined>();

useEffect(() => {
if (!teams) {
setTeamsWithBillingEnabled(undefined);
return;
}
const teamsWithBilling: Team[] = [];
Promise.all(
teams.map(async (t) => {
const subscriptionId = await getGitpodService().server.findStripeSubscriptionIdForTeam(t.id);
if (subscriptionId) {
teamsWithBilling.push(t);
}
}),
).then(() => setTeamsWithBillingEnabled(teamsWithBilling));
}, [teams]);

const setUsageAttributionTeam = async (team?: Team) => {
if (!user) {
return;
}
const additionalData = user.additionalData || {};
additionalData.usageAttributionId = team ? `team:${team.id}` : `user:${user.id}`;
await getGitpodService().server.updateLoggedInUser({ additionalData });
};

return (
<PageWithSubMenu
Expand All @@ -21,13 +55,45 @@ export default function Billing() {
>
<h3>Usage-Based Billing</h3>
<h2 className="text-gray-500">Manage usage-based billing, spending limit, and payment method.</h2>
<p className="mt-8">
Hint:{" "}
<Link className="gp-link" to="/teams/new">
Create a team
</Link>{" "}
to set up usage-based billing.
</p>
<div className="mt-8">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

<h3>Billing Account</h3>
{teamsWithBillingEnabled === undefined && <Spinner className="m-2 h-5 w-5 animate-spin" />}
{teamsWithBillingEnabled && teamsWithBillingEnabled.length === 0 && (
<div className="flex space-x-2">
<span>
<Link className="gp-link" to="/teams/new">
Create a team
</Link>{" "}
to set up usage-based billing.
</span>
</div>
)}
{teamsWithBillingEnabled && teamsWithBillingEnabled.length > 0 && (
<div className="flex space-x-2">
<span>Bill all my usage to:</span>
<DropDown
activeEntry={
teamsWithBillingEnabled.find(
(t) => `team:${t.id}` === user?.additionalData?.usageAttributionId,
)?.name
}
customClasses="w-32"
renderAsLink={true}
entries={[
{
title: "(myself)",
onClick: () => setUsageAttributionTeam(undefined),
},
].concat(
teamsWithBillingEnabled.map((t) => ({
title: t.name,
onClick: () => setUsageAttributionTeam(t),
})),
)}
/>
</div>
)}
</div>
</PageWithSubMenu>
);
}
3 changes: 2 additions & 1 deletion components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,10 @@ export interface AdditionalUserData {
oauthClientsApproved?: { [key: string]: string };
// to remember GH Orgs the user installed/updated the GH App for
knownGitHubOrgs?: string[];

// Git clone URL pointing to the user's dotfile repo
dotfileRepo?: string;
// Identifies an explicit team or user ID to which all the user's workspace usage should be attributed to (e.g. for billing purposes)
usageAttributionId?: string;
}

export interface EmailNotificationSettings {
Expand Down
19 changes: 19 additions & 0 deletions components/server/src/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,29 @@ export class UserService {
* Identifies the team or user to which a workspace instance's running time should be attributed to
* (e.g. for usage analytics or billing purposes).
*
* A. Billing-based attribution: If payments are enabled, we attribute all the user's usage to:
* - An explicitly selected billing account (e.g. a team with usage-based billing enabled)
* - Or, we default to the user for all usage (e.g. free tier or individual billing, regardless of project/team)
*
* B. Project-based attribution: If payments are not enabled (e.g. Self-Hosted), we attribute:
* - To the owner of the project (user or team), if the workspace is linked to a project
* - To the user, iff the workspace is not linked to a project
*
* @param user
* @param projectId
*/
async getWorkspaceUsageAttributionId(user: User, projectId?: string): Promise<string | undefined> {
// A. Billing-based attribution
if (this.config.enablePayment) {
if (!user.additionalData?.usageAttributionId) {
// No explicit user attribution ID yet -- attribute all usage to the user by default (regardless of project/team).
return `user:${user.id}`;
}
// Return the user's explicit attribution ID.
return user.additionalData.usageAttributionId;
}

// B. Project-based attribution
if (!projectId) {
// No project -- attribute to the user.
return `user:${user.id}`;
Expand Down