From 37185796943f88827693bc853865b2fa8deb9175 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Mon, 23 May 2022 07:42:01 +0000 Subject: [PATCH 1/5] [dashboard] Un-hide the new Team Billing menu entry This reverts commit 3479ced5f778babcc84e99a13328c9e8367ea224. --- components/dashboard/src/teams/TeamSettings.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/dashboard/src/teams/TeamSettings.tsx b/components/dashboard/src/teams/TeamSettings.tsx index 1f0fd0e8e8135a..310feab4b2f145 100644 --- a/components/dashboard/src/teams/TeamSettings.tsx +++ b/components/dashboard/src/teams/TeamSettings.tsx @@ -24,10 +24,10 @@ export function getTeamSettingsMenu(params: { team?: Team; showPaymentUI?: boole }, ...(showPaymentUI ? [ - // { - // title: "Billing", - // link: [`/t/${team?.slug}/billing`], - // }, + { + title: "Billing", + link: [`/t/${team?.slug}/billing`], + }, ] : []), ]; From 4de3308760c53e1e310f7476cc62c87f22cf6f57 Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Fri, 27 May 2022 07:04:06 +0000 Subject: [PATCH 2/5] [payment] Fix race condition on multiple new Team Plan joins --- .../chargebee/team-subscription-handler.ts | 31 +++++++++++++++++++ .../ee/src/workspace/gitpod-server-impl.ts | 30 +----------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/components/ee/payment-endpoint/src/chargebee/team-subscription-handler.ts b/components/ee/payment-endpoint/src/chargebee/team-subscription-handler.ts index 0281232caf32ab..7f9ba6a2448035 100644 --- a/components/ee/payment-endpoint/src/chargebee/team-subscription-handler.ts +++ b/components/ee/payment-endpoint/src/chargebee/team-subscription-handler.ts @@ -11,9 +11,11 @@ import { TeamSubscription2DB } from "@gitpod/gitpod-db/lib/team-subscription-2-d import { log, LogContext } from "@gitpod/gitpod-protocol/lib/util/logging"; import { Plans } from "@gitpod/gitpod-protocol/lib/plans"; import { TeamSubscription, TeamSubscription2 } from "@gitpod/gitpod-protocol/lib/team-subscription-protocol"; +import { formatDate } from "@gitpod/gitpod-protocol/lib/util/date-time"; import { getCancelledAt, getStartDate } from "./chargebee-subscription-helper"; import { Chargebee as chargebee } from "./chargebee-types"; import { EventHandler } from "./chargebee-event-handler"; +import { UpgradeHelper } from "./upgrade-helper"; import { TeamSubscriptionService } from "../accounting/team-subscription-service"; import { TeamSubscription2Service } from "../accounting/team-subscription2-service"; import { Config } from "../config"; @@ -25,6 +27,7 @@ export class TeamSubscriptionHandler implements EventHandler): boolean { if (event.event_type.startsWith("subscription")) { @@ -133,6 +136,34 @@ export class TeamSubscriptionHandler implements EventHandler sub.quantity) { + // Upgrade: Charge for it! + const oldQuantity = sub.quantity; + const newQuantity = chargebeeSubscription.plan_quantity; + let pricePerUnitInCents = chargebeeSubscription.plan_unit_price; + if (pricePerUnitInCents === undefined) { + const plan = Plans.getById(sub.planId)!; + pricePerUnitInCents = plan.pricePerMonth * 100; + } + const currentTermRemainingRatio = + this.upgradeHelper.getCurrentTermRemainingRatio(chargebeeSubscription); + const diffInCents = Math.round( + pricePerUnitInCents * (newQuantity - oldQuantity) * currentTermRemainingRatio, + ); + const upgradeTimestamp = new Date().toISOString(); + const dateString = formatDate(upgradeTimestamp); + const description = `Pro-rated upgrade from ${oldQuantity} to ${newQuantity} team members (${dateString})`; + this.upgradeHelper + .chargeForUpgrade("", sub.paymentReference, diffInCents, description, upgradeTimestamp) + .catch((error) => { + log.error(`Could not charge for upgrade on TeamSubscription2 quantity increase!`, { + error, + ts2Id: sub.id, + chargebeeId: sub.paymentReference, + oldQuantity, + newQuantity, + }); + }); } sub.quantity = chargebeeSubscription.plan_quantity; await db2.storeEntry(sub); diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 78803426991ac5..f548b63701a558 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -1436,37 +1436,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl { protected async updateTeamSubscriptionQuantity(teamSubscription: TeamSubscription2): Promise { const members = await this.teamDB.findMembersByTeam(teamSubscription.teamId); - const oldQuantity = teamSubscription.quantity; const newQuantity = members.length; try { - if (oldQuantity < newQuantity) { - // Upgrade: Charge for it! - const chargebeeSubscription = await this.getChargebeeSubscription( - {}, - teamSubscription.paymentReference, - ); - let pricePerUnitInCents = chargebeeSubscription.plan_unit_price; - if (pricePerUnitInCents === undefined) { - const plan = Plans.getById(teamSubscription.planId)!; - pricePerUnitInCents = plan.pricePerMonth * 100; - } - const currentTermRemainingRatio = - this.upgradeHelper.getCurrentTermRemainingRatio(chargebeeSubscription); - const diffInCents = Math.round( - pricePerUnitInCents * (newQuantity - oldQuantity) * currentTermRemainingRatio, - ); - const upgradeTimestamp = new Date().toISOString(); - const description = `Pro-rated upgrade from '${oldQuantity}' to '${newQuantity}' team members (${formatDate( - upgradeTimestamp, - )})`; - await this.upgradeHelper.chargeForUpgrade( - "", - teamSubscription.paymentReference, - diffInCents, - description, - upgradeTimestamp, - ); - } + // We only charge for upgrades in the Chargebee callback, to avoid race conditions. await this.doUpdateSubscription("", teamSubscription.paymentReference, { plan_quantity: newQuantity, end_of_term: false, From 2c1f29adf84786334096d2b6f00e8e90df4b382e Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Mon, 30 May 2022 08:44:55 +0000 Subject: [PATCH 3/5] =?UTF-8?q?[dashboard]=20Rename=20DropDown=20'contextM?= =?UTF-8?q?enuWidth'=20=E2=86=92=20'customClasses'=20to=20reflect=20realit?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/dashboard/src/Menu.tsx | 2 +- components/dashboard/src/admin/TeamDetail.tsx | 2 +- components/dashboard/src/components/ContextMenu.tsx | 4 ++-- components/dashboard/src/components/DropDown.tsx | 4 ++-- .../dashboard/src/components/PendingChangesDropdown.tsx | 2 +- components/dashboard/src/projects/NewProject.tsx | 5 ++++- components/dashboard/src/projects/Prebuilds.tsx | 2 +- components/dashboard/src/teams/Members.tsx | 4 ++-- components/dashboard/src/teams/TeamBilling.tsx | 2 +- components/dashboard/src/workspaces/Workspaces.tsx | 2 +- 10 files changed, 16 insertions(+), 13 deletions(-) diff --git a/components/dashboard/src/Menu.tsx b/components/dashboard/src/Menu.tsx index a8c73a0a4d28c8..2d7207fb3529ab 100644 --- a/components/dashboard/src/Menu.tsx +++ b/components/dashboard/src/Menu.tsx @@ -281,7 +281,7 @@ export default function Menu() { )}
diff --git a/components/dashboard/src/components/DropDown.tsx b/components/dashboard/src/components/DropDown.tsx index 5fc668cf099ee3..4e01ab65249897 100644 --- a/components/dashboard/src/components/DropDown.tsx +++ b/components/dashboard/src/components/DropDown.tsx @@ -10,7 +10,7 @@ import ContextMenu from "./ContextMenu"; export interface DropDownProps { prefix?: string; - contextMenuWidth?: string; + customClasses?: string; activeEntry?: string; entries: DropDownEntry[]; } @@ -38,7 +38,7 @@ function DropDown(props: DropDownProps) { const font = "text-gray-400 dark:text-gray-500 text-sm leading-1 group hover:text-gray-600 dark:hover:text-gray-400 transition ease-in-out"; return ( - + {props.prefix} {current} diff --git a/components/dashboard/src/components/PendingChangesDropdown.tsx b/components/dashboard/src/components/PendingChangesDropdown.tsx index c95e166ef09f4d..249871ec85d452 100644 --- a/components/dashboard/src/components/PendingChangesDropdown.tsx +++ b/components/dashboard/src/components/PendingChangesDropdown.tsx @@ -41,7 +41,7 @@ export default function PendingChangesDropdown(props: { workspaceInstance?: Work return
No Changes
; } return ( - +

{totalChanges} Change{totalChanges === 1 ? "" : "s"} diff --git a/components/dashboard/src/projects/NewProject.tsx b/components/dashboard/src/projects/NewProject.tsx index 1ddeb8a7855ccf..7470529c33322b 100644 --- a/components/dashboard/src/projects/NewProject.tsx +++ b/components/dashboard/src/projects/NewProject.tsx @@ -376,7 +376,10 @@ export default function NewProject() {

- +
{!selectedAccount && user && user.name && user.avatarUrl && ( <> diff --git a/components/dashboard/src/projects/Prebuilds.tsx b/components/dashboard/src/projects/Prebuilds.tsx index 1c5ce557946577..441a654e806512 100644 --- a/components/dashboard/src/projects/Prebuilds.tsx +++ b/components/dashboard/src/projects/Prebuilds.tsx @@ -180,7 +180,7 @@ export default function (props: { project?: Project; isAdminDashboard?: boolean
- +
{!isLoadingPrebuilds && prebuilds.length === 0 && !props.isAdminDashboard && (
@@ -219,10 +226,14 @@ export default function TeamBilling() { )} {!isLoading && teamPlan && ( <> - +
-
{teamPlan.name}
-
Unlimited hours
+
+ {teamPlan.name} +
+
+ Unlimited hours +
Includes:
{(featuresByPlanType[teamPlan.type] || []).map((f) => ( @@ -232,25 +243,31 @@ export default function TeamBilling() { ))}
-
+
{!teamSubscription ? ( - +
) : ( - +
-
Members
-
{members.length}
-
Next invoice on
-
+
+ Members +
+
+ {members.length} +
+
+ Next invoice on +
+
{guessNextInvoiceDate(teamSubscription.startDate).toDateString()}
-
+