mirror of
https://github.com/documenso/documenso.git
synced 2025-11-21 04:01:45 +10:00
feat: billing
This commit is contained in:
@ -3,6 +3,7 @@ import { OrganisationGroupType, OrganisationMemberInviteStatus } from '@prisma/c
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { jobs } from '../../jobs/client';
|
||||
|
||||
export type AcceptOrganisationInvitationOptions = {
|
||||
token: string;
|
||||
@ -21,7 +22,8 @@ export const acceptOrganisationInvitation = async ({
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
subscriptions: true,
|
||||
subscription: true,
|
||||
organisationClaim: true,
|
||||
groups: {
|
||||
include: {
|
||||
teamGroups: true,
|
||||
@ -46,19 +48,10 @@ export const acceptOrganisationInvitation = async ({
|
||||
},
|
||||
});
|
||||
|
||||
// If no user exists for the invitation, accept the invitation and create the organisation
|
||||
// user when the user signs up.
|
||||
if (!user) {
|
||||
await prisma.organisationMemberInvite.update({
|
||||
where: {
|
||||
id: organisationMemberInvite.id,
|
||||
},
|
||||
data: {
|
||||
status: OrganisationMemberInviteStatus.ACCEPTED,
|
||||
},
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User must exist to accept an organisation invitation',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { organisation } = organisationMemberInvite;
|
||||
@ -98,28 +91,13 @@ export const acceptOrganisationInvitation = async ({
|
||||
},
|
||||
});
|
||||
|
||||
// Todo: Orgs
|
||||
// if (IS_BILLING_ENABLED() && team.subscription) {
|
||||
// const numberOfSeats = await tx.teamMember.count({
|
||||
// where: {
|
||||
// teamId: organisationMemberInvite.teamId,
|
||||
// },
|
||||
// });
|
||||
|
||||
// await updateSubscriptionItemQuantity({
|
||||
// priceId: team.subscription.priceId,
|
||||
// subscriptionId: team.subscription.planId,
|
||||
// quantity: numberOfSeats,
|
||||
// });
|
||||
// }
|
||||
|
||||
// await jobs.triggerJob({
|
||||
// name: 'send.team-member-joined.email',
|
||||
// payload: {
|
||||
// teamId: teamMember.teamId,
|
||||
// memberId: teamMember.id,
|
||||
// },
|
||||
// });
|
||||
await jobs.triggerJob({
|
||||
name: 'send.organisation-member-joined.email',
|
||||
payload: {
|
||||
organisationId: organisation.id,
|
||||
memberUserId: user.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
|
||||
@ -5,6 +5,7 @@ import type { Organisation, OrganisationGlobalSettings, Prisma } from '@prisma/c
|
||||
import { OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { OrganisationInviteEmailTemplate } from '@documenso/email/templates/organisation-invite';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
@ -16,12 +17,11 @@ import { prisma } from '@documenso/prisma';
|
||||
import type { TCreateOrganisationMemberInvitesRequestSchema } from '@documenso/trpc/server/organisation-router/create-organisation-member-invites.types';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import {
|
||||
buildOrganisationWhereQuery,
|
||||
getHighestOrganisationRoleInGroup,
|
||||
} from '../../utils/organisations';
|
||||
import { validateIfSubscriptionIsRequired } from '../../utils/billing';
|
||||
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { organisationGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { getMemberOrganisationRole } from '../team/get-member-roles';
|
||||
|
||||
export type CreateOrganisationMemberInvitesOptions = {
|
||||
userId: number;
|
||||
@ -56,8 +56,14 @@ export const createOrganisationMemberInvites = async ({
|
||||
},
|
||||
},
|
||||
},
|
||||
invites: true,
|
||||
invites: {
|
||||
where: {
|
||||
status: OrganisationMemberInviteStatus.PENDING,
|
||||
},
|
||||
},
|
||||
organisationGlobalSettings: true,
|
||||
organisationClaim: true,
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -65,38 +71,20 @@ export const createOrganisationMemberInvites = async ({
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const currentOrganisationMember = await prisma.organisationMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
organisationId,
|
||||
},
|
||||
include: {
|
||||
organisationGroupMembers: {
|
||||
include: {
|
||||
group: true,
|
||||
},
|
||||
},
|
||||
const { organisationClaim } = organisation;
|
||||
|
||||
const subscription = validateIfSubscriptionIsRequired(organisation.subscription);
|
||||
|
||||
const currentOrganisationMemberRole = await getMemberOrganisationRole({
|
||||
organisationId: organisation.id,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!currentOrganisationMember) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const currentOrganisationMemberRole = getHighestOrganisationRoleInGroup(
|
||||
currentOrganisationMember.organisationGroupMembers.map((member) => member.group),
|
||||
);
|
||||
|
||||
const organisationMemberEmails = organisation.members.map((member) => member.user.email);
|
||||
const organisationMemberInviteEmails = organisation.invites
|
||||
.filter((invite) => invite.status === OrganisationMemberInviteStatus.PENDING)
|
||||
.map((invite) => invite.email);
|
||||
|
||||
if (!currentOrganisationMember) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'User not part of organisation.',
|
||||
});
|
||||
}
|
||||
const organisationMemberInviteEmails = organisation.invites.map((invite) => invite.email);
|
||||
|
||||
const usersToInvite = invitations.filter((invitation) => {
|
||||
// Filter out users that are already members of the organisation.
|
||||
@ -123,7 +111,6 @@ export const createOrganisationMemberInvites = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// Todo: (orgs)
|
||||
const organisationMemberInvites: Prisma.OrganisationMemberInviteCreateManyInput[] =
|
||||
usersToInvite.map(({ email, organisationRole }) => ({
|
||||
email,
|
||||
@ -132,9 +119,21 @@ export const createOrganisationMemberInvites = async ({
|
||||
token: nanoid(32),
|
||||
}));
|
||||
|
||||
console.log({
|
||||
organisationMemberInvites,
|
||||
});
|
||||
const numberOfCurrentMembers = organisation.members.length;
|
||||
const numberOfCurrentInvites = organisation.invites.length;
|
||||
const numberOfNewInvites = organisationMemberInvites.length;
|
||||
|
||||
const totalMemberCountWithInvites =
|
||||
numberOfCurrentMembers + numberOfCurrentInvites + numberOfNewInvites;
|
||||
|
||||
// Handle billing for seat based plans.
|
||||
if (subscription) {
|
||||
await syncMemberCountWithStripeSeatPlan(
|
||||
subscription,
|
||||
organisationClaim,
|
||||
totalMemberCountWithInvites,
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.organisationMemberInvite.createMany({
|
||||
data: organisationMemberInvites,
|
||||
|
||||
@ -5,32 +5,45 @@ import { prisma } from '@documenso/prisma';
|
||||
import { ORGANISATION_INTERNAL_GROUPS } from '../../constants/organisations';
|
||||
import { AppErrorCode } from '../../errors/app-error';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import { alphaid } from '../../universal/id';
|
||||
import { alphaid, generatePrefixedId } from '../../universal/id';
|
||||
import { generateDefaultOrganisationSettings } from '../../utils/organisations';
|
||||
import { generateDefaultOrganisationClaims } from '../../utils/organisations-claims';
|
||||
import { createTeam } from '../team/create-team';
|
||||
|
||||
type CreateOrganisationOptions = {
|
||||
userId: number;
|
||||
name: string;
|
||||
url: string;
|
||||
url?: string;
|
||||
customerId?: string;
|
||||
};
|
||||
|
||||
export const createOrganisation = async ({ name, url, userId }: CreateOrganisationOptions) => {
|
||||
export const createOrganisation = async ({
|
||||
name,
|
||||
url,
|
||||
userId,
|
||||
customerId,
|
||||
}: CreateOrganisationOptions) => {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const organisationSetting = await tx.organisationGlobalSettings.create({
|
||||
data: generateDefaultOrganisationSettings(),
|
||||
});
|
||||
|
||||
const organisationClaim = await tx.organisationClaim.create({
|
||||
data: generateDefaultOrganisationClaims(),
|
||||
});
|
||||
|
||||
const organisation = await tx.organisation
|
||||
.create({
|
||||
data: {
|
||||
name,
|
||||
url, // Todo: orgs constraint this
|
||||
url: url || generatePrefixedId('org'),
|
||||
ownerUserId: userId,
|
||||
organisationGlobalSettingsId: organisationSetting.id,
|
||||
organisationClaimId: organisationClaim.id,
|
||||
groups: {
|
||||
create: ORGANISATION_INTERNAL_GROUPS,
|
||||
},
|
||||
customerId,
|
||||
},
|
||||
include: {
|
||||
groups: true,
|
||||
@ -86,7 +99,7 @@ export const createPersonalOrganisation = async ({
|
||||
const organisation = await createOrganisation({
|
||||
name: 'Personal Organisation',
|
||||
userId,
|
||||
url: orgUrl || `org_${alphaid(8)}`,
|
||||
url: orgUrl,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
@ -94,7 +107,7 @@ export const createPersonalOrganisation = async ({
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Todo: (orgs) Add logging.
|
||||
// Todo: (LOGS)
|
||||
});
|
||||
|
||||
if (organisation) {
|
||||
@ -106,7 +119,8 @@ export const createPersonalOrganisation = async ({
|
||||
inheritMembers: true,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
// Todo: (orgs) Add logging.
|
||||
|
||||
// Todo: (LOGS)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export const getOrganisationClaim = async ({ organisationId }: { organisationId: string }) => {
|
||||
const organisationClaim = await prisma.organisationClaim.findFirst({
|
||||
where: {
|
||||
organisation: {
|
||||
id: organisationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisationClaim) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
return organisationClaim;
|
||||
};
|
||||
|
||||
export const getOrganisationClaimByTeamId = async ({ teamId }: { teamId: number }) => {
|
||||
const organisationClaim = await prisma.organisationClaim.findFirst({
|
||||
where: {
|
||||
organisation: {
|
||||
teams: {
|
||||
some: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisationClaim) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
return organisationClaim;
|
||||
};
|
||||
Reference in New Issue
Block a user