mirror of
https://github.com/documenso/documenso.git
synced 2025-11-24 13:41:30 +10:00
feat: billing
This commit is contained in:
33
packages/lib/client-only/providers/organisation.tsx
Normal file
33
packages/lib/client-only/providers/organisation.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { OrganisationSession } from '@documenso/trpc/server/organisation-router/get-organisation-session.types';
|
||||
|
||||
type OrganisationProviderValue = OrganisationSession;
|
||||
|
||||
interface OrganisationProviderProps {
|
||||
children: React.ReactNode;
|
||||
organisation: OrganisationProviderValue | null;
|
||||
}
|
||||
|
||||
const OrganisationContext = createContext<OrganisationProviderValue | null>(null);
|
||||
|
||||
export const useCurrentOrganisation = () => {
|
||||
const context = useContext(OrganisationContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCurrentOrganisation must be used within a OrganisationProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useOptionalCurrentOrganisation = () => {
|
||||
return useContext(OrganisationContext);
|
||||
};
|
||||
|
||||
export const OrganisationProvider = ({ children, organisation }: OrganisationProviderProps) => {
|
||||
return (
|
||||
<OrganisationContext.Provider value={organisation}>{children}</OrganisationContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,12 +1,18 @@
|
||||
export enum STRIPE_CUSTOMER_TYPE {
|
||||
INDIVIDUAL = 'individual',
|
||||
ORGANISATION = 'organisation',
|
||||
}
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
|
||||
export enum STRIPE_PLAN_TYPE {
|
||||
REGULAR = 'regular',
|
||||
TEAM = 'team',
|
||||
COMMUNITY = 'community',
|
||||
FREE = 'free',
|
||||
INDIVIDUAL = 'individual',
|
||||
PRO = 'pro',
|
||||
EARLY_ADOPTER = 'earlyAdopter',
|
||||
PLATFORM = 'platform',
|
||||
ENTERPRISE = 'enterprise',
|
||||
}
|
||||
|
||||
export const FREE_TIER_DOCUMENT_QUOTA = 5;
|
||||
|
||||
export const SUBSCRIPTION_STATUS_MAP = {
|
||||
[SubscriptionStatus.ACTIVE]: 'Active',
|
||||
[SubscriptionStatus.INACTIVE]: 'Inactive',
|
||||
[SubscriptionStatus.PAST_DUE]: 'Past Due',
|
||||
};
|
||||
|
||||
@ -2,7 +2,6 @@ import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from './app';
|
||||
|
||||
const NEXT_PUBLIC_FEATURE_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED');
|
||||
const NEXT_PUBLIC_POSTHOG_KEY = () => env('NEXT_PUBLIC_POSTHOG_KEY');
|
||||
|
||||
/**
|
||||
@ -10,26 +9,6 @@ const NEXT_PUBLIC_POSTHOG_KEY = () => env('NEXT_PUBLIC_POSTHOG_KEY');
|
||||
*/
|
||||
export const FEATURE_FLAG_GLOBAL_SESSION_RECORDING = 'global_session_recording';
|
||||
|
||||
/**
|
||||
* How frequent to poll for new feature flags in milliseconds.
|
||||
*/
|
||||
export const FEATURE_FLAG_POLL_INTERVAL = 30000;
|
||||
|
||||
/**
|
||||
* Feature flags that will be used when PostHog is disabled.
|
||||
*
|
||||
* Does not take any person or group properties into account.
|
||||
*/
|
||||
export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
||||
app_allow_encrypted_documents: false,
|
||||
app_billing: NEXT_PUBLIC_FEATURE_BILLING_ENABLED() === 'true',
|
||||
app_document_page_view_history_sheet: false,
|
||||
app_passkey: true,
|
||||
app_public_profile: true,
|
||||
marketing_header_single_player_mode: false,
|
||||
marketing_profiles_announcement_bar: true,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Extract the PostHog configuration from the environment.
|
||||
*/
|
||||
@ -46,10 +25,3 @@ export function extractPostHogConfig(): { key: string; host: string } | null {
|
||||
host: postHogHost,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether feature flags are enabled for the current instance.
|
||||
*/
|
||||
export function isFeatureFlagEnabled(): boolean {
|
||||
return extractPostHogConfig() !== null;
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { JobClient } from './client/client';
|
||||
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/send-confirmation-email';
|
||||
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
|
||||
import { SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-joined-email';
|
||||
import { SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-left-email';
|
||||
import { SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION } from './definitions/emails/send-password-reset-success-email';
|
||||
import { SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-recipient-signed-email';
|
||||
import { SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION } from './definitions/emails/send-rejection-emails';
|
||||
import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-signing-email';
|
||||
import { SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-deleted-email';
|
||||
import { SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-joined-email';
|
||||
import { SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-left-email';
|
||||
import { BULK_SEND_TEMPLATE_JOB_DEFINITION } from './definitions/internal/bulk-send-template';
|
||||
import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-document';
|
||||
|
||||
@ -18,8 +18,8 @@ import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-docume
|
||||
export const jobsClient = new JobClient([
|
||||
SEND_SIGNING_EMAIL_JOB_DEFINITION,
|
||||
SEND_CONFIRMATION_EMAIL_JOB_DEFINITION,
|
||||
SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION,
|
||||
SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION,
|
||||
SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION,
|
||||
SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION,
|
||||
SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION,
|
||||
SEAL_DOCUMENT_JOB_DEFINITION,
|
||||
SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION,
|
||||
|
||||
@ -1,80 +1,85 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||
import OrganisationJoinEmailTemplate from '@documenso/email/templates/organisation-join';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { getTeamSettings } from '../../../server-only/team/get-team-settings';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../../constants/organisations';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import { organisationGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendTeamMemberJoinedEmailJobDefinition } from './send-team-member-joined-email';
|
||||
import type { TSendOrganisationMemberJoinedEmailJobDefinition } from './send-organisation-member-joined-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendTeamMemberJoinedEmailJobDefinition;
|
||||
payload: TSendOrganisationMemberJoinedEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
const organisation = await prisma.organisation.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.teamId,
|
||||
id: payload.organisationId,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
role: {
|
||||
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
group: {
|
||||
organisationRole: {
|
||||
in: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
organisationGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId: payload.userId,
|
||||
teamId: payload.teamId,
|
||||
});
|
||||
|
||||
const invitedMember = await prisma.teamMember.findFirstOrThrow({
|
||||
const invitedMember = await prisma.organisationMember.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.memberId,
|
||||
teamId: payload.teamId,
|
||||
userId: payload.memberUserId,
|
||||
organisationId: payload.organisationId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const member of team.members) {
|
||||
for (const member of organisation.members) {
|
||||
if (member.id === invitedMember.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await io.runTask(
|
||||
`send-team-member-joined-email--${invitedMember.id}_${member.id}`,
|
||||
`send-organisation-member-joined-email--${invitedMember.id}_${member.id}`,
|
||||
async () => {
|
||||
const emailContent = createElement(TeamJoinEmailTemplate, {
|
||||
const emailContent = createElement(OrganisationJoinEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: invitedMember.user.name || '',
|
||||
memberEmail: invitedMember.user.email,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
organisationName: organisation.name,
|
||||
organisationUrl: organisation.url,
|
||||
});
|
||||
|
||||
const branding = teamGlobalSettingsToBranding(settings, team.id);
|
||||
const lang = settings.documentLanguage;
|
||||
const branding = organisationGlobalSettingsToBranding(
|
||||
organisation.organisationGlobalSettings,
|
||||
organisation.id,
|
||||
);
|
||||
|
||||
const lang = organisation.organisationGlobalSettings.documentLanguage;
|
||||
|
||||
// !: Replace with the actual language of the recipient later
|
||||
const [html, text] = await Promise.all([
|
||||
@ -97,7 +102,7 @@ export const run = async ({
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`A new member has joined your team`),
|
||||
subject: i18n._(msg`A new member has joined your organisation`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID =
|
||||
'send.organisation-member-joined.email';
|
||||
|
||||
const SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
organisationId: z.string(),
|
||||
memberUserId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendOrganisationMemberJoinedEmailJobDefinition = z.infer<
|
||||
typeof SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Organisation Member Joined Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-organisation-member-joined-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendOrganisationMemberJoinedEmailJobDefinition
|
||||
>;
|
||||
@ -0,0 +1,107 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import OrganisationLeaveEmailTemplate from '@documenso/email/templates/organisation-leave';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../../constants/organisations';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { organisationGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendOrganisationMemberLeftEmailJobDefinition } from './send-organisation-member-left-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendOrganisationMemberLeftEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const organisation = await prisma.organisation.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.organisationId,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
group: {
|
||||
organisationRole: {
|
||||
in: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
organisationGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
const oldMember = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.memberUserId,
|
||||
},
|
||||
});
|
||||
|
||||
for (const member of organisation.members) {
|
||||
if (member.userId === oldMember.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await io.runTask(
|
||||
`send-organisation-member-left-email--${oldMember.id}_${member.id}`,
|
||||
async () => {
|
||||
const emailContent = createElement(OrganisationLeaveEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: oldMember.name || '',
|
||||
memberEmail: oldMember.email,
|
||||
organisationName: organisation.name,
|
||||
organisationUrl: organisation.url,
|
||||
});
|
||||
|
||||
const branding = organisationGlobalSettingsToBranding(
|
||||
organisation.organisationGlobalSettings,
|
||||
organisation.id,
|
||||
);
|
||||
|
||||
const lang = organisation.organisationGlobalSettings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: member.user.email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`A member has left your organisation`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID = 'send.organisation-member-left.email';
|
||||
|
||||
const SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
organisationId: z.string(),
|
||||
memberUserId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendOrganisationMemberLeftEmailJobDefinition = z.infer<
|
||||
typeof SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Organisation Member Left Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-organisation-member-left-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendOrganisationMemberLeftEmailJobDefinition
|
||||
>;
|
||||
@ -1,4 +1,3 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
@ -9,23 +8,24 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
team: z.object({
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
teamGlobalSettings: z
|
||||
.object({
|
||||
documentVisibility: z.nativeEnum(DocumentVisibility),
|
||||
documentLanguage: z.string(),
|
||||
includeSenderDetails: z.boolean(),
|
||||
includeSigningCertificate: z.boolean(),
|
||||
brandingEnabled: z.boolean(),
|
||||
brandingLogo: z.string(),
|
||||
brandingUrl: z.string(),
|
||||
brandingCompanyDetails: z.string(),
|
||||
brandingHidePoweredBy: z.boolean(),
|
||||
teamId: z.number(),
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
uploadSignatureEnabled: z.boolean(),
|
||||
drawSignatureEnabled: z.boolean(),
|
||||
})
|
||||
.nullish(),
|
||||
// This is never passed along for some reason so commenting it out.
|
||||
// teamGlobalSettings: z
|
||||
// .object({
|
||||
// documentVisibility: z.nativeEnum(DocumentVisibility),
|
||||
// documentLanguage: z.string(),
|
||||
// includeSenderDetails: z.boolean(),
|
||||
// includeSigningCertificate: z.boolean(),
|
||||
// brandingEnabled: z.boolean(),
|
||||
// brandingLogo: z.string(),
|
||||
// brandingUrl: z.string(),
|
||||
// brandingCompanyDetails: z.string(),
|
||||
// brandingHidePoweredBy: z.boolean(),
|
||||
// teamId: z.number(),
|
||||
// typedSignatureEnabled: z.boolean(),
|
||||
// uploadSignatureEnabled: z.boolean(),
|
||||
// drawSignatureEnabled: z.boolean(),
|
||||
// })
|
||||
// .nullish(),
|
||||
}),
|
||||
members: z.array(
|
||||
z.object({
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID = 'send.team-member-joined.email';
|
||||
|
||||
const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
teamId: z.number(),
|
||||
memberId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendTeamMemberJoinedEmailJobDefinition = z.infer<
|
||||
typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Team Member Joined Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-team-member-joined-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendTeamMemberJoinedEmailJobDefinition
|
||||
>;
|
||||
@ -1,94 +0,0 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import TeamJoinEmailTemplate from '@documenso/email/templates/team-join';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
|
||||
import { getTeamSettings } from '../../../server-only/team/get-team-settings';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendTeamMemberLeftEmailJobDefinition } from './send-team-member-left-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendTeamMemberLeftEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.teamId,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
role: {
|
||||
in: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
teamId: payload.teamId,
|
||||
});
|
||||
|
||||
const oldMember = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: payload.memberUserId,
|
||||
},
|
||||
});
|
||||
|
||||
for (const member of team.members) {
|
||||
await io.runTask(`send-team-member-left-email--${oldMember.id}_${member.id}`, async () => {
|
||||
const emailContent = createElement(TeamJoinEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: oldMember.name || '',
|
||||
memberEmail: oldMember.email,
|
||||
teamName: team.name,
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
const branding = teamGlobalSettingsToBranding(settings, team.id);
|
||||
const lang = settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: member.user.email,
|
||||
from: {
|
||||
name: FROM_NAME,
|
||||
address: FROM_ADDRESS,
|
||||
},
|
||||
subject: i18n._(msg`A team member has left ${team.name}`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,32 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID = 'send.team-member-left.email';
|
||||
|
||||
const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
teamId: z.number(),
|
||||
memberUserId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendTeamMemberLeftEmailJobDefinition = z.infer<
|
||||
typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Team Member Left Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-team-member-left-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendTeamMemberLeftEmailJobDefinition
|
||||
>;
|
||||
@ -1,13 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export const findSubscriptions = async () => {
|
||||
return prisma.subscription.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
periodEnd: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -29,37 +29,26 @@ export async function getSigningVolume({
|
||||
|
||||
let findQuery = kyselyPrisma.$kysely
|
||||
.selectFrom('Subscription as s')
|
||||
.leftJoin('User as u', 's.userId', 'u.id')
|
||||
.leftJoin('Team as t', 's.teamId', 't.id')
|
||||
.leftJoin('Document as ud', (join) =>
|
||||
.innerJoin('Organisation as o', 's.organisationId', 'o.id')
|
||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
||||
.leftJoin('Document as d', (join) =>
|
||||
join
|
||||
.onRef('u.id', '=', 'ud.userId')
|
||||
.on('ud.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('ud.deletedAt', 'is', null)
|
||||
.on('ud.teamId', 'is', null),
|
||||
)
|
||||
.leftJoin('Document as td', (join) =>
|
||||
join
|
||||
.onRef('t.id', '=', 'td.teamId')
|
||||
.on('td.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('td.deletedAt', 'is', null),
|
||||
.onRef('t.id', '=', 'd.teamId')
|
||||
.on('d.status', '=', sql.lit(DocumentStatus.COMPLETED))
|
||||
.on('d.deletedAt', 'is', null),
|
||||
)
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('u.name', 'ilike', `%${search}%`),
|
||||
eb('u.email', 'ilike', `%${search}%`),
|
||||
eb('t.name', 'ilike', `%${search}%`),
|
||||
]),
|
||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
||||
)
|
||||
.select([
|
||||
's.id as id',
|
||||
's.createdAt as createdAt',
|
||||
's.planId as planId',
|
||||
sql<string>`COALESCE(u.name, t.name, u.email, 'Unknown')`.as('name'),
|
||||
sql<number>`COUNT(DISTINCT ud.id) + COUNT(DISTINCT td.id)`.as('signingVolume'),
|
||||
sql<string>`COALESCE(o.name, 'Unknown')`.as('name'),
|
||||
sql<number>`COUNT(DISTINCT d.id)`.as('signingVolume'),
|
||||
])
|
||||
.groupBy(['s.id', 'u.name', 't.name', 'u.email']);
|
||||
.groupBy(['s.id', 'o.name']);
|
||||
|
||||
switch (sortBy) {
|
||||
case 'name':
|
||||
@ -79,15 +68,11 @@ export async function getSigningVolume({
|
||||
|
||||
const countQuery = kyselyPrisma.$kysely
|
||||
.selectFrom('Subscription as s')
|
||||
.leftJoin('User as u', 's.userId', 'u.id')
|
||||
.leftJoin('Team as t', 's.teamId', 't.id')
|
||||
.innerJoin('Organisation as o', 's.organisationId', 'o.id')
|
||||
.leftJoin('Team as t', 'o.id', 't.organisationId')
|
||||
.where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('u.name', 'ilike', `%${search}%`),
|
||||
eb('u.email', 'ilike', `%${search}%`),
|
||||
eb('t.name', 'ilike', `%${search}%`),
|
||||
]),
|
||||
eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]),
|
||||
)
|
||||
.select(({ fn }) => [fn.countAll().as('count')]);
|
||||
|
||||
|
||||
@ -7,13 +7,11 @@ export const getUsersCount = async () => {
|
||||
return await prisma.user.count();
|
||||
};
|
||||
|
||||
export const getUsersWithSubscriptionsCount = async () => {
|
||||
return await prisma.user.count({
|
||||
export const getOrganisationsWithSubscriptionsCount = async () => {
|
||||
return await prisma.organisation.count({
|
||||
where: {
|
||||
subscriptions: {
|
||||
some: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
},
|
||||
subscription: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -27,6 +26,7 @@ import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { determineDocumentVisibility } from '../../utils/document-visibility';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getMemberRoles } from '../team/get-member-roles';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
@ -60,6 +60,23 @@ export const createDocumentV2 = async ({
|
||||
}: CreateDocumentOptions) => {
|
||||
const { title, formValues } = data;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery(userId, teamId),
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found',
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({
|
||||
userId,
|
||||
teamId,
|
||||
@ -96,17 +113,13 @@ export const createDocumentV2 = async ({
|
||||
const recipientsHaveActionAuth = data.recipients?.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (authOptions.globalActionAuth || recipientsHaveActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (
|
||||
(authOptions.globalActionAuth || recipientsHaveActionAuth) &&
|
||||
!team.organisation.organisationClaim.flags.cfr21
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { teamRole } = await getMemberRoles({
|
||||
|
||||
@ -2,7 +2,6 @@ import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
@ -43,6 +42,17 @@ export const updateDocument = async ({
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
@ -108,17 +118,10 @@ export const updateDocument = async ({
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (newGlobalActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -1,13 +1,14 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type { Duration } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
// temporary choice for testing only
|
||||
import * as timeConstants from '../../constants/time';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { alphaid } from '../../universal/id';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { hashString } from '../auth/hash';
|
||||
|
||||
type TimeConstants = typeof timeConstants & {
|
||||
@ -33,20 +34,14 @@ export const createApiToken = async ({
|
||||
|
||||
const timeConstantsRecords: TimeConstants = timeConstants;
|
||||
|
||||
if (teamId) {
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
});
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to create a token for this team',
|
||||
});
|
||||
}
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to create a token for this team',
|
||||
});
|
||||
}
|
||||
|
||||
const storedToken = await prisma.apiToken.create({
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type DeleteTokenByIdOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
@ -9,24 +11,20 @@ export type DeleteTokenByIdOptions = {
|
||||
};
|
||||
|
||||
export const deleteTokenById = async ({ id, userId, teamId }: DeleteTokenByIdOptions) => {
|
||||
if (teamId) {
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
});
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new Error('You do not have permission to delete this token');
|
||||
}
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to delete this token',
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.apiToken.delete({
|
||||
where: {
|
||||
id,
|
||||
teamId: teamId ?? null,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
@ -46,6 +45,15 @@ export const createDocumentRecipients = async ({
|
||||
where: documentWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -64,17 +72,10 @@ export const createDocumentRecipients = async ({
|
||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth) {
|
||||
const isEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
@ -38,6 +37,15 @@ export const createTemplateRecipients = async ({
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -50,17 +58,10 @@ export const createTemplateRecipients = async ({
|
||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth) {
|
||||
const isEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
|
||||
|
||||
@ -5,7 +5,6 @@ import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
@ -60,6 +59,15 @@ export const setDocumentRecipients = async ({
|
||||
include: {
|
||||
fields: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -90,17 +98,10 @@ export const setDocumentRecipients = async ({
|
||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedRecipients = recipients.map((recipient) => ({
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import {
|
||||
DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
DIRECT_TEMPLATE_RECIPIENT_NAME,
|
||||
@ -44,6 +43,15 @@ export const setTemplateRecipients = async ({
|
||||
},
|
||||
include: {
|
||||
directLink: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -54,17 +62,10 @@ export const setTemplateRecipients = async ({
|
||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedRecipients = recipients.map((recipient) => {
|
||||
|
||||
@ -2,7 +2,6 @@ import type { Recipient } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
@ -47,6 +46,15 @@ export const updateDocumentRecipients = async ({
|
||||
include: {
|
||||
fields: true,
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -65,17 +73,10 @@ export const updateDocumentRecipients = async ({
|
||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth) {
|
||||
const isEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const recipientsToUpdate = recipients.map((recipient) => {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { RecipientRole, Team } from '@prisma/client';
|
||||
import type { RecipientRole } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
@ -12,6 +11,7 @@ import {
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData, diffRecipientChanges } from '../../utils/document-audit-logs';
|
||||
import { createRecipientAuthOptions } from '../../utils/document-auth';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
export type UpdateRecipientOptions = {
|
||||
documentId: number;
|
||||
@ -44,57 +44,43 @@ export const updateRecipient = async ({
|
||||
document: {
|
||||
id: documentId,
|
||||
userId,
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
team: buildTeamWhereQuery(teamId, userId), // Todo: orgs i know i messed the orders of some of these up somewhere
|
||||
},
|
||||
},
|
||||
include: {
|
||||
document: {
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
document: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
// Todo: orgs check if this is supposed to only be documents
|
||||
if (!recipient || !recipient.document) {
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
if (actionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
const team = recipient.document.team;
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
if (actionAuth && !recipient.document.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
type TRecipientActionAuthTypes,
|
||||
@ -41,6 +40,15 @@ export const updateTemplateRecipients = async ({
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -53,17 +61,10 @@ export const updateTemplateRecipients = async ({
|
||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth) {
|
||||
const isEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const recipientsToUpdate = recipients.map((recipient) => {
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type GetSubscriptionsByUserIdOptions = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const getSubscriptionsByUserId = async ({ userId }: GetSubscriptionsByUserIdOptions) => {
|
||||
return await prisma.subscription.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,54 +0,0 @@
|
||||
import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session';
|
||||
import { getTeamPrices } from '@documenso/ee/server-only/stripe/get-team-prices';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type CreateTeamPendingCheckoutSession = {
|
||||
userId: number;
|
||||
pendingTeamId: number;
|
||||
interval: 'monthly' | 'yearly';
|
||||
};
|
||||
|
||||
export const createTeamPendingCheckoutSession = async ({
|
||||
userId,
|
||||
pendingTeamId,
|
||||
interval,
|
||||
}: CreateTeamPendingCheckoutSession) => {
|
||||
const teamPendingCreation = await prisma.teamPending.findFirstOrThrow({
|
||||
where: {
|
||||
id: pendingTeamId,
|
||||
ownerUserId: userId,
|
||||
},
|
||||
include: {
|
||||
owner: true,
|
||||
},
|
||||
});
|
||||
|
||||
const prices = await getTeamPrices();
|
||||
const priceId = prices[interval].priceId;
|
||||
|
||||
try {
|
||||
const stripeCheckoutSession = await getCheckoutSession({
|
||||
customerId: teamPendingCreation.customerId,
|
||||
priceId,
|
||||
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/teams`,
|
||||
subscriptionMetadata: {
|
||||
pendingTeamId: pendingTeamId.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!stripeCheckoutSession) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR);
|
||||
}
|
||||
|
||||
return stripeCheckoutSession;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
// Absorb all the errors incase Stripe throws something sensitive.
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Something went wrong.',
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,19 +1,7 @@
|
||||
import {
|
||||
OrganisationGroupType,
|
||||
OrganisationMemberRole,
|
||||
Prisma,
|
||||
TeamMemberRole,
|
||||
} from '@prisma/client';
|
||||
import type Stripe from 'stripe';
|
||||
import { OrganisationGroupType, OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createOrganisationCustomer } from '@documenso/ee/server-only/stripe/create-team-customer';
|
||||
import { getTeamRelatedPrices } from '@documenso/ee/server-only/stripe/get-team-related-prices';
|
||||
import { mapStripeSubscriptionToPrismaUpsertAction } from '@documenso/ee/server-only/stripe/webhook/on-subscription-updated';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import {
|
||||
@ -23,7 +11,6 @@ import {
|
||||
import { TEAM_INTERNAL_GROUPS } from '../../constants/teams';
|
||||
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||
import { generateDefaultTeamSettings } from '../../utils/teams';
|
||||
import { stripe } from '../stripe';
|
||||
|
||||
export type CreateTeamOptions = {
|
||||
/**
|
||||
@ -41,7 +28,7 @@ export type CreateTeamOptions = {
|
||||
*
|
||||
* Used as the URL path, example: https://documenso.com/t/{teamUrl}/settings
|
||||
*/
|
||||
teamUrl: string;
|
||||
teamUrl: string; // Todo: orgs make unique
|
||||
|
||||
/**
|
||||
* ID of the organisation the team belongs to.
|
||||
@ -62,28 +49,13 @@ export type CreateTeamOptions = {
|
||||
}[];
|
||||
};
|
||||
|
||||
export const ZCreateTeamResponseSchema = z.union([
|
||||
z.object({
|
||||
paymentRequired: z.literal(false),
|
||||
}),
|
||||
z.object({
|
||||
paymentRequired: z.literal(true),
|
||||
pendingTeamId: z.number(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type TCreateTeamResponse = z.infer<typeof ZCreateTeamResponseSchema>;
|
||||
|
||||
/**
|
||||
* Create a team or pending team depending on the user's subscription or application's billing settings.
|
||||
*/
|
||||
export const createTeam = async ({
|
||||
userId,
|
||||
teamName,
|
||||
teamUrl,
|
||||
organisationId,
|
||||
inheritMembers,
|
||||
}: CreateTeamOptions): Promise<TCreateTeamResponse> => {
|
||||
}: CreateTeamOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery(
|
||||
organisationId,
|
||||
@ -91,8 +63,9 @@ export const createTeam = async ({
|
||||
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
),
|
||||
include: {
|
||||
groups: true, // Todo: (orgs)
|
||||
subscriptions: true,
|
||||
groups: true,
|
||||
subscription: true,
|
||||
organisationClaim: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -109,6 +82,21 @@ export const createTeam = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// Validate they have enough team slots. 0 means they can create unlimited teams.
|
||||
if (organisation.organisationClaim.teamCount !== 0) {
|
||||
const teamCount = await prisma.team.count({
|
||||
where: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (teamCount >= organisation.organisationClaim.teamCount) {
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
message: 'You have reached the maximum number of teams for your plan.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Inherit internal organisation groups to the team.
|
||||
// Organisation Admins/Mangers get assigned as team admins, members get assigned as team members.
|
||||
const internalOrganisationGroups = organisation.groups
|
||||
@ -141,254 +129,46 @@ export const createTeam = async ({
|
||||
.exhaustive(),
|
||||
);
|
||||
|
||||
console.log({
|
||||
internalOrganisationGroups,
|
||||
});
|
||||
|
||||
if (Date.now() > 0) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const teamSettings = await tx.teamGlobalSettings.create({
|
||||
data: generateDefaultTeamSettings(),
|
||||
});
|
||||
|
||||
const team = await tx.team.create({
|
||||
data: {
|
||||
name: teamName,
|
||||
url: teamUrl,
|
||||
organisationId,
|
||||
teamGlobalSettingsId: teamSettings.id,
|
||||
teamGroups: {
|
||||
createMany: {
|
||||
// Attach the internal organisation groups to the team.
|
||||
data: internalOrganisationGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
teamGroups: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Create the internal team groups.
|
||||
await Promise.all(
|
||||
TEAM_INTERNAL_GROUPS.map(async (teamGroup) =>
|
||||
tx.organisationGroup.create({
|
||||
data: {
|
||||
type: teamGroup.type,
|
||||
organisationRole: LOWEST_ORGANISATION_ROLE,
|
||||
organisationId,
|
||||
teamGroups: {
|
||||
create: {
|
||||
teamId: team.id,
|
||||
teamRole: teamGroup.teamRole,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
paymentRequired: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (Date.now() > 0) {
|
||||
throw new Error('Todo: Orgs');
|
||||
}
|
||||
|
||||
let isPaymentRequired = IS_BILLING_ENABLED();
|
||||
let customerId: string | null = null;
|
||||
|
||||
if (IS_BILLING_ENABLED()) {
|
||||
const teamRelatedPriceIds = await getTeamRelatedPrices().then((prices) =>
|
||||
prices.map((price) => price.id),
|
||||
);
|
||||
|
||||
isPaymentRequired = !subscriptionsContainsActivePlan(
|
||||
organisation.subscriptions,
|
||||
teamRelatedPriceIds, // Todo: (orgs)
|
||||
);
|
||||
|
||||
customerId = await createOrganisationCustomer({
|
||||
name: organisation.owner.name ?? teamName,
|
||||
email: organisation.owner.email,
|
||||
}).then((customer) => customer.id);
|
||||
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
customerId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the team directly if no payment is required.
|
||||
if (!isPaymentRequired) {
|
||||
await prisma.team.create({
|
||||
data: {
|
||||
name: teamName,
|
||||
url: teamUrl,
|
||||
organisationId,
|
||||
members: {
|
||||
create: [
|
||||
{
|
||||
userId,
|
||||
role: TeamMemberRole.ADMIN, // Todo: (orgs)
|
||||
},
|
||||
],
|
||||
},
|
||||
teamGlobalSettings: {
|
||||
create: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
paymentRequired: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a pending team if payment is required.
|
||||
const pendingTeam = await prisma.$transaction(async (tx) => {
|
||||
const existingTeamWithUrl = await tx.team.findUnique({
|
||||
where: {
|
||||
url: teamUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const existingUserProfileWithUrl = await tx.user.findUnique({
|
||||
where: {
|
||||
url: teamUrl,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUserProfileWithUrl) {
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
|
||||
message: 'URL already taken.',
|
||||
});
|
||||
}
|
||||
|
||||
if (existingTeamWithUrl) {
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
|
||||
message: 'Team URL already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!customerId) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Missing customer ID for pending teams.',
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.teamPending.create({
|
||||
data: {
|
||||
name: teamName,
|
||||
url: teamUrl,
|
||||
ownerUserId: user.id,
|
||||
customerId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
paymentRequired: true,
|
||||
pendingTeamId: pendingTeam.id,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
if (!(err instanceof Prisma.PrismaClientKnownRequestError)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const target = z.array(z.string()).safeParse(err.meta?.target);
|
||||
|
||||
if (err.code === 'P2002' && target.success && target.data.includes('url')) {
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
|
||||
message: 'Team URL already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export type CreateTeamFromPendingTeamOptions = {
|
||||
pendingTeamId: number;
|
||||
subscription: Stripe.Subscription;
|
||||
};
|
||||
|
||||
export const createTeamFromPendingTeam = async ({
|
||||
pendingTeamId,
|
||||
subscription,
|
||||
}: CreateTeamFromPendingTeamOptions) => {
|
||||
const createdTeam = await prisma.$transaction(async (tx) => {
|
||||
const pendingTeam = await tx.teamPending.findUniqueOrThrow({
|
||||
where: {
|
||||
id: pendingTeamId,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.teamPending.delete({
|
||||
where: {
|
||||
id: pendingTeamId,
|
||||
},
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const teamSettings = await tx.teamGlobalSettings.create({
|
||||
data: generateDefaultTeamSettings(),
|
||||
});
|
||||
|
||||
const team = await tx.team.create({
|
||||
data: {
|
||||
name: pendingTeam.name,
|
||||
url: pendingTeam.url,
|
||||
ownerUserId: pendingTeam.ownerUserId,
|
||||
customerId: pendingTeam.customerId,
|
||||
members: {
|
||||
create: [
|
||||
{
|
||||
userId: pendingTeam.ownerUserId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
],
|
||||
name: teamName,
|
||||
url: teamUrl,
|
||||
organisationId,
|
||||
teamGlobalSettingsId: teamSettings.id,
|
||||
teamGroups: {
|
||||
createMany: {
|
||||
// Attach the internal organisation groups to the team.
|
||||
data: internalOrganisationGroups,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.teamGlobalSettings.upsert({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
teamId: team.id,
|
||||
include: {
|
||||
teamGroups: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.subscription.upsert(
|
||||
mapStripeSubscriptionToPrismaUpsertAction(subscription, undefined, team.id),
|
||||
// Create the internal team groups.
|
||||
await Promise.all(
|
||||
TEAM_INTERNAL_GROUPS.map(async (teamGroup) =>
|
||||
tx.organisationGroup.create({
|
||||
data: {
|
||||
type: teamGroup.type,
|
||||
organisationRole: LOWEST_ORGANISATION_ROLE,
|
||||
organisationId,
|
||||
teamGroups: {
|
||||
create: {
|
||||
teamId: team.id,
|
||||
teamRole: teamGroup.teamRole,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return team;
|
||||
});
|
||||
|
||||
// Attach the team ID to the subscription metadata for sanity reasons.
|
||||
await stripe.subscriptions
|
||||
.update(subscription.id, {
|
||||
metadata: {
|
||||
teamId: createdTeam.id.toString(),
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
// Non-critical error, but we want to log it so we can rectify it.
|
||||
// Todo: Teams - Alert us.
|
||||
});
|
||||
|
||||
return createdTeam;
|
||||
};
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type DeleteTeamPendingOptions = {
|
||||
userId: number;
|
||||
pendingTeamId: number;
|
||||
};
|
||||
|
||||
export const deleteTeamPending = async ({ userId, pendingTeamId }: DeleteTeamPendingOptions) => {
|
||||
await prisma.teamPending.delete({
|
||||
where: {
|
||||
id: pendingTeamId,
|
||||
ownerUserId: userId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,8 +1,8 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { OrganisationGlobalSettings } from '@prisma/client';
|
||||
import { OrganisationGroupType, type Team } from '@prisma/client';
|
||||
import { uniqueBy } from 'remeda';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { TeamDeleteEmailTemplate } from '@documenso/email/templates/team-delete';
|
||||
@ -13,8 +13,8 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getTeamSettings } from './get-team-settings';
|
||||
|
||||
@ -28,6 +28,11 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['DELETE_TEAM']),
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationGlobalSettings: true,
|
||||
},
|
||||
},
|
||||
teamGroups: {
|
||||
include: {
|
||||
organisationGroup: {
|
||||
@ -65,21 +70,19 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||
teamId,
|
||||
});
|
||||
|
||||
const membersToNotify = uniqueBy(
|
||||
team.teamGroups.flatMap((group) =>
|
||||
group.organisationGroup.organisationGroupMembers.map((member) => ({
|
||||
id: member.organisationMember.user.id,
|
||||
name: member.organisationMember.user.name || '',
|
||||
email: member.organisationMember.user.email,
|
||||
})),
|
||||
),
|
||||
(member) => member.id,
|
||||
);
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
// Todo: orgs handle any subs?
|
||||
// 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,
|
||||
@ -96,25 +99,20 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||
},
|
||||
});
|
||||
|
||||
// const members = team.teamGroups.flatMap((group) =>
|
||||
// group.organisationGroup.organisationMembers.map((member) => ({
|
||||
// id: member.user.id,
|
||||
// name: member.user.name || '',
|
||||
// email: member.user.email,
|
||||
// })),
|
||||
// );
|
||||
|
||||
// await jobs.triggerJob({
|
||||
// name: 'send.team-deleted.email',
|
||||
// payload: {
|
||||
// team: {
|
||||
// name: team.name,
|
||||
// url: team.url,
|
||||
// teamGlobalSettings: team.teamGlobalSettings, // Todo: orgs
|
||||
// },
|
||||
// members,
|
||||
// },
|
||||
// });
|
||||
await jobs.triggerJob({
|
||||
name: 'send.team-deleted.email',
|
||||
payload: {
|
||||
team: {
|
||||
name: team.name,
|
||||
url: team.url,
|
||||
// teamGlobalSettings: {
|
||||
// ...settings,
|
||||
// teamId: team.id,
|
||||
// },
|
||||
},
|
||||
members: membersToNotify,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
@ -122,14 +120,14 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => {
|
||||
|
||||
type SendTeamDeleteEmailOptions = {
|
||||
email: string;
|
||||
team: Pick<Team, 'id' | 'url' | 'name'>;
|
||||
settings: Omit<OrganisationGlobalSettings, 'id'>;
|
||||
team: Pick<Team, 'url' | 'name'>;
|
||||
// settings: Omit<OrganisationGlobalSettings, 'id'>;
|
||||
};
|
||||
|
||||
export const sendTeamDeleteEmail = async ({
|
||||
email,
|
||||
team,
|
||||
settings,
|
||||
// settings,
|
||||
}: SendTeamDeleteEmailOptions) => {
|
||||
const template = createElement(TeamDeleteEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
@ -137,9 +135,12 @@ export const sendTeamDeleteEmail = async ({
|
||||
teamUrl: team.url,
|
||||
});
|
||||
|
||||
const branding = teamGlobalSettingsToBranding(settings, team.id);
|
||||
// This is never actually passed on so commenting it out.
|
||||
// const branding = teamGlobalSettingsToBranding(settings, team.id);
|
||||
// const lang = settings.documentLanguage;
|
||||
|
||||
const lang = settings.documentLanguage;
|
||||
const branding = undefined;
|
||||
const lang = undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
import type { Team } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamPendingSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamPendingSchema';
|
||||
|
||||
import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params';
|
||||
|
||||
export interface FindTeamsPendingOptions {
|
||||
userId: number;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
column: keyof Team;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export const ZFindTeamsPendingResponseSchema = ZFindResultResponse.extend({
|
||||
data: TeamPendingSchema.array(),
|
||||
});
|
||||
|
||||
export type TFindTeamsPendingResponse = z.infer<typeof ZFindTeamsPendingResponseSchema>;
|
||||
|
||||
export const findTeamsPending = async ({
|
||||
userId,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
}: FindTeamsPendingOptions): Promise<TFindTeamsPendingResponse> => {
|
||||
const orderByColumn = orderBy?.column ?? 'name';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const whereClause: Prisma.TeamPendingWhereInput = {
|
||||
ownerUserId: userId,
|
||||
};
|
||||
|
||||
if (query && query.length > 0) {
|
||||
whereClause.name = {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.teamPending.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
}),
|
||||
prisma.teamPending.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@ -19,7 +19,7 @@ export type UpdateTeamOptions = {
|
||||
export const updateTeam = async ({ userId, teamId, data }: UpdateTeamOptions): Promise<void> => {
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const foundPendingTeamWithUrl = await tx.teamPending.findFirst({
|
||||
const foundTeamWithUrl = await tx.team.findFirst({
|
||||
where: {
|
||||
url: data.url,
|
||||
},
|
||||
@ -31,21 +31,19 @@ export const updateTeam = async ({ userId, teamId, data }: UpdateTeamOptions): P
|
||||
},
|
||||
});
|
||||
|
||||
if (foundPendingTeamWithUrl || foundOrganisationWithUrl) {
|
||||
if (foundTeamWithUrl || foundOrganisationWithUrl) {
|
||||
throw new AppError(AppErrorCode.ALREADY_EXISTS, {
|
||||
message: 'Team URL already exists.',
|
||||
});
|
||||
}
|
||||
|
||||
const team = await tx.team.update({
|
||||
return await tx.team.update({
|
||||
where: buildTeamWhereQuery(teamId, userId, TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM']),
|
||||
data: {
|
||||
url: data.url,
|
||||
name: data.name,
|
||||
},
|
||||
});
|
||||
|
||||
return team;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { DocumentVisibility, Template, TemplateMeta } from '@prisma/client';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
@ -39,6 +38,15 @@ export const updateTemplate = async ({
|
||||
},
|
||||
include: {
|
||||
templateMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -66,17 +74,10 @@ export const updateTemplate = async ({
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
if (newGlobalActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const authOptions = createDocumentAuthOptions({
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,8 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import OrganisationClaimSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationClaimSchema';
|
||||
import { OrganisationSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
|
||||
|
||||
/**
|
||||
* The full document response schema.
|
||||
*
|
||||
* Mainly used for returning a single document from the API.
|
||||
*/
|
||||
export const ZOrganisationSchema = OrganisationSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
@ -17,54 +13,20 @@ export const ZOrganisationSchema = OrganisationSchema.pick({
|
||||
customerId: true,
|
||||
ownerUserId: true,
|
||||
}).extend({
|
||||
// // Todo: Maybe we want to alter this a bit since this returns a lot of data.
|
||||
// documentData: OrganisationDataSchema.pick({
|
||||
// type: true,
|
||||
// id: true,
|
||||
// data: true,
|
||||
// initialData: true,
|
||||
// }),
|
||||
// documentMeta: OrganisationMetaSchema.pick({
|
||||
// signingOrder: true,
|
||||
// distributionMethod: true,
|
||||
// id: true,
|
||||
// subject: true,
|
||||
// message: true,
|
||||
// timezone: true,
|
||||
// password: true,
|
||||
// dateFormat: true,
|
||||
// documentId: true,
|
||||
// redirectUrl: true,
|
||||
// typedSignatureEnabled: true,
|
||||
// uploadSignatureEnabled: true,
|
||||
// drawSignatureEnabled: true,
|
||||
// allowDictateNextSigner: true,
|
||||
// language: true,
|
||||
// emailSettings: true,
|
||||
// }).nullable(),
|
||||
// recipients: ZRecipientLiteSchema.array(),
|
||||
// fields: ZFieldSchema.array(),
|
||||
organisationClaim: OrganisationClaimSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
originalSubscriptionClaimId: true,
|
||||
teamCount: true,
|
||||
memberCount: true,
|
||||
flags: true,
|
||||
}),
|
||||
});
|
||||
|
||||
export type TOrganisation = z.infer<typeof ZOrganisationSchema>;
|
||||
|
||||
/**
|
||||
* A lite version of the document response schema without relations.
|
||||
*/
|
||||
export const ZOrganisationLiteSchema = OrganisationSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
avatarImageId: true,
|
||||
customerId: true,
|
||||
ownerUserId: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* A version of the document response schema when returning multiple documents at once from a single API endpoint.
|
||||
*/
|
||||
export const ZOrganisationManySchema = OrganisationSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
@ -73,15 +35,9 @@ export const ZOrganisationManySchema = OrganisationSchema.pick({
|
||||
avatarImageId: true,
|
||||
customerId: true,
|
||||
ownerUserId: true,
|
||||
}).extend({
|
||||
// user: UserSchema.pick({
|
||||
// id: true,
|
||||
// name: true,
|
||||
// email: true,
|
||||
// }),
|
||||
// recipients: ZRecipientLiteSchema.array(),
|
||||
// team: TeamSchema.pick({
|
||||
// id: true,
|
||||
// url: true,
|
||||
// }).nullable(),
|
||||
});
|
||||
|
||||
/**
|
||||
* A version of the organisation response schema when returning multiple organisations at once from a single API endpoint.
|
||||
*/
|
||||
export const ZOrganisationManySchema = ZOrganisationLiteSchema;
|
||||
|
||||
177
packages/lib/types/subscription.ts
Normal file
177
packages/lib/types/subscription.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { SubscriptionClaim } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZOrganisationNameSchema } from '@documenso/trpc/server/organisation-router/create-organisation.types';
|
||||
|
||||
export const ZClaimFlagsSchema = z.object({
|
||||
unlimitedDocuments: z.boolean().optional(),
|
||||
|
||||
/**
|
||||
* Allows disabling of Documenso branding for:
|
||||
* - Certificates
|
||||
* - Emails
|
||||
* - Todo: orgs
|
||||
*
|
||||
* Rename to allowCustomBranding
|
||||
*/
|
||||
branding: z.boolean().optional(),
|
||||
|
||||
embedAuthoring: z.boolean().optional(),
|
||||
embedAuthoringWhiteLabel: z.boolean().optional(),
|
||||
|
||||
embedSigning: z.boolean().optional(),
|
||||
embedSigningWhiteLabel: z.boolean().optional(),
|
||||
|
||||
cfr21: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TClaimFlags = z.infer<typeof ZClaimFlagsSchema>;
|
||||
|
||||
// When adding keys, update internal documentation with this.
|
||||
export const SUBSCRIPTION_CLAIM_FEATURE_FLAGS: Record<
|
||||
keyof TClaimFlags,
|
||||
{
|
||||
label: string;
|
||||
key: keyof TClaimFlags;
|
||||
}
|
||||
> = {
|
||||
unlimitedDocuments: {
|
||||
key: 'unlimitedDocuments',
|
||||
label: 'Unlimited documents',
|
||||
},
|
||||
branding: {
|
||||
key: 'branding',
|
||||
label: 'Branding',
|
||||
},
|
||||
embedAuthoring: {
|
||||
key: 'embedAuthoring',
|
||||
label: 'Embed authoring',
|
||||
},
|
||||
embedSigning: {
|
||||
key: 'embedSigning',
|
||||
label: 'Embed signing',
|
||||
},
|
||||
embedAuthoringWhiteLabel: {
|
||||
key: 'embedAuthoringWhiteLabel',
|
||||
label: 'White label for embed authoring',
|
||||
},
|
||||
embedSigningWhiteLabel: {
|
||||
key: 'embedSigningWhiteLabel',
|
||||
label: 'White label for embed signing',
|
||||
},
|
||||
cfr21: {
|
||||
key: 'cfr21',
|
||||
label: '21 CFR',
|
||||
},
|
||||
};
|
||||
|
||||
export enum INTERNAL_CLAIM_ID {
|
||||
FREE = 'free',
|
||||
INDIVIDUAL = 'individual',
|
||||
PRO = 'pro',
|
||||
EARLY_ADOPTER = 'earlyAdopter',
|
||||
PLATFORM = 'platform',
|
||||
ENTERPRISE = 'enterprise',
|
||||
}
|
||||
|
||||
export type InternalClaim = Omit<SubscriptionClaim, 'createdAt' | 'updatedAt'> & {
|
||||
description: MessageDescriptor | string;
|
||||
};
|
||||
|
||||
export type InternalClaims = {
|
||||
[key in INTERNAL_CLAIM_ID]: InternalClaim;
|
||||
};
|
||||
|
||||
export const internalClaims: InternalClaims = {
|
||||
[INTERNAL_CLAIM_ID.FREE]: {
|
||||
id: INTERNAL_CLAIM_ID.FREE,
|
||||
name: 'Free',
|
||||
description: msg`5 Documents a month`,
|
||||
teamCount: 1,
|
||||
memberCount: 1,
|
||||
locked: true,
|
||||
flags: {},
|
||||
},
|
||||
[INTERNAL_CLAIM_ID.INDIVIDUAL]: {
|
||||
id: INTERNAL_CLAIM_ID.INDIVIDUAL,
|
||||
name: 'Individual',
|
||||
description: msg`Unlimited documents, API and more`,
|
||||
teamCount: 1,
|
||||
memberCount: 1,
|
||||
locked: true,
|
||||
flags: {
|
||||
unlimitedDocuments: true,
|
||||
},
|
||||
},
|
||||
[INTERNAL_CLAIM_ID.PRO]: {
|
||||
id: INTERNAL_CLAIM_ID.PRO, // Team -> Pro
|
||||
name: 'Teams',
|
||||
description: msg`Embedding, 5 members included and more`,
|
||||
teamCount: 1,
|
||||
memberCount: 5,
|
||||
locked: true,
|
||||
flags: {
|
||||
unlimitedDocuments: true,
|
||||
branding: true,
|
||||
embedSigning: true, // Pro (team) plan only gets embedSigning right?
|
||||
},
|
||||
},
|
||||
[INTERNAL_CLAIM_ID.PLATFORM]: {
|
||||
id: INTERNAL_CLAIM_ID.PLATFORM,
|
||||
name: 'Platform',
|
||||
description: msg`Whitelabeling, unlimited members and more`,
|
||||
teamCount: 1,
|
||||
memberCount: 0,
|
||||
locked: true,
|
||||
flags: {
|
||||
unlimitedDocuments: true,
|
||||
branding: true,
|
||||
embedAuthoring: false,
|
||||
embedAuthoringWhiteLabel: true,
|
||||
embedSigning: false,
|
||||
embedSigningWhiteLabel: true,
|
||||
},
|
||||
},
|
||||
[INTERNAL_CLAIM_ID.ENTERPRISE]: {
|
||||
id: INTERNAL_CLAIM_ID.ENTERPRISE,
|
||||
name: 'Enterprise',
|
||||
description: '',
|
||||
teamCount: 0,
|
||||
memberCount: 0,
|
||||
locked: true,
|
||||
flags: {
|
||||
unlimitedDocuments: true,
|
||||
branding: true,
|
||||
embedAuthoring: true,
|
||||
embedAuthoringWhiteLabel: true,
|
||||
embedSigning: true,
|
||||
embedSigningWhiteLabel: true,
|
||||
cfr21: true,
|
||||
},
|
||||
},
|
||||
[INTERNAL_CLAIM_ID.EARLY_ADOPTER]: {
|
||||
id: INTERNAL_CLAIM_ID.EARLY_ADOPTER,
|
||||
name: 'Early Adopter',
|
||||
description: '',
|
||||
teamCount: 0,
|
||||
memberCount: 0,
|
||||
locked: true,
|
||||
flags: {
|
||||
unlimitedDocuments: true,
|
||||
branding: true,
|
||||
embedSigning: true,
|
||||
embedSigningWhiteLabel: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ZStripeOrganisationCreateMetadataSchema = z.object({
|
||||
organisationName: ZOrganisationNameSchema,
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export type StripeOrganisationCreateMetadata = z.infer<
|
||||
typeof ZStripeOrganisationCreateMetadataSchema
|
||||
>;
|
||||
@ -3,3 +3,7 @@ import { customAlphabet } from 'nanoid';
|
||||
export const alphaid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 21);
|
||||
|
||||
export { nanoid } from 'nanoid';
|
||||
|
||||
export const generatePrefixedId = (prefix: string, length = 8) => {
|
||||
return `${prefix}_${alphaid(length)}`;
|
||||
};
|
||||
|
||||
@ -1,36 +1,39 @@
|
||||
import type { Subscription } from '@prisma/client';
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
import type { Subscription } from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '../constants/app';
|
||||
import { AppErrorCode } from '../errors/app-error';
|
||||
import { AppError } from '../errors/app-error';
|
||||
import type { StripeOrganisationCreateMetadata } from '../types/subscription';
|
||||
|
||||
export const generateStripeOrganisationCreateMetadata = (
|
||||
organisationName: string,
|
||||
userId: number,
|
||||
) => {
|
||||
const metadata: StripeOrganisationCreateMetadata = {
|
||||
organisationName,
|
||||
userId,
|
||||
};
|
||||
|
||||
return {
|
||||
organisationCreateData: JSON.stringify(metadata),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if there is a subscription that is active and is one of the provided price IDs.
|
||||
* Throws an error if billing is enabled and no subscription is found.
|
||||
*/
|
||||
export const subscriptionsContainsActivePlan = (
|
||||
subscriptions: Subscription[],
|
||||
priceIds: string[],
|
||||
allowPastDue?: boolean,
|
||||
) => {
|
||||
const allowedSubscriptionStatuses: SubscriptionStatus[] = [SubscriptionStatus.ACTIVE];
|
||||
export const validateIfSubscriptionIsRequired = (subscription?: Subscription | null) => {
|
||||
const isBillingEnabled = IS_BILLING_ENABLED();
|
||||
|
||||
if (allowPastDue) {
|
||||
allowedSubscriptionStatuses.push(SubscriptionStatus.PAST_DUE);
|
||||
if (!isBillingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return subscriptions.some(
|
||||
(subscription) =>
|
||||
allowedSubscriptionStatuses.includes(subscription.status) &&
|
||||
priceIds.includes(subscription.priceId),
|
||||
);
|
||||
};
|
||||
if (isBillingEnabled && !subscription) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Subscription not found',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is a subscription that is active and is one of the provided product IDs.
|
||||
*/
|
||||
export const subscriptionsContainsActiveProductId = (
|
||||
subscriptions: Subscription[],
|
||||
productId: string[],
|
||||
) => {
|
||||
return subscriptions.some(
|
||||
(subscription) =>
|
||||
subscription.status === SubscriptionStatus.ACTIVE && productId.includes(subscription.planId),
|
||||
);
|
||||
return subscription;
|
||||
};
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { I18n, MessageDescriptor } from '@lingui/core';
|
||||
import { i18n } from '@lingui/core';
|
||||
import type { MacroMessageDescriptor } from '@lingui/core/macro';
|
||||
|
||||
import type { I18nLocaleData, SupportedLanguageCodes } from '../constants/i18n';
|
||||
import { APP_I18N_OPTIONS } from '../constants/i18n';
|
||||
@ -84,3 +85,10 @@ export const extractLocaleData = ({ headers }: ExtractLocaleDataOptions): I18nLo
|
||||
export const parseMessageDescriptor = (_: I18n['_'], value: string | MessageDescriptor) => {
|
||||
return typeof value === 'string' ? value : _(value);
|
||||
};
|
||||
|
||||
export const parseMessageDescriptorMacro = (
|
||||
t: (descriptor: MacroMessageDescriptor) => string,
|
||||
value: string | MessageDescriptor,
|
||||
) => {
|
||||
return typeof value === 'string' ? value : t(value);
|
||||
};
|
||||
|
||||
25
packages/lib/utils/organisations-claims.ts
Normal file
25
packages/lib/utils/organisations-claims.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { OrganisationClaim, SubscriptionClaim } from '@prisma/client';
|
||||
|
||||
export const generateDefaultOrganisationClaims = (): Omit<
|
||||
OrganisationClaim,
|
||||
'id' | 'organisation' | 'createdAt' | 'updatedAt' | 'originalSubscriptionClaimId'
|
||||
> => {
|
||||
return {
|
||||
teamCount: 1,
|
||||
memberCount: 1,
|
||||
flags: {},
|
||||
};
|
||||
};
|
||||
|
||||
export const generateDefaultSubscriptionClaim = (): Omit<
|
||||
SubscriptionClaim,
|
||||
'id' | 'organisation' | 'createdAt' | 'updatedAt' | 'originalSubscriptionClaimId'
|
||||
> => {
|
||||
return {
|
||||
name: '',
|
||||
teamCount: 1,
|
||||
memberCount: 1,
|
||||
locked: false,
|
||||
flags: {},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user