fix: cancel platform subscription on dashboard (#20787)
* add new endpoint to cancel user subscription * remove comments * add delete button to frontend * update correct request method * fixup * frontend for cancelling subscription * better warning message * better function name * update locales * better error handling in case stripe api call fails * rename teamId to organizationId --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
Delete,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiExcludeController } from "@nestjs/swagger";
|
||||
@@ -96,6 +97,19 @@ export class BillingController {
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:organizationId/unsubscribe")
|
||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||
@MembershipRoles(["OWNER", "ADMIN"])
|
||||
async cancelTeamSubscriptionInStripe(
|
||||
@Param("organizationId") organizationId: number
|
||||
): Promise<ApiResponse> {
|
||||
await this.billingService.cancelTeamSubscription(organizationId);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/webhook")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async stripeWebhook(
|
||||
|
||||
@@ -7,6 +7,7 @@ import { OrganizationsRepository } from "@/modules/organizations/index/organizat
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { InjectQueue } from "@nestjs/bull";
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
@@ -313,6 +314,28 @@ export class BillingService implements OnModuleDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async cancelTeamSubscription(teamId: number) {
|
||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||
const customerId = teamWithBilling?.platformBilling?.customerId;
|
||||
|
||||
if (!customerId) {
|
||||
throw new NotFoundException("No customer id found for team in Stripe");
|
||||
}
|
||||
|
||||
if (!teamWithBilling?.platformBilling || !teamWithBilling?.platformBilling.subscriptionId) {
|
||||
throw new NotFoundException("Team plan not found");
|
||||
}
|
||||
|
||||
try {
|
||||
await this.stripeService
|
||||
.getStripe()
|
||||
.subscriptions.cancel(teamWithBilling?.platformBilling?.subscriptionId);
|
||||
} catch (error) {
|
||||
this.logger.log(error, "error while cancelling team subscription in stripe");
|
||||
throw new BadRequestException("Failed to cancel team subscription");
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
await this.billingQueue.close();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
export const useUnsubscribeTeamToStripe = (
|
||||
{ onSuccess, onError, teamId }: { teamId?: number | null; onSuccess: () => void; onError: () => void } = {
|
||||
onSuccess: () => {
|
||||
return;
|
||||
},
|
||||
onError: () => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const mutation = useMutation<ApiResponse, unknown>({
|
||||
mutationFn: (data) => {
|
||||
return fetch(`/api/v2/billing/${teamId}/unsubscribe`, {
|
||||
method: "delete",
|
||||
headers: { "Content-type": "application/json" },
|
||||
}).then((res) => res?.json());
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.status === SUCCESS_STATUS) {
|
||||
onSuccess?.();
|
||||
} else {
|
||||
onError?.();
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
onError?.();
|
||||
},
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
||||
@@ -2,12 +2,17 @@
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { DialogTrigger, ConfirmationDialogContent } from "@calcom/ui/components/dialog";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { PlatformPricing } from "@calcom/web/components/settings/platform/pricing/platform-pricing/index";
|
||||
|
||||
import { useUnsubscribeTeamToStripe } from "@lib/hooks/settings/platform/billing/useUnsubscribeTeamToStripe";
|
||||
|
||||
import NoPlatformPlan from "@components/settings/platform/dashboard/NoPlatformPlan";
|
||||
import { useGetUserAttributes } from "@components/settings/platform/hooks/useGetUserAttributes";
|
||||
|
||||
@@ -37,6 +42,18 @@ export default function PlatformBillingUpgrade() {
|
||||
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, userBillingData, isPaidUser, userOrgId } =
|
||||
useGetUserAttributes();
|
||||
|
||||
const { mutateAsync: removeTeamSubscription, isPending: isRemoveTeamSubscriptionLoading } =
|
||||
useUnsubscribeTeamToStripe({
|
||||
onSuccess: () => {
|
||||
window.location.href = "/settings/platform/";
|
||||
showToast(t("team_subscription_cancelled_successfully"), "success");
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("team_subscription_cancellation_error"), "error");
|
||||
},
|
||||
teamId: userOrgId,
|
||||
});
|
||||
|
||||
if (isUserLoading || (isUserBillingDataLoading && !userBillingData)) {
|
||||
return <div className="m-5">{t("loading")}</div>;
|
||||
}
|
||||
@@ -92,6 +109,18 @@ export default function PlatformBillingUpgrade() {
|
||||
|
||||
<hr className="border-subtle" />
|
||||
|
||||
<CtaRow title="Cancel subscription" description={t("Cancel your existing platform subscription")}>
|
||||
<CancelSubscriptionButton
|
||||
buttonClassName="hidden me-2 sm:inline"
|
||||
isPending={isRemoveTeamSubscriptionLoading}
|
||||
handleDelete={() => {
|
||||
removeTeamSubscription();
|
||||
}}
|
||||
/>
|
||||
</CtaRow>
|
||||
|
||||
<hr className="border-subtle" />
|
||||
|
||||
<CtaRow title={t("need_anything_else")} description={t("further_billing_help")}>
|
||||
<Button color="secondary" onClick={onContactSupportClick}>
|
||||
{t("contact_support")}
|
||||
@@ -103,3 +132,41 @@ export default function PlatformBillingUpgrade() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CancelSubscriptionButton = ({
|
||||
buttonClassName,
|
||||
isPending,
|
||||
onDeleteConfirmed,
|
||||
handleDelete,
|
||||
}: {
|
||||
onDeleteConfirmed?: () => void;
|
||||
buttonClassName: string;
|
||||
handleDelete: () => void;
|
||||
isPending: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="destructive" className={buttonClassName}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<ConfirmationDialogContent
|
||||
isPending={isPending}
|
||||
variety="danger"
|
||||
title={t("cancel_subscription")}
|
||||
confirmBtnText={t("confirm_subscription_cancellation")}
|
||||
loadingText={t("confirm_subscription_cancellation")}
|
||||
cancelBtnText={t("back")}
|
||||
onConfirm={() => {
|
||||
handleDelete();
|
||||
onDeleteConfirmed?.();
|
||||
}}>
|
||||
{t("cancel_subscription_description")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -725,6 +725,9 @@
|
||||
"remove_member_confirmation_message": "Are you sure you want to remove this member from the team?",
|
||||
"confirm_disband_team": "Yes, disband team",
|
||||
"confirm_remove_member": "Yes, remove member",
|
||||
"confirm_subscription_cancellation": "Yes, cancel subscription",
|
||||
"cancel_subscription": "Cancel subscription",
|
||||
"cancel_subscription_description": "Cancelling your subscription will permanently deactivate your current plan. This action cannot be undone.",
|
||||
"remove_member": "Remove member",
|
||||
"manage_your_team": "Manage your team",
|
||||
"no_teams": "You don't have any teams yet.",
|
||||
@@ -2681,6 +2684,8 @@
|
||||
"proceeding_agreement": "By proceeding, you agree to our",
|
||||
"terms": "Terms",
|
||||
"always_show_x_days": "Always {{x}} days available",
|
||||
"team_subscription_cancelled_successfully": "Team subscription cancelled successfully",
|
||||
"team_subscription_cancellation_error": "Error while cancelling the team subscription",
|
||||
"unable_to_subscribe_to_the_platform": "An error occurred while trying to subscribe to the platform plan, please try again later",
|
||||
"updating_oauth_client_error": "An error occurred while updating the OAuth client, please try again later",
|
||||
"creating_oauth_client_error": "An error occurred while creating the OAuth client, please try again later",
|
||||
|
||||
Reference in New Issue
Block a user