Skip to content

Commit

Permalink
fix: cancel Stripe subscription when account is deleted
Browse files Browse the repository at this point in the history
Also fix typings for purchase in database
  • Loading branch information
gregberge committed Jan 27, 2024
1 parent 46b501f commit f6eba78
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 15 deletions.
23 changes: 20 additions & 3 deletions apps/backend/src/database/models/Purchase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ export class Purchase extends Model {
},
status: {
type: ["string"],
enum: ["active", "canceled", "trialing", "past_due"],
enum: [
"active",
"canceled",
"trialing",
"past_due",
"incomplete",
"unpaid",
"incomplete_expired",
"paused",
],
},
endDate: { type: ["string", "null"] },
startDate: { type: ["string"] },
Expand All @@ -40,12 +49,20 @@ export class Purchase extends Model {
planId!: string;
purchaserId!: string | null;
stripeSubscriptionId!: string | null;
source!: string;
source!: "github" | "stripe";
endDate!: string | null;
startDate!: string;
trialEndDate!: string | null;
paymentMethodFilled!: boolean | null;
status!: string;
status!:
| "active"
| "canceled"
| "trialing"
| "past_due"
| "incomplete"
| "unpaid"
| "incomplete_expired"
| "paused";

static override get relationMappings(): RelationMappings {
return {
Expand Down
10 changes: 9 additions & 1 deletion apps/backend/src/graphql/__generated__/resolver-types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions apps/backend/src/graphql/definitions/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export const typeDefs = gql`
past_due
"Post-cancelation date"
canceled
"Incomplete"
incomplete
"Incomplete expired"
incomplete_expired
"Unpaid"
unpaid
"Paused"
paused
}
enum TrialStatus {
Expand Down
7 changes: 7 additions & 0 deletions apps/backend/src/graphql/services/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@/database/models/index.js";

import { deleteProject } from "./project.js";
import { cancelStripeSubscription } from "@/stripe/index.js";

export const getWritableAccount = async (args: {
id: string;
Expand Down Expand Up @@ -40,6 +41,12 @@ export const deleteAccount = async (args: {
});
}),
);
const subscription = account.$getSubscription();
const activePurchase = await subscription.getActivePurchase();
// Cancel the Stripe subscription if it exists
if (activePurchase?.stripeSubscriptionId) {
await cancelStripeSubscription(activePurchase.stripeSubscriptionId);
}
await Purchase.query().where("accountId", account.id).delete();
await account.$query().delete();

Expand Down
42 changes: 33 additions & 9 deletions apps/backend/src/stripe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,22 @@ export const createPurchaseFromSubscription = async ({
});
};

export async function cancelStripeSubscription(subscriptionId: string) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
if (subscription.status === "canceled") {
return;
}
await stripe.subscriptions.cancel(subscriptionId);
}

const getPurchaseFromStripeSubscriptionId = async (
stripeSubscriptionId: string,
) => {
const purchase = await Purchase.query().findOne({
stripeSubscriptionId,
});

if (!purchase) {
throw new Error(
`Purchase not found for subscription ${stripeSubscriptionId}`,
);
}

return purchase;
return purchase ?? null;
};

export const getStripePriceFromPlanOrThrow = async (plan: Plan) => {
Expand Down Expand Up @@ -322,6 +324,11 @@ export const handleStripeEvent = async ({
const purchase = await getPurchaseFromStripeSubscriptionId(
subscription.id,
);
if (!purchase) {
throw new Error(
`No purchase found for stripe subscription id ${subscription.id}`,
);
}
await updatePurchaseFromSubscription(purchase, subscription);
}

Expand Down Expand Up @@ -353,16 +360,33 @@ export const handleStripeEvent = async ({

return;
}
case "customer.subscription.updated":
case "customer.subscription.deleted": {
case "customer.subscription.updated": {
const subscription = data.object as Stripe.Subscription;
const purchase = await getPurchaseFromStripeSubscriptionId(
subscription.id,
);
if (!purchase) {
throw new Error(
`No purchase found for stripe subscription id ${subscription.id}`,
);
}
await updatePurchaseFromSubscription(purchase, subscription);
return;
}

case "customer.subscription.deleted": {
const subscription = data.object as Stripe.Subscription;
const purchase = await getPurchaseFromStripeSubscriptionId(
subscription.id,
);
// Purchase can be null in case the team as been deleted
if (!purchase) {
return;
}
await purchase.$query().patch({ status: "canceled" });
return;
}

default:
console.log(`Unhandled event type ${type}`);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/synchronize/github/updatePurchase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const updatePurchase = async (
Purchase.query(trx)
.patch({
endDate: effectiveDate,
status: new Date(effectiveDate) > new Date() ? "cancelled" : "active",
status: new Date(effectiveDate) > new Date() ? "canceled" : "active",
})
.findById(activePurchase.id),
Purchase.query(trx).insert({
Expand Down
10 changes: 9 additions & 1 deletion apps/frontend/src/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,12 +630,20 @@ export enum PurchaseStatus {
Active = 'active',
/** Post-cancelation date */
Canceled = 'canceled',
/** Incomplete */
Incomplete = 'incomplete',
/** Incomplete expired */
IncompleteExpired = 'incomplete_expired',
/** No paid purchase */
Missing = 'missing',
/** Payment due */
PastDue = 'past_due',
/** Paused */
Paused = 'paused',
/** Ongoing trial */
Trialing = 'trialing'
Trialing = 'trialing',
/** Unpaid */
Unpaid = 'unpaid'
}

export type Query = {
Expand Down

0 comments on commit f6eba78

Please sign in to comment.