mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 02:32:00 +10:00
feat: wip
This commit is contained in:
@ -71,6 +71,7 @@ export const getServerLimits = async ({ email }: GetServerLimitsOptions) => {
|
||||
const documents = await prisma.document.count({
|
||||
where: {
|
||||
userId: user.id,
|
||||
teamId: null,
|
||||
createdAt: {
|
||||
gte: DateTime.utc().startOf('month').toJSDate(),
|
||||
},
|
||||
|
||||
@ -1,17 +1,21 @@
|
||||
'use server';
|
||||
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
|
||||
export type GetCheckoutSessionOptions = {
|
||||
customerId: string;
|
||||
priceId: string;
|
||||
returnUrl: string;
|
||||
subscriptionMetadata?: Stripe.Metadata;
|
||||
};
|
||||
|
||||
export const getCheckoutSession = async ({
|
||||
customerId,
|
||||
priceId,
|
||||
returnUrl,
|
||||
subscriptionMetadata,
|
||||
}: GetCheckoutSessionOptions) => {
|
||||
'use server';
|
||||
|
||||
@ -26,6 +30,9 @@ export const getCheckoutSession = async ({
|
||||
],
|
||||
success_url: `${returnUrl}?success=true`,
|
||||
cancel_url: `${returnUrl}?canceled=true`,
|
||||
subscription_data: {
|
||||
metadata: subscriptionMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
return session.url;
|
||||
|
||||
@ -78,6 +78,14 @@ export const getStripeCustomerByUser = async (user: User) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getStripeCustomerIdByUser = async (user: User) => {
|
||||
if (user.customerId !== null) {
|
||||
return user.customerId;
|
||||
}
|
||||
|
||||
return await getStripeCustomerByUser(user).then((session) => session.stripeCustomer.id);
|
||||
};
|
||||
|
||||
const syncStripeCustomerSubscriptions = async (userId: number, stripeCustomerId: string) => {
|
||||
const stripeSubscriptions = await stripe.subscriptions.list({
|
||||
customer: stripeCustomerId,
|
||||
|
||||
23
packages/ee/server-only/stripe/get-team-invoices.ts
Normal file
23
packages/ee/server-only/stripe/get-team-invoices.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
|
||||
export type GetTeamInvoicesOptions = {
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const getTeamInvoices = async ({ teamId }: GetTeamInvoicesOptions) => {
|
||||
const teamSubscriptions = await stripe.subscriptions.search({
|
||||
limit: 100,
|
||||
query: `metadata["teamId"]:"${teamId}"`,
|
||||
});
|
||||
|
||||
const subscriptionIds = teamSubscriptions.data.map((subscription) => subscription.id);
|
||||
|
||||
if (subscriptionIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await stripe.invoices.search({
|
||||
query: subscriptionIds.map((id) => `subscription:"${id}"`).join(' OR '),
|
||||
limit: 100,
|
||||
});
|
||||
};
|
||||
96
packages/ee/server-only/stripe/transfer-team-subscription.ts
Normal file
96
packages/ee/server-only/stripe/transfer-team-subscription.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
import {
|
||||
getTeamSeatPriceId,
|
||||
isSomeSubscriptionsActiveAndCommunityPlan,
|
||||
} from '@documenso/lib/utils/billing';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Subscription, Team, User } from '@documenso/prisma/client';
|
||||
|
||||
import { getStripeCustomerByUser } from './get-customer';
|
||||
|
||||
type TransferStripeSubscriptionOptions = {
|
||||
user: User & { Subscription: Subscription[] };
|
||||
team: Team;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transfer the Stripe Team seats subscription from one user to another.
|
||||
*
|
||||
* Will create a new subscription for the new owner and cancel the old one.
|
||||
*
|
||||
* Returns the new subscription, null if no subscription is needed (for community plan).
|
||||
*/
|
||||
export const transferTeamSubscription = async ({
|
||||
user,
|
||||
team,
|
||||
}: TransferStripeSubscriptionOptions) => {
|
||||
const teamSeatPriceId = getTeamSeatPriceId();
|
||||
const { stripeCustomer } = await getStripeCustomerByUser(user);
|
||||
|
||||
const newOwnerHasCommunityPlan = isSomeSubscriptionsActiveAndCommunityPlan(user.Subscription);
|
||||
const currentTeamSubscriptionId = team.subscriptionId;
|
||||
|
||||
let oldSubscription: Stripe.Subscription | null = null;
|
||||
let newSubscription: Stripe.Subscription | null = null;
|
||||
|
||||
if (currentTeamSubscriptionId) {
|
||||
oldSubscription = await stripe.subscriptions.retrieve(currentTeamSubscriptionId);
|
||||
}
|
||||
|
||||
const numberOfSeats = await prisma.teamMember.count({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!newOwnerHasCommunityPlan) {
|
||||
let stripeCreateSubscriptionPayload: Stripe.SubscriptionCreateParams = {
|
||||
customer: stripeCustomer.id,
|
||||
items: [
|
||||
{
|
||||
price: teamSeatPriceId,
|
||||
quantity: numberOfSeats,
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
teamId: team.id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
// If no payment method is attached to the new owner Stripe customer account, send an
|
||||
// invoice instead.
|
||||
if (!stripeCustomer.invoice_settings.default_payment_method) {
|
||||
stripeCreateSubscriptionPayload = {
|
||||
...stripeCreateSubscriptionPayload,
|
||||
collection_method: 'send_invoice',
|
||||
days_until_due: 7,
|
||||
};
|
||||
}
|
||||
|
||||
newSubscription = await stripe.subscriptions.create(stripeCreateSubscriptionPayload);
|
||||
}
|
||||
|
||||
if (oldSubscription) {
|
||||
try {
|
||||
// Set the quantity to 0 so we can refund/charge the old Stripe customer the prorated amount.
|
||||
await stripe.subscriptions.update(oldSubscription.id, {
|
||||
items: oldSubscription.items.data.map((item) => ({
|
||||
id: item.id,
|
||||
quantity: 0,
|
||||
})),
|
||||
});
|
||||
|
||||
await stripe.subscriptions.cancel(oldSubscription.id, {
|
||||
invoice_now: true,
|
||||
prorate: false,
|
||||
});
|
||||
} catch (e) {
|
||||
// Do not error out since we can't easily undo the transfer.
|
||||
// Todo: Teams - Alert us.
|
||||
}
|
||||
}
|
||||
|
||||
return newSubscription;
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
|
||||
export type UpdateSubscriptionItemQuantityOptions = {
|
||||
subscriptionId: string;
|
||||
quantity?: number;
|
||||
priceId: string;
|
||||
};
|
||||
|
||||
export const updateSubscriptionItemQuantity = async ({
|
||||
subscriptionId,
|
||||
quantity,
|
||||
priceId,
|
||||
}: UpdateSubscriptionItemQuantityOptions) => {
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
|
||||
const items = subscription.items.data.filter((item) => item.price.id === priceId);
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await stripe.subscriptions.update(subscriptionId, {
|
||||
items: items.map((item) => ({
|
||||
id: item.id,
|
||||
quantity,
|
||||
})),
|
||||
});
|
||||
};
|
||||
@ -5,6 +5,7 @@ import { match } from 'ts-pattern';
|
||||
|
||||
import type { Stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { createTeamFromPendingTeam } from '@documenso/lib/server-only/team/create-team';
|
||||
import { getFlag } from '@documenso/lib/universal/get-feature-flag';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
@ -110,6 +111,12 @@ export const stripeWebhookHandler = async (
|
||||
|
||||
await onSubscriptionUpdated({ userId, subscription });
|
||||
|
||||
if (
|
||||
subscription.items.data[0].price.id === process.env.NEXT_PUBLIC_STRIPE_TEAM_SEAT_PRICE_ID
|
||||
) {
|
||||
await handleTeamSeatCheckout({ subscription });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Webhook received',
|
||||
@ -282,3 +289,21 @@ export const stripeWebhookHandler = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export type HandleTeamSeatCheckoutOptions = {
|
||||
subscription: Stripe.Subscription;
|
||||
};
|
||||
|
||||
const handleTeamSeatCheckout = async ({ subscription }: HandleTeamSeatCheckoutOptions) => {
|
||||
if (subscription.metadata?.pendingTeamId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingTeamId = Number(subscription.metadata.pendingTeamId);
|
||||
|
||||
if (Number.isNaN(pendingTeamId)) {
|
||||
throw new Error('Invalid pending team ID');
|
||||
}
|
||||
|
||||
await createTeamFromPendingTeam({ pendingTeamId, subscriptionId: subscription.id });
|
||||
};
|
||||
|
||||
@ -13,12 +13,19 @@ export const onSubscriptionUpdated = async ({
|
||||
userId,
|
||||
subscription,
|
||||
}: OnSubscriptionUpdatedOptions) => {
|
||||
await prisma.subscription.upsert(mapStripeSubscriptionToPrismaUpsertAction(userId, subscription));
|
||||
};
|
||||
|
||||
export const mapStripeSubscriptionToPrismaUpsertAction = (
|
||||
userId: number,
|
||||
subscription: Stripe.Subscription,
|
||||
) => {
|
||||
const status = match(subscription.status)
|
||||
.with('active', () => SubscriptionStatus.ACTIVE)
|
||||
.with('past_due', () => SubscriptionStatus.PAST_DUE)
|
||||
.otherwise(() => SubscriptionStatus.INACTIVE);
|
||||
|
||||
await prisma.subscription.upsert({
|
||||
return {
|
||||
where: {
|
||||
planId: subscription.id,
|
||||
},
|
||||
@ -37,5 +44,5 @@ export const onSubscriptionUpdated = async ({
|
||||
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user