Skip to content
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

[payment] cancel all subscriptions on user deletion #4501

Merged
merged 2 commits into from
Aug 12, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ export class TeamSubscriptionService {
const subscriptionsFromTS = subscriptions.filter(s => AssignedTeamSubscription.is(s));
return new SubscriptionModel(userId, subscriptionsFromTS);
}

async findTeamSubscriptionSlotsByAssignee(assigneeId: string): Promise<TeamSubscriptionSlot[]> {
return this.db.findSlotsByAssignee(assigneeId);
}
}

const flatten = <T>(input: T[][]): T[] => {
Expand Down
53 changes: 37 additions & 16 deletions components/server/ee/src/user/user-deletion-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,59 @@ import { SubscriptionService } from "@gitpod/gitpod-payment-endpoint/lib/account
import { Plans } from "@gitpod/gitpod-protocol/lib/plans";
import { ChargebeeService } from "./chargebee-service";
import { EnvEE } from "../env";
import { TeamSubscriptionService } from "@gitpod/gitpod-payment-endpoint/lib/accounting";
import { log } from '@gitpod/gitpod-protocol/lib/util/logging';

@injectable()
export class UserDeletionServiceEE extends UserDeletionService {
@inject(ChargebeeService) protected readonly chargebeeService: ChargebeeService;
@inject(SubscriptionService) protected readonly subscriptionService: SubscriptionService;
@inject(EnvEE) protected readonly env: EnvEE;
@inject(TeamSubscriptionService) protected readonly teamSubscriptionService: TeamSubscriptionService;

async deleteUser(id: string): Promise<void> {
const user = await this.db.findUserById(id);
if (!user) {
throw new Error(`No user with id ${id} found!`);
}

const errors = [];
if (this.env.enablePayment) {
const now = new Date().toISOString();
const subscriptions = await this.subscriptionService.getNotYetCancelledSubscriptions(user, now);
const now = new Date();
const subscriptions = await this.subscriptionService.getNotYetCancelledSubscriptions(user, now.toISOString());
for (const subscription of subscriptions) {
const planId = subscription.planId!;
if (Plans.isFreeNonTransientPlan(planId)) {
// only delete those plans that are persisted in the DB
await this.subscriptionService.unsubscribe(user.id, now, planId);
} else if (Plans.isFreePlan(planId)) {
// we do not care about transient plans
continue;
} else {
// cancel Chargebee subscriptions
const subscriptionId = subscription.uid;
const chargebeeSubscriptionId = subscription.paymentReference!;
await this.chargebeeService.cancelSubscription(chargebeeSubscriptionId, { userId: user.id }, { subscriptionId, chargebeeSubscriptionId });
try {
const planId = subscription.planId!;
const paymentReference = subscription.paymentReference;
if (Plans.isFreeNonTransientPlan(planId)) {
// only delete those plans that are persisted in the DB
await this.subscriptionService.unsubscribe(user.id, now.toISOString(), planId);
} else if (Plans.isFreePlan(planId)) {
// we do not care about transient plans
continue;
} else {
if (!paymentReference) {
const teamSlots = await this.teamSubscriptionService.findTeamSubscriptionSlotsByAssignee(id);
teamSlots.forEach(async ts => await this.teamSubscriptionService.deactivateSlot(ts.teamSubscriptionId, ts.id, now))
} else if (paymentReference.startsWith("github:")) {
throw new Error("Cannot delete user subscription from GitHub")
} else {
// cancel Chargebee subscriptions
const subscriptionId = subscription.uid;
const chargebeeSubscriptionId = subscription.paymentReference!;
await this.chargebeeService.cancelSubscription(chargebeeSubscriptionId, { userId: user.id }, { subscriptionId, chargebeeSubscriptionId });
}
}
} catch (error) {
errors.push(error);
log.error("Error cancelling subscription", error, { subscription })
}
}
}

return super.deleteUser(id);
await super.deleteUser(id);
if (errors) {
throw new Error(errors.join("\n"))
Copy link
Member

@svenefftinge svenefftinge Aug 12, 2021

Choose a reason for hiding this comment

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

why throwing the errors here? We still deleted the user and already logged any errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea was to give feedback to the caller, e.g. the admin dashboard.

In particular in the case of GitHub subscription, it may be worthwhile to notify the customer that their subscription will still be charged as we have no means to cancel it.

}
}
}
}