Merge branch 'main' into feat/separate-document-page

This commit is contained in:
Lucas Smith
2024-02-22 22:54:15 +11:00
committed by GitHub
16 changed files with 639 additions and 591 deletions

View File

@ -16,9 +16,8 @@ import { prisma } from '@documenso/prisma';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import type { Prisma } from '@documenso/prisma/client';
import { getDocumentWhereInput } from './get-document-by-id';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { getDocumentWhereInput } from './get-document-by-id';
export type ResendDocumentOptions = {
documentId: number;
@ -111,40 +110,43 @@ export const resendDocument = async ({
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
await prisma.$transaction(async (tx) => {
await mailer.sendMail({
to: {
address: email,
name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: customEmail?.subject
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
: `Please ${actionVerb.toLowerCase()} this document`,
html: render(template),
text: render(template, { plainText: true }),
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user,
requestMetadata,
data: {
emailType: recipientEmailType,
recipientEmail: recipient.email,
recipientName: recipient.name,
recipientRole: recipient.role,
recipientId: recipient.id,
isResending: true,
await prisma.$transaction(
async (tx) => {
await mailer.sendMail({
to: {
address: email,
name,
},
}),
});
});
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: customEmail?.subject
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
: `Please ${actionVerb.toLowerCase()} this document`,
html: render(template),
text: render(template, { plainText: true }),
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user,
requestMetadata,
data: {
emailType: recipientEmailType,
recipientEmail: recipient.email,
recipientName: recipient.name,
recipientRole: recipient.role,
recipientId: recipient.id,
isResending: true,
},
}),
});
},
{ timeout: 30_000 },
);
}),
);
};

View File

