fix: revert canceled individual subscriptions to free claim (#2483)

## Description

Resolves an issue where individual plan customers who cancel are not
correctly put down to the free plan.

To resolve this, we delete the subscription on the stripe subscription
delete webhook. Since the customerId is stored on the organisation they
can still access their old invoices.
This commit is contained in:
David Nguyen 2026-02-12 17:44:33 +11:00 committed by GitHub
parent 066e6bc847
commit 9bcb240895
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,19 +1,89 @@
import { SubscriptionStatus } from '@prisma/client';
import { createOrganisationClaimUpsertData } from '@documenso/lib/server-only/organisation/create-organisation';
import type { Stripe } from '@documenso/lib/server-only/stripe';
import { INTERNAL_CLAIM_ID, internalClaims } from '@documenso/lib/types/subscription';
import { prisma } from '@documenso/prisma';
import { extractStripeClaimId } from './on-subscription-updated';
export type OnSubscriptionDeletedOptions = {
subscription: Stripe.Subscription;
};
export const onSubscriptionDeleted = async ({ subscription }: OnSubscriptionDeletedOptions) => {
await prisma.subscription.update({
const existingSubscription = await prisma.subscription.findUnique({
where: {
planId: subscription.id,
},
include: {
organisation: {
include: {
organisationClaim: true,
},
},
},
});
// If the subscription doesn't exist, we don't need to do anything.
if (!existingSubscription) {
return;
}
const subscriptionClaimId = await extractClaimIdFromStripeSubscription(subscription);
// Individuals get their subscription deleted so they can return to the
// free plan.
if (subscriptionClaimId === INTERNAL_CLAIM_ID.INDIVIDUAL) {
await prisma.$transaction(async (tx) => {
await tx.subscription.delete({
where: {
id: existingSubscription.id,
},
});
await tx.organisationClaim.update({
where: {
id: existingSubscription.organisation.organisationClaim.id,
},
data: {
originalSubscriptionClaimId: INTERNAL_CLAIM_ID.FREE,
...createOrganisationClaimUpsertData(internalClaims[INTERNAL_CLAIM_ID.FREE]),
},
});
});
return;
}
// For all other cases, mark the subscription as inactive since
// they should still have a "Personal" account.
await prisma.subscription.update({
where: {
id: existingSubscription.id,
},
data: {
status: SubscriptionStatus.INACTIVE,
},
});
};
/**
* Extracts the claim ID from the Stripe subscription.
*
* Returns `null` if no claim ID found.
*/
const extractClaimIdFromStripeSubscription = async (subscription: Stripe.Subscription) => {
const deletedItem = subscription.items.data[0];
if (!deletedItem) {
return null;
}
try {
return await extractStripeClaimId(deletedItem.price);
} catch (error) {
console.error(error);
return null;
}
};