mirror of
https://github.com/documenso/documenso.git
synced 2025-11-21 04:01:45 +10:00
feat: billing
This commit is contained in:
@ -1,22 +0,0 @@
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
|
||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||
import { IS_BILLING_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
|
||||
export type CreateBillingPortalOptions = {
|
||||
user: Pick<User, 'id' | 'customerId' | 'email' | 'name'>;
|
||||
};
|
||||
|
||||
export const createBillingPortal = async ({ user }: CreateBillingPortalOptions) => {
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
throw new Error('Billing is not enabled');
|
||||
}
|
||||
|
||||
const { stripeCustomer } = await getStripeCustomerByUser(user);
|
||||
|
||||
return getPortalSession({
|
||||
customerId: stripeCustomer.id,
|
||||
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
|
||||
});
|
||||
};
|
||||
@ -1,39 +0,0 @@
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session';
|
||||
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
|
||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
|
||||
import { getSubscriptionsByUserId } from '../subscription/get-subscriptions-by-user-id';
|
||||
|
||||
export type CreateCheckoutSession = {
|
||||
user: Pick<User, 'id' | 'customerId' | 'email' | 'name'>;
|
||||
priceId: string;
|
||||
};
|
||||
|
||||
export const createCheckoutSession = async ({ user, priceId }: CreateCheckoutSession) => {
|
||||
const { stripeCustomer } = await getStripeCustomerByUser(user);
|
||||
|
||||
const existingSubscriptions = await getSubscriptionsByUserId({ userId: user.id });
|
||||
|
||||
const foundSubscription = existingSubscriptions.find(
|
||||
(subscription) =>
|
||||
subscription.priceId === priceId &&
|
||||
subscription.periodEnd &&
|
||||
subscription.periodEnd >= new Date(),
|
||||
);
|
||||
|
||||
if (foundSubscription) {
|
||||
return getPortalSession({
|
||||
customerId: stripeCustomer.id,
|
||||
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
|
||||
});
|
||||
}
|
||||
|
||||
return getCheckoutSession({
|
||||
customerId: stripeCustomer.id,
|
||||
priceId,
|
||||
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`,
|
||||
});
|
||||
};
|
||||
@ -1,10 +1,8 @@
|
||||
import { hash } from '@node-rs/bcrypt';
|
||||
import type { User } from '@prisma/client';
|
||||
import { OrganisationGroupType, OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '../../constants/app';
|
||||
import { SALT_ROUNDS } from '../../constants/auth';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { createPersonalOrganisation } from '../organisation/create-organisation';
|
||||
@ -14,16 +12,9 @@ export interface CreateUserOptions {
|
||||
email: string;
|
||||
password: string;
|
||||
signature?: string | null;
|
||||
orgUrl: string;
|
||||
}
|
||||
|
||||
export const createUser = async ({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
signature,
|
||||
orgUrl,
|
||||
}: CreateUserOptions) => {
|
||||
export const createUser = async ({ name, email, password, signature }: CreateUserOptions) => {
|
||||
const hashedPassword = await hash(password, SALT_ROUNDS);
|
||||
|
||||
const userExists = await prisma.user.findFirst({
|
||||
@ -36,22 +27,6 @@ export const createUser = async ({
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
// Todo: orgs handle htis
|
||||
if (orgUrl) {
|
||||
const urlExists = await prisma.team.findFirst({
|
||||
where: {
|
||||
url: orgUrl,
|
||||
},
|
||||
});
|
||||
|
||||
if (urlExists) {
|
||||
throw new AppError('PROFILE_URL_TAKEN', {
|
||||
message: 'Profile username is taken',
|
||||
userMessage: 'The profile username is already taken',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.create({
|
||||
data: {
|
||||
@ -76,8 +51,7 @@ export const createUser = async ({
|
||||
return user;
|
||||
});
|
||||
|
||||
await createPersonalOrganisation({ userId: user.id, orgUrl });
|
||||
|
||||
// Not used at the moment, uncomment if required.
|
||||
await onCreateUserHook(user).catch((err) => {
|
||||
// Todo: (RR7) Add logging.
|
||||
console.error(err);
|
||||
@ -87,119 +61,12 @@ export const createUser = async ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Should be run after a user is created.
|
||||
* Should be run after a user is created, example during email password signup or google sign in.
|
||||
*
|
||||
* @returns User
|
||||
*/
|
||||
export const onCreateUserHook = async (user: User) => {
|
||||
const { email } = user;
|
||||
|
||||
const acceptedOrganisationInvites = await prisma.organisationMemberInvite.findMany({
|
||||
where: {
|
||||
status: OrganisationMemberInviteStatus.ACCEPTED,
|
||||
email: {
|
||||
equals: email,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
groups: {
|
||||
where: {
|
||||
type: OrganisationGroupType.INTERNAL_ORGANISATION,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// For each team invite, add the user to the organisation and team, then delete the team invite.
|
||||
// If an error occurs, reset the invitation to not accepted.
|
||||
await Promise.allSettled(
|
||||
acceptedOrganisationInvites.map(async (invite) =>
|
||||
prisma
|
||||
.$transaction(
|
||||
async (tx) => {
|
||||
const organisationGroupToUse = invite.organisation.groups.find(
|
||||
(group) =>
|
||||
group.type === OrganisationGroupType.INTERNAL_ORGANISATION &&
|
||||
group.organisationRole === invite.organisationRole,
|
||||
);
|
||||
|
||||
if (!organisationGroupToUse) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Organisation group not found',
|
||||
});
|
||||
}
|
||||
|
||||
await tx.organisationMember.create({
|
||||
data: {
|
||||
organisationId: invite.organisationId,
|
||||
userId: user.id,
|
||||
organisationGroupMembers: {
|
||||
create: {
|
||||
groupId: organisationGroupToUse.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.organisationMemberInvite.delete({
|
||||
where: {
|
||||
id: invite.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const organisation = await tx.organisation.findFirstOrThrow({
|
||||
where: {
|
||||
id: invite.organisationId,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
subscriptions: {
|
||||
select: {
|
||||
id: true,
|
||||
priceId: true,
|
||||
planId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// const organisationSeatSubscription = // TODO
|
||||
|
||||
// if (organisation.subscriptions) {
|
||||
// await updateSubscriptionItemQuantity({
|
||||
// priceId: team.subscription.priceId,
|
||||
// subscriptionId: team.subscription.planId,
|
||||
// quantity: team.members.length,
|
||||
// });
|
||||
// }
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.catch(async () => {
|
||||
await prisma.organisationMemberInvite.update({
|
||||
where: {
|
||||
id: invite.id,
|
||||
},
|
||||
data: {
|
||||
status: OrganisationMemberInviteStatus.PENDING,
|
||||
},
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
await createPersonalOrganisation({ userId: user.id });
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user