@ -49,44 +49,47 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
downloadLink: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}/complete`,
});
await prisma.$transaction(async (tx) => {
await mailer.sendMail({
to: {
address: email,
name,
},
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Signing Complete!',
html: render(template),
text: render(template, { plainText: true }),
attachments: [
{
filename: document.title,
content: Buffer.from(buffer),
await prisma.$transaction(
async (tx) => {
await mailer.sendMail({
to: {
address: email,
name,
},
],
});
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: 'Signing Complete!',
html: render(template),
text: render(template, { plainText: true }),
attachments: [
{
filename: document.title,
content: Buffer.from(buffer),
},
],
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user: null,
requestMetadata,
data: {
emailType: 'DOCUMENT_COMPLETED',
recipientEmail: recipient.email,
recipientName: recipient.name,
recipientId: recipient.id,
recipientRole: recipient.role,
isResending: false,
},
}),
});
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user: null,
requestMetadata,
data: {
emailType: 'DOCUMENT_COMPLETED',
recipientEmail: recipient.email,
recipientName: recipient.name,
recipientId: recipient.id,
recipientRole: recipient.role,
isResending: false,
},
}),
});
},
{ timeout: 30_000 },
);
}),
);
};

View File

@ -108,49 +108,52 @@ export const sendDocument = async ({
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
await prisma.$transaction(async (tx) => {
await mailer.sendMail({
to: {
address: email,
name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: customEmail?.subject
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
: `Please ${actionVerb.toLowerCase()} this document`,
html: render(template),
text: render(template, { plainText: true }),
});
await tx.recipient.update({
where: {
id: recipient.id,
},
data: {
sendStatus: SendStatus.SENT,
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user,
requestMetadata,
data: {
emailType: recipientEmailType,
recipientEmail: recipient.email,
recipientName: recipient.name,
recipientRole: recipient.role,
recipientId: recipient.id,
isResending: false,
await prisma.$transaction(
async (tx) => {
await mailer.sendMail({
to: {
address: email,
name,
},
}),
});
});
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: customEmail?.subject
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
: `Please ${actionVerb.toLowerCase()} this document`,
html: render(template),
text: render(template, { plainText: true }),
});
await tx.recipient.update({
where: {
id: recipient.id,
},
data: {
sendStatus: SendStatus.SENT,
},
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user,
requestMetadata,
data: {
emailType: recipientEmailType,
recipientEmail: recipient.email,
recipientName: recipient.name,
recipientRole: recipient.role,
recipientId: recipient.id,
isResending: false,
},
}),
});
},
{ timeout: 30_000 },
);
}),
);

View File

@ -24,34 +24,38 @@ export const updateTitle = async ({
},
});
return await prisma.$transaction(async (tx) => {
const document = await tx.document.findFirstOrThrow({
where: {
id: documentId,
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
},
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
OR: [
{
userId,
},
{
team: {
members: {
some: {
userId,
},
},
},
],
},
});
},
],
},
});
if (document.title === title) {
return document;
}
if (document.title === title) {
return document;
}
return await prisma.$transaction(async (tx) => {
// Instead of doing everything in a transaction we can use our knowledge
// of the current document title to ensure we aren't performing a conflicting
// update.
const updatedDocument = await tx.document.update({
where: {
id: documentId,
title: document.title,
},
data: {
title,

View File

@ -9,55 +9,58 @@ export type AcceptTeamInvitationOptions = {
};
export const acceptTeamInvitation = async ({ userId, teamId }: AcceptTeamInvitationOptions) => {
await prisma.$transaction(async (tx) => {
const user = await tx.user.findFirstOrThrow({
where: {
id: userId,
},
});
await prisma.$transaction(
async (tx) => {
const user = await tx.user.findFirstOrThrow({
where: {
id: userId,
},
});
const teamMemberInvite = await tx.teamMemberInvite.findFirstOrThrow({
where: {
teamId,
email: user.email,
},
include: {
team: {
include: {
subscription: true,
const teamMemberInvite = await tx.teamMemberInvite.findFirstOrThrow({
where: {
teamId,
email: user.email,
},
include: {
team: {
include: {
subscription: true,
},
},
},
},
});
});
const { team } = teamMemberInvite;
const { team } = teamMemberInvite;
await tx.teamMember.create({
data: {
teamId: teamMemberInvite.teamId,
userId: user.id,
role: teamMemberInvite.role,
},
});
await tx.teamMemberInvite.delete({
where: {
id: teamMemberInvite.id,
},
});
if (IS_BILLING_ENABLED() && team.subscription) {
const numberOfSeats = await tx.teamMember.count({
where: {
await tx.teamMember.create({
data: {
teamId: teamMemberInvite.teamId,
userId: user.id,
role: teamMemberInvite.role,
},
});
await updateSubscriptionItemQuantity({
priceId: team.subscription.priceId,
subscriptionId: team.subscription.planId,
quantity: numberOfSeats,
await tx.teamMemberInvite.delete({
where: {
id: teamMemberInvite.id,
},
});
}
});
if (IS_BILLING_ENABLED() && team.subscription) {
const numberOfSeats = await tx.teamMember.count({
where: {
teamId: teamMemberInvite.teamId,
},
});
await updateSubscriptionItemQuantity({
priceId: team.subscription.priceId,
subscriptionId: team.subscription.planId,
quantity: numberOfSeats,
});
}
},
{ timeout: 30_000 },
);
};

View File

@ -28,56 +28,59 @@ export const createTeamEmailVerification = async ({
data,
}: CreateTeamEmailVerificationOptions) => {
try {
await prisma.$transaction(async (tx) => {
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
await prisma.$transaction(
async (tx) => {
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
},
},
},
},
},
include: {
teamEmail: true,
emailVerification: true,
},
});
include: {
teamEmail: true,
emailVerification: true,
},
});
if (team.teamEmail || team.emailVerification) {
throw new AppError(
AppErrorCode.INVALID_REQUEST,
'Team already has an email or existing email verification.',
);
}
if (team.teamEmail || team.emailVerification) {
throw new AppError(
AppErrorCode.INVALID_REQUEST,
'Team already has an email or existing email verification.',
);
}
const existingTeamEmail = await tx.teamEmail.findFirst({
where: {
email: data.email,
},
});
const existingTeamEmail = await tx.teamEmail.findFirst({
where: {
email: data.email,
},
});
if (existingTeamEmail) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Email already taken by another team.');
}
if (existingTeamEmail) {
throw new AppError(AppErrorCode.ALREADY_EXISTS, 'Email already taken by another team.');
}
const { token, expiresAt } = createTokenVerification({ hours: 1 });
const { token, expiresAt } = createTokenVerification({ hours: 1 });
await tx.teamEmailVerification.create({
data: {
token,
expiresAt,
email: data.email,
name: data.name,
teamId,
},
});
await tx.teamEmailVerification.create({
data: {
token,
expiresAt,
email: data.email,
name: data.name,
teamId,
},
});
await sendTeamEmailVerificationEmail(data.email, token, team.name, team.url);
});
await sendTeamEmailVerificationEmail(data.email, token, team.name, team.url);
},
{ timeout: 30_000 },
);
} catch (err) {
console.error(err);

View File

@ -27,76 +27,81 @@ export const deleteTeamMembers = async ({
teamId,
teamMemberIds,
}: DeleteTeamMembersOptions) => {
await prisma.$transaction(async (tx) => {
// Find the team and validate that the user is allowed to remove members.
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
await prisma.$transaction(
async (tx) => {
// Find the team and validate that the user is allowed to remove members.
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
},
},
},
},
},
include: {
members: {
select: {
id: true,
userId: true,
role: true,
include: {
members: {
select: {
id: true,
userId: true,
role: true,
},
},
subscription: true,
},
});
const currentTeamMember = team.members.find((member) => member.userId === userId);
const teamMembersToRemove = team.members.filter((member) =>
teamMemberIds.includes(member.id),
);
if (!currentTeamMember) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Team member record does not exist');
}
if (teamMembersToRemove.find((member) => member.userId === team.ownerUserId)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove the team owner');
}
const isMemberToRemoveHigherRole = teamMembersToRemove.some(
(member) => !isTeamRoleWithinUserHierarchy(currentTeamMember.role, member.role),
);
if (isMemberToRemoveHigherRole) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove a member with a higher role');
}
// Remove the team members.
await tx.teamMember.deleteMany({
where: {
id: {
in: teamMemberIds,
},
teamId,
userId: {
not: team.ownerUserId,
},
},
subscription: true,
},
});
const currentTeamMember = team.members.find((member) => member.userId === userId);
const teamMembersToRemove = team.members.filter((member) => teamMemberIds.includes(member.id));
if (!currentTeamMember) {
throw new AppError(AppErrorCode.NOT_FOUND, 'Team member record does not exist');
}
if (teamMembersToRemove.find((member) => member.userId === team.ownerUserId)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove the team owner');
}
const isMemberToRemoveHigherRole = teamMembersToRemove.some(
(member) => !isTeamRoleWithinUserHierarchy(currentTeamMember.role, member.role),
);
if (isMemberToRemoveHigherRole) {
throw new AppError(AppErrorCode.UNAUTHORIZED, 'Cannot remove a member with a higher role');
}
// Remove the team members.
await tx.teamMember.deleteMany({
where: {
id: {
in: teamMemberIds,
},
teamId,
userId: {
not: team.ownerUserId,
},
},
});
if (IS_BILLING_ENABLED() && team.subscription) {
const numberOfSeats = await tx.teamMember.count({
where: {
teamId,
},
});
await updateSubscriptionItemQuantity({
priceId: team.subscription.priceId,
subscriptionId: team.subscription.planId,
quantity: numberOfSeats,
});
}
});
if (IS_BILLING_ENABLED() && team.subscription) {
const numberOfSeats = await tx.teamMember.count({
where: {
teamId,
},
});
await updateSubscriptionItemQuantity({
priceId: team.subscription.priceId,
subscriptionId: team.subscription.planId,
quantity: numberOfSeats,
});
}
},
{ timeout: 30_000 },
);
};

View File

@ -9,34 +9,37 @@ export type DeleteTeamOptions = {
};
export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
await prisma.$transaction(async (tx) => {
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
ownerUserId: userId,
},
include: {
subscription: true,
},
});
await prisma.$transaction(
async (tx) => {
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
ownerUserId: userId,
},
include: {
subscription: true,
},
});
if (team.subscription) {
await stripe.subscriptions
.cancel(team.subscription.planId, {
prorate: false,
invoice_now: true,
})
.catch((err) => {
console.error(err);
throw AppError.parseError(err);
});
}
if (team.subscription) {
await stripe.subscriptions
.cancel(team.subscription.planId, {
prorate: false,
invoice_now: true,
})
.catch((err) => {
console.error(err);
throw AppError.parseError(err);
});
}
await tx.team.delete({
where: {
id: teamId,
ownerUserId: userId,
},
});
});
await tx.team.delete({
where: {
id: teamId,
ownerUserId: userId,
},
});
},
{ timeout: 30_000 },
);
};

View File

@ -15,45 +15,48 @@ export type LeaveTeamOptions = {
};
export const leaveTeam = async ({ userId, teamId }: LeaveTeamOptions) => {
await prisma.$transaction(async (tx) => {
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
ownerUserId: {
not: userId,
},
},
include: {
subscription: true,
},
});
await tx.teamMember.delete({
where: {
userId_teamId: {
userId,
teamId,
},
team: {
await prisma.$transaction(
async (tx) => {
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
ownerUserId: {
not: userId,
},
},
},
});
if (IS_BILLING_ENABLED() && team.subscription) {
const numberOfSeats = await tx.teamMember.count({
where: {
teamId,
include: {
subscription: true,
},
});
await updateSubscriptionItemQuantity({
priceId: team.subscription.priceId,
subscriptionId: team.subscription.planId,
quantity: numberOfSeats,
await tx.teamMember.delete({
where: {
userId_teamId: {
userId,
teamId,
},
team: {
ownerUserId: {
not: userId,
},
},
},
});
}
});
if (IS_BILLING_ENABLED() && team.subscription) {
const numberOfSeats = await tx.teamMember.count({
where: {
teamId,
},
});
await updateSubscriptionItemQuantity({
priceId: team.subscription.priceId,
subscriptionId: team.subscription.planId,
quantity: numberOfSeats,
});
}
},
{ timeout: 30_000 },
);
};

View File

@ -44,63 +44,66 @@ export const requestTeamOwnershipTransfer = async ({
// Todo: Clear payment methods disabled for now.
const clearPaymentMethods = false;
await prisma.$transaction(async (tx) => {
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
ownerUserId: userId,
members: {
some: {
userId: newOwnerUserId,
await prisma.$transaction(
async (tx) => {
const team = await tx.team.findFirstOrThrow({
where: {
id: teamId,
ownerUserId: userId,
members: {
some: {
userId: newOwnerUserId,
},
},
},
},
});
});
const newOwnerUser = await tx.user.findFirstOrThrow({
where: {
id: newOwnerUserId,
},
});
const newOwnerUser = await tx.user.findFirstOrThrow({
where: {
id: newOwnerUserId,
},
});
const { token, expiresAt } = createTokenVerification({ minute: 10 });
const { token, expiresAt } = createTokenVerification({ minute: 10 });
const teamVerificationPayload = {
teamId,
token,
expiresAt,
userId: newOwnerUserId,
name: newOwnerUser.name ?? '',
email: newOwnerUser.email,
clearPaymentMethods,
};
await tx.teamTransferVerification.upsert({
where: {
const teamVerificationPayload = {
teamId,
},
create: teamVerificationPayload,
update: teamVerificationPayload,
});
token,
expiresAt,
userId: newOwnerUserId,
name: newOwnerUser.name ?? '',
email: newOwnerUser.email,
clearPaymentMethods,
};
const template = createElement(TeamTransferRequestTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
senderName: userName,
teamName: team.name,
teamUrl: team.url,
token,
});
await tx.teamTransferVerification.upsert({
where: {
teamId,
},
create: teamVerificationPayload,
update: teamVerificationPayload,
});
await mailer.sendMail({
to: newOwnerUser.email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: `You have been requested to take ownership of team ${team.name} on Documenso`,
html: render(template),
text: render(template, { plainText: true }),
});
});
const template = createElement(TeamTransferRequestTemplate, {
assetBaseUrl: WEBAPP_BASE_URL,
baseUrl: WEBAPP_BASE_URL,
senderName: userName,
teamName: team.name,
teamUrl: team.url,
token,
});
await mailer.sendMail({
to: newOwnerUser.email,
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: `You have been requested to take ownership of team ${team.name} on Documenso`,
html: render(template),
text: render(template, { plainText: true }),
});
},
{ timeout: 30_000 },
);
};

View File

@ -17,49 +17,52 @@ export const resendTeamEmailVerification = async ({
userId,
teamId,
}: ResendTeamMemberInvitationOptions) => {
await prisma.$transaction(async (tx) => {
const team = await tx.team.findUniqueOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
await prisma.$transaction(
async (tx) => {
const team = await tx.team.findUniqueOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
},
},
},
},
},
include: {
emailVerification: true,
},
});
include: {
emailVerification: true,
},
});
if (!team) {
throw new AppError('TeamNotFound', 'User is not a member of the team.');
}
if (!team) {
throw new AppError('TeamNotFound', 'User is not a member of the team.');
}
const { emailVerification } = team;
const { emailVerification } = team;
if (!emailVerification) {
throw new AppError(
'VerificationNotFound',
'No team email verification exists for this team.',
);
}
if (!emailVerification) {
throw new AppError(
'VerificationNotFound',
'No team email verification exists for this team.',
);
}
const { token, expiresAt } = createTokenVerification({ hours: 1 });
const { token, expiresAt } = createTokenVerification({ hours: 1 });
await tx.teamEmailVerification.update({
where: {
teamId,
},
data: {
token,
expiresAt,
},
});
await tx.teamEmailVerification.update({
where: {
teamId,
},
data: {
token,
expiresAt,
},
});
await sendTeamEmailVerificationEmail(emailVerification.email, token, team.name, team.url);
});
await sendTeamEmailVerificationEmail(emailVerification.email, token, team.name, team.url);
},
{ timeout: 30_000 },
);
};

View File

@ -35,42 +35,45 @@ export const resendTeamMemberInvitation = async ({
teamId,
invitationId,
}: ResendTeamMemberInvitationOptions) => {
await prisma.$transaction(async (tx) => {
const team = await tx.team.findUniqueOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
await prisma.$transaction(
async (tx) => {
const team = await tx.team.findUniqueOrThrow({
where: {
id: teamId,
members: {
some: {
userId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
},
},
},
},
},
});
});
if (!team) {
throw new AppError('TeamNotFound', 'User is not a valid member of the team.');
}
if (!team) {
throw new AppError('TeamNotFound', 'User is not a valid member of the team.');
}
const teamMemberInvite = await tx.teamMemberInvite.findUniqueOrThrow({
where: {
id: invitationId,
teamId,
},
});
const teamMemberInvite = await tx.teamMemberInvite.findUniqueOrThrow({
where: {
id: invitationId,
teamId,
},
});
if (!teamMemberInvite) {
throw new AppError('InviteNotFound', 'No invite exists for this user.');
}
if (!teamMemberInvite) {
throw new AppError('InviteNotFound', 'No invite exists for this user.');
}
await sendTeamMemberInviteEmail({
email: teamMemberInvite.email,
token: teamMemberInvite.token,
teamName: team.name,
teamUrl: team.url,
senderName: userName,
});
});
await sendTeamMemberInviteEmail({
email: teamMemberInvite.email,
token: teamMemberInvite.token,
teamName: team.name,
teamUrl: team.url,
senderName: userName,
});
},
{ timeout: 30_000 },
);
};

View File

@ -11,78 +11,81 @@ export type TransferTeamOwnershipOptions = {
};
export const transferTeamOwnership = async ({ token }: TransferTeamOwnershipOptions) => {
await prisma.$transaction(async (tx) => {
const teamTransferVerification = await tx.teamTransferVerification.findFirstOrThrow({
where: {
token,
},
include: {
team: {
include: {
subscription: true,
await prisma.$transaction(
async (tx) => {
const teamTransferVerification = await tx.teamTransferVerification.findFirstOrThrow({
where: {
token,
},
include: {
team: {
include: {
subscription: true,
},
},
},
},
});
const { team, userId: newOwnerUserId } = teamTransferVerification;
await tx.teamTransferVerification.delete({
where: {
teamId: team.id,
},
});
const newOwnerUser = await tx.user.findFirstOrThrow({
where: {
id: newOwnerUserId,
teamMembers: {
some: {
teamId: team.id,
},
},
},
include: {
Subscription: true,
},
});
let teamSubscription: Stripe.Subscription | null = null;
if (IS_BILLING_ENABLED()) {
teamSubscription = await transferTeamSubscription({
user: newOwnerUser,
team,
clearPaymentMethods: teamTransferVerification.clearPaymentMethods,
});
}
if (teamSubscription) {
await tx.subscription.upsert(
mapStripeSubscriptionToPrismaUpsertAction(teamSubscription, undefined, team.id),
);
}
const { team, userId: newOwnerUserId } = teamTransferVerification;
await tx.team.update({
where: {
id: team.id,
},
data: {
ownerUserId: newOwnerUserId,
members: {
update: {
where: {
userId_teamId: {
teamId: team.id,
userId: newOwnerUserId,
await tx.teamTransferVerification.delete({
where: {
teamId: team.id,
},
});
const newOwnerUser = await tx.user.findFirstOrThrow({
where: {
id: newOwnerUserId,
teamMembers: {
some: {
teamId: team.id,
},
},
},
include: {
Subscription: true,
},
});
let teamSubscription: Stripe.Subscription | null = null;
if (IS_BILLING_ENABLED()) {
teamSubscription = await transferTeamSubscription({
user: newOwnerUser,
team,
clearPaymentMethods: teamTransferVerification.clearPaymentMethods,
});
}
if (teamSubscription) {
await tx.subscription.upsert(
mapStripeSubscriptionToPrismaUpsertAction(teamSubscription, undefined, team.id),
);
}
await tx.team.update({
where: {
id: team.id,
},
data: {
ownerUserId: newOwnerUserId,
members: {
update: {
where: {
userId_teamId: {
teamId: team.id,
userId: newOwnerUserId,
},
},
data: {
role: TeamMemberRole.ADMIN,
},
},
data: {
role: TeamMemberRole.ADMIN,
},
},
},
},
});
});
});
},
{ timeout: 30_000 },
);
};

View File

@ -53,47 +53,50 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
await Promise.allSettled(
acceptedTeamInvites.map(async (invite) =>
prisma
.$transaction(async (tx) => {
await tx.teamMember.create({
data: {
teamId: invite.teamId,
userId: user.id,
role: invite.role,
},
});
await tx.teamMemberInvite.delete({
where: {
id: invite.id,
},
});
if (!IS_BILLING_ENABLED()) {
return;
}
const team = await tx.team.findFirstOrThrow({
where: {
id: invite.teamId,
},
include: {
members: {
select: {
id: true,
},
.$transaction(
async (tx) => {
await tx.teamMember.create({
data: {
teamId: invite.teamId,
userId: user.id,
role: invite.role,
},
subscription: true,
},
});
if (team.subscription) {
await updateSubscriptionItemQuantity({
priceId: team.subscription.priceId,
subscriptionId: team.subscription.planId,
quantity: team.members.length,
});
}
})
await tx.teamMemberInvite.delete({
where: {
id: invite.id,
},
});
if (!IS_BILLING_ENABLED()) {
return;
}
const team = await tx.team.findFirstOrThrow({
where: {
id: invite.teamId,
},
include: {
members: {
select: {
id: true,
},
},
subscription: true,
},
});
if (team.subscription) {
await updateSubscriptionItemQuantity({
priceId: team.subscription.priceId,
subscriptionId: team.subscription.planId,
quantity: team.members.length,
});
}
},
{ timeout: 30_000 },
)
.catch(async () => {
await prisma.teamMemberInvite.update({
where: {