mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 09:12:02 +10:00
feat: add organisations (#1820)
This commit is contained in:
@ -1,59 +1,163 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type { Stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { createOrganisationClaimUpsertData } from '@documenso/lib/server-only/organisation/create-organisation';
|
||||
import { type Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type OnSubscriptionUpdatedOptions = {
|
||||
userId?: number;
|
||||
teamId?: number;
|
||||
subscription: Stripe.Subscription;
|
||||
previousAttributes: Partial<Stripe.Subscription> | null;
|
||||
};
|
||||
|
||||
type StripeWebhookResponse = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const onSubscriptionUpdated = async ({
|
||||
userId,
|
||||
teamId,
|
||||
subscription,
|
||||
previousAttributes,
|
||||
}: OnSubscriptionUpdatedOptions) => {
|
||||
await prisma.subscription.upsert(
|
||||
mapStripeSubscriptionToPrismaUpsertAction(subscription, userId, teamId),
|
||||
);
|
||||
};
|
||||
const customerId =
|
||||
typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id;
|
||||
|
||||
export const mapStripeSubscriptionToPrismaUpsertAction = (
|
||||
subscription: Stripe.Subscription,
|
||||
userId?: number,
|
||||
teamId?: number,
|
||||
): Prisma.SubscriptionUpsertArgs => {
|
||||
if ((!userId && !teamId) || (userId && teamId)) {
|
||||
throw new Error('Either userId or teamId must be provided.');
|
||||
// Todo: logging
|
||||
if (subscription.items.data.length !== 1) {
|
||||
console.error('No support for multiple items');
|
||||
|
||||
throw Response.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'No support for multiple items',
|
||||
} satisfies StripeWebhookResponse,
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: {
|
||||
customerId,
|
||||
},
|
||||
include: {
|
||||
organisationClaim: true,
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw Response.json(
|
||||
{
|
||||
success: false,
|
||||
message: `Organisation not found`,
|
||||
} satisfies StripeWebhookResponse,
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
if (organisation.subscription?.planId !== subscription.id) {
|
||||
console.error('[WARNING]: Organisation has two subscriptions');
|
||||
}
|
||||
|
||||
const previousItem = previousAttributes?.items?.data[0];
|
||||
const updatedItem = subscription.items.data[0];
|
||||
|
||||
const previousSubscriptionClaimId = previousItem
|
||||
? await extractStripeClaimId(previousItem.price)
|
||||
: null;
|
||||
const updatedSubscriptionClaim = await extractStripeClaim(updatedItem.price);
|
||||
|
||||
if (!updatedSubscriptionClaim) {
|
||||
console.error(`Subscription claim on ${updatedItem.price.id} not found`);
|
||||
|
||||
throw Response.json(
|
||||
{
|
||||
success: false,
|
||||
message: `Subscription claim on ${updatedItem.price.id} not found`,
|
||||
} satisfies StripeWebhookResponse,
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const newClaimFound = previousSubscriptionClaimId !== updatedSubscriptionClaim.id;
|
||||
|
||||
const status = match(subscription.status)
|
||||
.with('active', () => SubscriptionStatus.ACTIVE)
|
||||
.with('past_due', () => SubscriptionStatus.PAST_DUE)
|
||||
.otherwise(() => SubscriptionStatus.INACTIVE);
|
||||
|
||||
return {
|
||||
where: {
|
||||
planId: subscription.id,
|
||||
},
|
||||
create: {
|
||||
status: status,
|
||||
planId: subscription.id,
|
||||
priceId: subscription.items.data[0].price.id,
|
||||
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||
userId: userId ?? null,
|
||||
teamId: teamId ?? null,
|
||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||
},
|
||||
update: {
|
||||
status: status,
|
||||
planId: subscription.id,
|
||||
priceId: subscription.items.data[0].price.id,
|
||||
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||
},
|
||||
};
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.subscription.update({
|
||||
where: {
|
||||
planId: subscription.id,
|
||||
},
|
||||
data: {
|
||||
organisationId: organisation.id,
|
||||
status: status,
|
||||
planId: subscription.id,
|
||||
priceId: subscription.items.data[0].price.id,
|
||||
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||
},
|
||||
});
|
||||
|
||||
// Override current organisation claim if new one is found.
|
||||
if (newClaimFound) {
|
||||
await tx.organisationClaim.update({
|
||||
where: {
|
||||
id: organisation.organisationClaim.id,
|
||||
},
|
||||
data: {
|
||||
originalSubscriptionClaimId: updatedSubscriptionClaim.id,
|
||||
...createOrganisationClaimUpsertData(updatedSubscriptionClaim),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the price metadata for a claimId, if it is missing it will fetch
|
||||
* and check the product metadata for a claimId.
|
||||
*
|
||||
* The order of priority is:
|
||||
* 1. Price metadata
|
||||
* 2. Product metadata
|
||||
*
|
||||
* @returns The claimId or null if no claimId is found.
|
||||
*/
|
||||
export const extractStripeClaimId = async (priceId: Stripe.Price) => {
|
||||
if (priceId.metadata.claimId) {
|
||||
return priceId.metadata.claimId;
|
||||
}
|
||||
|
||||
const productId = typeof priceId.product === 'string' ? priceId.product : priceId.product.id;
|
||||
|
||||
const product = await stripe.products.retrieve(productId);
|
||||
|
||||
return product.metadata.claimId || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the price metadata for a claimId, if it is missing it will fetch
|
||||
* and check the product metadata for a claimId.
|
||||
*
|
||||
*/
|
||||
export const extractStripeClaim = async (priceId: Stripe.Price) => {
|
||||
const claimId = await extractStripeClaimId(priceId);
|
||||
|
||||
if (!claimId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subscriptionClaim = await prisma.subscriptionClaim.findFirst({
|
||||
where: { id: claimId },
|
||||
});
|
||||
|
||||
if (!subscriptionClaim) {
|
||||
console.error(`Subscription claim ${claimId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return subscriptionClaim;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user