feat: billing

This commit is contained in:
David Nguyen
2025-05-19 12:38:50 +10:00
parent 7abfc9e271
commit 2805478e0d
221 changed files with 8436 additions and 5847 deletions

View File

@ -0,0 +1,50 @@
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZCreateStripeCustomerRequestSchema,
ZCreateStripeCustomerResponseSchema,
} from './create-stripe-customer.types';
export const createStripeCustomerRoute = adminProcedure
.input(ZCreateStripeCustomerRequestSchema)
.output(ZCreateStripeCustomerResponseSchema)
.mutation(async ({ input }) => {
const { organisationId } = input;
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,
},
include: {
owner: {
select: {
email: true,
name: true,
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
await prisma.$transaction(async (tx) => {
const stripeCustomer = await createCustomer({
name: organisation.name,
email: organisation.owner.email,
});
await tx.organisation.update({
where: {
id: organisationId,
},
data: {
customerId: stripeCustomer.id,
},
});
});
});

View File

@ -0,0 +1,9 @@
import { z } from 'zod';
export const ZCreateStripeCustomerRequestSchema = z.object({
organisationId: z.string().describe('The organisation to attach the customer to'),
});
export const ZCreateStripeCustomerResponseSchema = z.void();
export type TCreateStripeCustomerRequest = z.infer<typeof ZCreateStripeCustomerRequestSchema>;

View File

@ -0,0 +1,23 @@
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZCreateSubscriptionClaimRequestSchema,
ZCreateSubscriptionClaimResponseSchema,
} from './create-subscription-claim.types';
export const createSubscriptionClaimRoute = adminProcedure
.input(ZCreateSubscriptionClaimRequestSchema)
.output(ZCreateSubscriptionClaimResponseSchema)
.mutation(async ({ input }) => {
const { name, teamCount, memberCount, flags } = input;
await prisma.subscriptionClaim.create({
data: {
name,
teamCount,
memberCount,
flags,
},
});
});

View File

@ -0,0 +1,14 @@
import { z } from 'zod';
import { ZClaimFlagsSchema } from '@documenso/lib/types/subscription';
export const ZCreateSubscriptionClaimRequestSchema = z.object({
name: z.string().min(1),
teamCount: z.number().int().min(0),
memberCount: z.number().int().min(0),
flags: ZClaimFlagsSchema,
});
export const ZCreateSubscriptionClaimResponseSchema = z.void();
export type TCreateSubscriptionClaimRequest = z.infer<typeof ZCreateSubscriptionClaimRequestSchema>;

View File

@ -0,0 +1,37 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZDeleteSubscriptionClaimRequestSchema,
ZDeleteSubscriptionClaimResponseSchema,
} from './delete-subscription-claim.types';
export const deleteSubscriptionClaimRoute = adminProcedure
.input(ZDeleteSubscriptionClaimRequestSchema)
.output(ZDeleteSubscriptionClaimResponseSchema)
.mutation(async ({ input }) => {
const { id } = input;
const existingClaim = await prisma.subscriptionClaim.findFirst({
where: {
id,
},
});
if (!existingClaim) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Subscription claim not found' });
}
if (existingClaim.locked) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Cannot delete locked subscription claim',
});
}
await prisma.subscriptionClaim.delete({
where: {
id,
},
});
});

View File

@ -0,0 +1,9 @@
import { z } from 'zod';
export const ZDeleteSubscriptionClaimRequestSchema = z.object({
id: z.string().cuid(),
});
export const ZDeleteSubscriptionClaimResponseSchema = z.void();
export type TDeleteSubscriptionClaimRequest = z.infer<typeof ZDeleteSubscriptionClaimRequestSchema>;

View File

@ -0,0 +1,128 @@
import { Prisma } from '@prisma/client';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZFindAdminOrganisationsRequestSchema,
ZFindAdminOrganisationsResponseSchema,
} from './find-admin-organisations.types';
export const findAdminOrganisationsRoute = adminProcedure
.input(ZFindAdminOrganisationsRequestSchema)
.output(ZFindAdminOrganisationsResponseSchema)
.query(async ({ input }) => {
const { query, page, perPage } = input;
return await findAdminOrganisations({
query,
page,
perPage,
});
});
type FindAdminOrganisationsOptions = {
query?: string;
page?: number;
perPage?: number;
};
export const findAdminOrganisations = async ({
query,
page = 1,
perPage = 10,
}: FindAdminOrganisationsOptions) => {
let whereClause: Prisma.OrganisationWhereInput = {};
if (query) {
whereClause = {
OR: [
{
id: {
contains: query,
mode: Prisma.QueryMode.insensitive,
},
},
{
owner: {
email: {
contains: query,
mode: Prisma.QueryMode.insensitive,
},
},
},
{
customerId: {
contains: query,
mode: Prisma.QueryMode.insensitive,
},
},
{
name: {
contains: query,
mode: Prisma.QueryMode.insensitive,
},
},
],
};
}
if (query && query.startsWith('claim:')) {
whereClause = {
organisationClaim: {
originalSubscriptionClaimId: {
contains: query.slice(6),
mode: Prisma.QueryMode.insensitive,
},
},
};
}
if (query && query.startsWith('org_')) {
whereClause = {
url: {
equals: query,
mode: Prisma.QueryMode.insensitive,
},
};
}
const [data, count] = await Promise.all([
prisma.organisation.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
createdAt: true,
updatedAt: true,
name: true,
url: true,
customerId: true,
owner: {
select: {
id: true,
email: true,
name: true,
},
},
subscription: true,
},
}),
prisma.organisation.count({
where: whereClause,
}),
]);
return {
data,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<typeof data>;
};

View File

@ -0,0 +1,39 @@
import type { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
import SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
export const ZFindAdminOrganisationsRequestSchema = ZFindSearchParamsSchema;
export const ZFindAdminOrganisationsResponseSchema = ZFindResultResponse.extend({
data: OrganisationSchema.pick({
id: true,
createdAt: true,
updatedAt: true,
name: true,
url: true,
customerId: true,
})
.extend({
owner: UserSchema.pick({
id: true,
email: true,
name: true,
}),
subscription: SubscriptionSchema.pick({
status: true,
id: true,
planId: true,
priceId: true,
periodEnd: true,
createdAt: true,
updatedAt: true,
cancelAtPeriodEnd: true,
}).nullable(),
})
.array(),
});
export type TFindAdminOrganisationsResponse = z.infer<typeof ZFindAdminOrganisationsResponseSchema>;

View File

@ -0,0 +1,76 @@
import type { Prisma } from '@prisma/client';
import type { z } from 'zod';
import type { FindResultResponse } from '@documenso/lib/types/search-params';
import { prisma } from '@documenso/prisma';
import type SubscriptionClaimSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionClaimSchema';
import { adminProcedure } from '../trpc';
import {
ZFindSubscriptionClaimsRequestSchema,
ZFindSubscriptionClaimsResponseSchema,
} from './find-subscription-claims.types';
export const findSubscriptionClaimsRoute = adminProcedure
.input(ZFindSubscriptionClaimsRequestSchema)
.output(ZFindSubscriptionClaimsResponseSchema)
.query(async ({ input }) => {
const { query, page, perPage } = input;
return await findSubscriptionClaims({ query, page, perPage });
});
type FindSubscriptionClaimsOptions = {
query?: string;
page?: number;
perPage?: number;
};
export const findSubscriptionClaims = async ({
query,
page = 1,
perPage = 50,
}: FindSubscriptionClaimsOptions) => {
let whereClause: Prisma.SubscriptionClaimWhereInput = {};
if (query) {
whereClause = {
OR: [
{
id: {
contains: query,
mode: 'insensitive',
},
},
{
name: {
contains: query,
mode: 'insensitive',
},
},
],
};
}
const [data, count] = await Promise.all([
prisma.subscriptionClaim.findMany({
where: whereClause,
skip: Math.max(page - 1, 0) * perPage,
take: perPage,
orderBy: {
createdAt: 'desc',
},
}),
prisma.subscriptionClaim.count({
where: whereClause,
}),
]);
return {
data,
count,
currentPage: Math.max(page, 1),
perPage,
totalPages: Math.ceil(count / perPage),
} satisfies FindResultResponse<z.infer<typeof SubscriptionClaimSchema>[]>;
};

View File

@ -0,0 +1,22 @@
import type { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import SubscriptionClaimSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionClaimSchema';
export const ZFindSubscriptionClaimsRequestSchema = ZFindSearchParamsSchema.extend({});
export const ZFindSubscriptionClaimsResponseSchema = ZFindResultResponse.extend({
data: SubscriptionClaimSchema.pick({
id: true,
createdAt: true,
updatedAt: true,
name: true,
teamCount: true,
memberCount: true,
locked: true,
flags: true,
}).array(),
});
export type TFindSubscriptionClaimsRequest = z.infer<typeof ZFindSubscriptionClaimsRequestSchema>;
export type TFindSubscriptionClaimsResponse = z.infer<typeof ZFindSubscriptionClaimsResponseSchema>;

View File

@ -0,0 +1,56 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZGetAdminOrganisationRequestSchema,
ZGetAdminOrganisationResponseSchema,
} from './get-admin-organisation.types';
export const getAdminOrganisationRoute = adminProcedure
.input(ZGetAdminOrganisationRequestSchema)
.output(ZGetAdminOrganisationResponseSchema)
.query(async ({ input }) => {
const { organisationId } = input;
return await getAdminOrganisation({
organisationId,
});
});
type GetOrganisationOptions = {
organisationId: string;
};
export const getAdminOrganisation = async ({ organisationId }: GetOrganisationOptions) => {
const organisation = await prisma.organisation.findFirst({
where: {
id: organisationId,
},
include: {
organisationClaim: true,
organisationGlobalSettings: true,
teams: true,
members: {
include: {
user: {
select: {
id: true,
email: true,
name: true,
},
},
},
},
subscription: true,
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Organisation not found',
});
}
return organisation;
};

View File

@ -0,0 +1,38 @@
import { z } from 'zod';
import { ZOrganisationSchema } from '@documenso/lib/types/organisation';
import OrganisationClaimSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationClaimSchema';
import OrganisationGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationGlobalSettingsSchema';
import OrganisationMemberSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
import SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
export const ZGetAdminOrganisationRequestSchema = z.object({
organisationId: z.string(),
});
export const ZGetAdminOrganisationResponseSchema = ZOrganisationSchema.extend({
organisationGlobalSettings: OrganisationGlobalSettingsSchema,
teams: z.array(
TeamSchema.pick({
id: true,
name: true,
url: true,
createdAt: true,
avatarImageId: true,
organisationId: true,
}),
),
members: OrganisationMemberSchema.extend({
user: UserSchema.pick({
id: true,
email: true,
name: true,
}),
}).array(),
subscription: SubscriptionSchema.nullable(),
organisationClaim: OrganisationClaimSchema,
});
export type TGetAdminOrganisationResponse = z.infer<typeof ZGetAdminOrganisationResponseSchema>;

View File

@ -14,6 +14,12 @@ import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { adminProcedure, router } from '../trpc';
import { createStripeCustomerRoute } from './create-stripe-customer';
import { createSubscriptionClaimRoute } from './create-subscription-claim';
import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
import { findAdminOrganisationsRoute } from './find-admin-organisations';
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
import { getAdminOrganisationRoute } from './get-admin-organisation';
import {
ZAdminDeleteDocumentMutationSchema,
ZAdminDeleteUserMutationSchema,
@ -25,8 +31,30 @@ import {
ZAdminUpdateRecipientMutationSchema,
ZAdminUpdateSiteSettingMutationSchema,
} from './schema';
import { updateAdminOrganisationRoute } from './update-admin-organisation';
import { updateSubscriptionClaimRoute } from './update-subscription-claim';
export const adminRouter = router({
// Todo: orgs Ensure all procedures are admin within this route.
// Todo: orgs Ensure all procedures are admin within this route.
// Todo: orgs Ensure all procedures are admin within this route.
// Todo: orgs Ensure all procedures are admin within this route.
organisation: {
find: findAdminOrganisationsRoute,
get: getAdminOrganisationRoute,
update: updateAdminOrganisationRoute,
},
claims: {
find: findSubscriptionClaimsRoute,
create: createSubscriptionClaimRoute,
update: updateSubscriptionClaimRoute,
delete: deleteSubscriptionClaimRoute,
},
stripe: {
createCustomer: createStripeCustomerRoute,
},
// Todo: migrate old routes
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
const { query, page, perPage } = input;

View File

@ -0,0 +1,51 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZUpdateAdminOrganisationRequestSchema,
ZUpdateAdminOrganisationResponseSchema,
} from './update-admin-organisation.types';
export const updateAdminOrganisationRoute = adminProcedure
.input(ZUpdateAdminOrganisationRequestSchema)
.output(ZUpdateAdminOrganisationResponseSchema)
.mutation(async ({ input }) => {
const { organisationId, data } = input;
const organisation = await prisma.organisation.findUnique({
where: {
id: organisationId,
},
include: {
organisationClaim: true,
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const { name, url, customerId, claims, originalSubscriptionClaimId } = data;
await prisma.organisation.update({
where: {
id: organisationId,
},
data: {
name,
url,
customerId: customerId === undefined ? null : customerId,
},
});
await prisma.organisationClaim.update({
where: {
id: organisation.organisationClaimId,
},
data: {
...claims,
originalSubscriptionClaimId,
},
});
});

View File

@ -0,0 +1,24 @@
import { z } from 'zod';
import { ZOrganisationNameSchema } from '../organisation-router/create-organisation.types';
import { ZTeamUrlSchema } from '../team-router/schema';
import { ZCreateSubscriptionClaimRequestSchema } from './create-subscription-claim.types';
export const ZUpdateAdminOrganisationRequestSchema = z.object({
organisationId: z.string(),
data: z.object({
name: ZOrganisationNameSchema.optional(),
url: ZTeamUrlSchema.optional(),
claims: ZCreateSubscriptionClaimRequestSchema.pick({
teamCount: true,
memberCount: true,
flags: true,
}).optional(),
customerId: z.string().optional(),
originalSubscriptionClaimId: z.string().optional(),
}),
});
export const ZUpdateAdminOrganisationResponseSchema = z.void();
export type TUpdateAdminOrganisationRequest = z.infer<typeof ZUpdateAdminOrganisationRequestSchema>;

View File

@ -0,0 +1,56 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TClaimFlags } from '@documenso/lib/types/subscription';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZUpdateSubscriptionClaimRequestSchema,
ZUpdateSubscriptionClaimResponseSchema,
} from './update-subscription-claim.types';
export const updateSubscriptionClaimRoute = adminProcedure
.input(ZUpdateSubscriptionClaimRequestSchema)
.output(ZUpdateSubscriptionClaimResponseSchema)
.mutation(async ({ input }) => {
const { id, data } = input;
const existingClaim = await prisma.subscriptionClaim.findUnique({
where: { id },
});
if (!existingClaim) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Subscription claim not found' });
}
const newlyEnabledFlags = getNewTruthyKeys(existingClaim.flags, data.flags);
console.log({
newlyEnabledFlags,
});
if (newlyEnabledFlags.length > 0) {
// Todo: orgs backport claims
}
await prisma.subscriptionClaim.update({
where: {
id,
},
data,
});
});
type BoolMap = Record<string, boolean | undefined>;
/**
* Get the new truthy keys from the existing flags and the new flags.
*
* @param a - The existing flags.
* @param b - The new flags.
* @returns The new truthy keys.
*/
function getNewTruthyKeys(a: BoolMap, b: BoolMap): (keyof TClaimFlags)[] {
return Object.entries(b)
.filter(([key, value]) => value && !a[key])
.map(([key]) => key as keyof TClaimFlags);
}

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
import { ZCreateSubscriptionClaimRequestSchema } from './create-subscription-claim.types';
export const ZUpdateSubscriptionClaimRequestSchema = z.object({
id: z.string(),
data: ZCreateSubscriptionClaimRequestSchema,
});
export const ZUpdateSubscriptionClaimResponseSchema = z.void();
export type TUpdateSubscriptionClaimRequest = z.infer<typeof ZUpdateSubscriptionClaimRequestSchema>;

View File

@ -0,0 +1,81 @@
import { createCheckoutSession } from '@documenso/ee/server-only/stripe/create-checkout-session';
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
import { IS_BILLING_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import { ZCreateSubscriptionRequestSchema } from './create-subscription.types';
export const createSubscriptionRoute = authenticatedProcedure
.input(ZCreateSubscriptionRequestSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId, priceId } = input;
const userId = ctx.user.id;
if (!IS_BILLING_ENABLED()) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Billing is not enabled',
});
}
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_BILLING'],
),
include: {
subscription: true,
owner: {
select: {
email: true,
name: true,
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
let customerId = organisation.customerId;
if (!customerId) {
const customer = await createCustomer({
name: organisation.name,
email: organisation.owner.email,
});
customerId = customer.id;
await prisma.organisation.update({
where: {
id: organisationId,
},
data: {
customerId: customer.id,
},
});
}
const redirectUrl = await createCheckoutSession({
customerId,
priceId,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/org/${organisation.url}/settings/billing`,
});
if (!redirectUrl) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Failed to create checkout session',
});
}
return {
redirectUrl,
};
});

View File

@ -0,0 +1,6 @@
import { z } from 'zod';
export const ZCreateSubscriptionRequestSchema = z.object({
organisationId: z.string().describe('The organisation to create the subscription for'),
priceId: z.string().describe('The price to create the subscription for'),
});

View File

@ -0,0 +1,58 @@
import { getInvoices } from '@documenso/ee/server-only/stripe/get-invoices';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import { ZGetInvoicesRequestSchema } from './get-invoices.types';
export const getInvoicesRoute = authenticatedProcedure
.input(ZGetInvoicesRequestSchema)
.query(async ({ ctx, input }) => {
const { organisationId } = input;
const userId = ctx.user.id;
if (!IS_BILLING_ENABLED()) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Billing is not enabled',
});
}
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
include: {
subscription: true,
},
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You are not authorized to access this organisation',
});
}
if (!organisation.subscription?.customerId) {
return null;
}
const invoices = await getInvoices({
customerId: organisation.subscription?.customerId,
});
return invoices.data.map((invoice) => ({
id: invoice.id,
status: invoice.status,
created: invoice.created,
currency: invoice.currency,
total: invoice.total,
hosted_invoice_url: invoice.hosted_invoice_url,
invoice_pdf: invoice.invoice_pdf,
}));
});

View File

@ -0,0 +1,5 @@
import { z } from 'zod';
export const ZGetInvoicesRequestSchema = z.object({
organisationId: z.string().describe('The organisation to get the invoices for'),
});

View File

@ -0,0 +1,31 @@
import { getInternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
export const getPlansRoute = authenticatedProcedure.query(async ({ ctx }) => {
const userId = ctx.user.id;
const plans = await getInternalClaimPlans();
let canCreateFreeOrganisation = false;
if (IS_BILLING_ENABLED()) {
const numberOfFreeOrganisations = await prisma.organisation.count({
where: {
ownerUserId: userId,
subscription: {
is: null,
},
},
});
canCreateFreeOrganisation = numberOfFreeOrganisations === 0;
}
return {
plans,
canCreateFreeOrganisation,
};
});

View File

@ -0,0 +1,34 @@
import { getInternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans';
import { getSubscription } from '@documenso/ee/server-only/stripe/get-subscription';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { authenticatedProcedure } from '../trpc';
import { ZGetSubscriptionRequestSchema } from './get-subscription.types';
export const getSubscriptionRoute = authenticatedProcedure
.input(ZGetSubscriptionRequestSchema)
.query(async ({ ctx, input }) => {
const { organisationId } = input;
const userId = ctx.user.id;
if (!IS_BILLING_ENABLED()) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Billing is not enabled',
});
}
const [subscription, plans] = await Promise.all([
getSubscription({
organisationId,
userId,
}),
getInternalClaimPlans(),
]);
return {
subscription,
plans,
};
});

View File

@ -0,0 +1,5 @@
import { z } from 'zod';
export const ZGetSubscriptionRequestSchema = z.object({
organisationId: z.string().describe('The organisation to get the subscription for'),
});

View File

@ -0,0 +1,74 @@
import { createCustomer } from '@documenso/ee/server-only/stripe/create-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';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import { ZManageSubscriptionRequestSchema } from './manage-subscription.types';
export const manageSubscriptionRoute = authenticatedProcedure
.input(ZManageSubscriptionRequestSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId } = input;
const userId = ctx.user.id;
if (!IS_BILLING_ENABLED()) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Billing is not enabled',
});
}
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_BILLING'],
),
include: {
subscription: true,
owner: {
select: {
email: true,
name: true,
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
let customerId = organisation.customerId;
if (!customerId) {
const customer = await createCustomer({
name: organisation.name,
email: organisation.owner.email,
});
customerId = customer.id;
await prisma.organisation.update({
where: {
id: organisationId,
},
data: {
customerId: customer.id,
},
});
}
const redirectUrl = await getPortalSession({
customerId,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/org/${organisation.url}/settings/billing`,
});
return {
redirectUrl,
};
});

View File

@ -0,0 +1,5 @@
import { z } from 'zod';
export const ZManageSubscriptionRequestSchema = z.object({
organisationId: z.string().describe('The organisation to manage the subscription for'),
});

View File

@ -0,0 +1,20 @@
import { router } from '../trpc';
import { createSubscriptionRoute } from './create-subscription';
import { getInvoicesRoute } from './get-invoices';
import { getPlansRoute } from './get-plans';
import { getSubscriptionRoute } from './get-subscription';
import { manageSubscriptionRoute } from './manage-subscription';
export const billingRouter = router({
plans: {
get: getPlansRoute,
},
subscription: {
get: getSubscriptionRoute,
create: createSubscriptionRoute,
manage: manageSubscriptionRoute,
},
invoices: {
get: getInvoicesRoute,
},
});

View File

@ -238,7 +238,7 @@ export const documentRouter = router({
.input(ZCreateDocumentV2RequestSchema)
.output(ZCreateDocumentV2ResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { teamId, user } = ctx;
const {
title,
@ -250,7 +250,7 @@ export const documentRouter = router({
meta,
} = input;
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
const { remaining } = await getServerLimits({ userId: user.id, teamId });
if (remaining.documents <= 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
@ -307,10 +307,10 @@ export const documentRouter = router({
// })
.input(ZCreateDocumentRequestSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { teamId, user } = ctx;
const { title, documentDataId, timezone } = input;
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
const { remaining } = await getServerLimits({ userId: user.id, teamId });
if (remaining.documents <= 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
@ -320,7 +320,7 @@ export const documentRouter = router({
}
return await createDocument({
userId: ctx.user.id,
userId: user.id,
teamId,
title,
documentDataId,

View File

@ -1,4 +1,10 @@
import { createCheckoutSession } from '@documenso/ee/server-only/stripe/create-checkout-session';
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
import { IS_BILLING_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createOrganisation } from '@documenso/lib/server-only/organisation/create-organisation';
import { generateStripeOrganisationCreateMetadata } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
@ -11,12 +17,60 @@ export const createOrganisationRoute = authenticatedProcedure
.input(ZCreateOrganisationRequestSchema)
.output(ZCreateOrganisationResponseSchema)
.mutation(async ({ input, ctx }) => {
const { name, url } = input;
const { name, priceId } = input;
const { user } = ctx;
// Todo: orgs
// Todo: Check if user has reached limit.
if (IS_BILLING_ENABLED() && !priceId) {
const userOrganisations = await prisma.organisation.findMany({
where: {
ownerUserId: user.id,
subscription: {
is: null,
},
},
});
if (userOrganisations.length >= 1) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached the maximum number of free organisations.',
});
}
}
// Create checkout session for payment.
if (IS_BILLING_ENABLED() && priceId) {
// if (!claimId) {
// throw new AppError(AppErrorCode.INVALID_REQUEST, {
// message: 'Claim ID is required',
// });
// }
const customer = await createCustomer({
email: user.email,
name: user.name || user.email,
});
const checkoutUrl = await createCheckoutSession({
priceId,
customerId: customer.id,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/organisations`,
subscriptionMetadata: generateStripeOrganisationCreateMetadata(name, user.id),
});
return {
paymentRequired: true,
checkoutUrl,
};
}
await createOrganisation({
userId: user.id,
name,
url,
});
return {
paymentRequired: false,
};
});

View File

@ -1,7 +1,5 @@
import { z } from 'zod';
import { ZTeamUrlSchema } from '../team-router/schema';
// export const createOrganisationMeta: TrpcOpenApiMeta = {
// openapi: {
// method: 'POST',
@ -19,7 +17,17 @@ export const ZOrganisationNameSchema = z
export const ZCreateOrganisationRequestSchema = z.object({
name: ZOrganisationNameSchema,
url: ZTeamUrlSchema,
priceId: z.string().optional(),
});
export const ZCreateOrganisationResponseSchema = z.void();
export const ZCreateOrganisationResponseSchema = z.union([
z.object({
paymentRequired: z.literal(false),
}),
z.object({
paymentRequired: z.literal(true),
checkoutUrl: z.string(),
}),
]);
export type TCreateOrganisationResponse = z.infer<typeof ZCreateOrganisationResponseSchema>;

View File

@ -1,5 +1,7 @@
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { validateIfSubscriptionIsRequired } from '@documenso/lib/utils/billing';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
@ -23,12 +25,42 @@ export const deleteOrganisationMemberInvitesRoute = authenticatedProcedure
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
include: {
organisationClaim: true,
subscription: true,
members: {
select: {
id: true,
},
},
invites: {
select: {
id: true,
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const { organisationClaim } = organisation;
const subscription = validateIfSubscriptionIsRequired(organisation.subscription);
const numberOfCurrentMembers = organisation.members.length;
const numberOfCurrentInvites = organisation.invites.length;
const totalMemberCountWithInvites = numberOfCurrentMembers + numberOfCurrentInvites - 1;
if (subscription) {
await syncMemberCountWithStripeSeatPlan(
subscription,
organisationClaim,
totalMemberCountWithInvites,
);
}
await prisma.organisationMemberInvite.deleteMany({
where: {
id: {

View File

@ -1,7 +1,10 @@
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { validateIfSubscriptionIsRequired } from '@documenso/lib/utils/billing';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { OrganisationMemberInviteStatus } from '@documenso/prisma/client';
import { authenticatedProcedure } from '../trpc';
import {
@ -30,76 +33,73 @@ type DeleteOrganisationMembersProps = {
organisationMemberIds: string[];
};
/**
* Deletes multiple organisation members.
*
* This logic is also used to leave a team (hence strange logic).
*/
export const deleteOrganisationMembers = async ({
userId,
organisationId,
organisationMemberIds,
}: DeleteOrganisationMembersProps) => {
const membersToDelete = await prisma.organisationMember.findMany({
where: {
id: {
in: organisationMemberIds,
},
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
include: {
subscription: true,
organisationClaim: true,
members: {
select: {
id: true,
userId: true,
},
},
invites: {
where: {
status: OrganisationMemberInviteStatus.PENDING,
},
select: {
id: true,
},
},
},
});
// Prevent the user from deleting other users if they do not have permission.
if (membersToDelete.some((member) => member.userId !== userId)) {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(
organisationId,
userId,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
),
});
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
if (!organisation) {
throw new AppError(AppErrorCode.UNAUTHORIZED);
}
// Todo: Orgs - Handle seats.
await prisma.$transaction(
async (tx) => {
await tx.organisationMember.deleteMany({
where: {
id: {
in: organisationMemberIds,
},
organisationId,
},
});
const { organisationClaim } = organisation;
// Todo: orgs handle removing groups
// if (IS_BILLING_ENABLED() && team.subscription) {
// const numberOfSeats = await tx.teamMember.count({
// where: {
// teamId,
// },
// });
// await updateSubscriptionItemQuantity({
// priceId: team.subscription.priceId,
// subscriptionId: team.subscription.planId,
// quantity: numberOfSeats,
// });
// }
// await jobs.triggerJob({
// name: 'send.team-member-left.email',
// payload: {
// teamId,
// memberUserId: leavingUser.id,
// },
// });
},
{ timeout: 30_000 },
const membersToDelete = organisation.members.filter((member) =>
organisationMemberIds.includes(member.id),
);
const subscription = validateIfSubscriptionIsRequired(organisation.subscription);
const inviteCount = organisation.invites.length;
const newMemberCount = organisation.members.length + inviteCount - membersToDelete.length;
if (subscription) {
await syncMemberCountWithStripeSeatPlan(subscription, organisationClaim, newMemberCount);
}
await prisma.$transaction(async (tx) => {
await tx.organisationMember.deleteMany({
where: {
id: {
in: organisationMemberIds,
},
organisationId,
},
});
// Todo: orgs
// await jobs.triggerJob({
// name: 'send.team-member-left.email',
// payload: {
// teamId,
// memberUserId: leavingUser.id,
// },
// });
});
};

View File

@ -25,6 +25,7 @@ export const getOrganisationMemberInvitesRoute = authenticatedProcedure
select: {
id: true,
name: true,
url: true,
avatarImageId: true,
},
},

View File

@ -19,6 +19,7 @@ export const ZGetOrganisationMemberInvitesResponseSchema = OrganisationMemberInv
organisation: OrganisationSchema.pick({
id: true,
name: true,
url: true,
avatarImageId: true,
}),
})

View File

@ -29,6 +29,7 @@ export const getOrganisationSession = async ({
},
},
include: {
organisationClaim: true,
groups: {
where: {
organisationGroupMembers: {

View File

@ -44,6 +44,8 @@ export const getOrganisation = async ({
},
include: {
organisationGlobalSettings: true,
subscription: true,
organisationClaim: true,
teams: {
where: {
teamGroups: {

View File

@ -1,7 +1,9 @@
import { z } from 'zod';
import { ZOrganisationSchema } from '@documenso/lib/types/organisation';
import OrganisationClaimSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationClaimSchema';
import OrganisationGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationGlobalSettingsSchema';
import SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
// export const getOrganisationMeta: TrpcOpenApiMeta = {
@ -20,6 +22,8 @@ export const ZGetOrganisationRequestSchema = z.object({
export const ZGetOrganisationResponseSchema = ZOrganisationSchema.extend({
organisationGlobalSettings: OrganisationGlobalSettingsSchema,
organisationClaim: OrganisationClaimSchema,
subscription: SubscriptionSchema.nullable(),
teams: z.array(
TeamSchema.pick({
id: true,

View File

@ -0,0 +1,75 @@
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { validateIfSubscriptionIsRequired } from '@documenso/lib/utils/billing';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
import { OrganisationMemberInviteStatus } from '@documenso/prisma/client';
import { authenticatedProcedure } from '../trpc';
import {
ZLeaveOrganisationRequestSchema,
ZLeaveOrganisationResponseSchema,
} from './leave-organisation.types';
export const leaveOrganisationRoute = authenticatedProcedure
.input(ZLeaveOrganisationRequestSchema)
.output(ZLeaveOrganisationResponseSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId } = input;
const userId = ctx.user.id;
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery(organisationId, userId),
include: {
organisationClaim: true,
subscription: true,
invites: {
where: {
status: OrganisationMemberInviteStatus.PENDING,
},
select: {
id: true,
},
},
members: {
select: {
id: true,
},
},
},
});
if (!organisation) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
const { organisationClaim } = organisation;
const subscription = validateIfSubscriptionIsRequired(organisation.subscription);
const inviteCount = organisation.invites.length;
const newMemberCount = organisation.members.length + inviteCount - 1;
if (subscription) {
await syncMemberCountWithStripeSeatPlan(subscription, organisationClaim, newMemberCount);
}
await prisma.organisationMember.delete({
where: {
userId_organisationId: {
userId,
organisationId,
},
},
});
await jobs.triggerJob({
name: 'send.organisation-member-left.email',
payload: {
organisationId: organisation.id,
memberUserId: userId,
},
});
});

View File

@ -0,0 +1,7 @@
import { z } from 'zod';
export const ZLeaveOrganisationRequestSchema = z.object({
organisationId: z.string(),
});
export const ZLeaveOrganisationResponseSchema = z.void();

View File

@ -16,6 +16,7 @@ import { getOrganisationRoute } from './get-organisation';
import { getOrganisationMemberInvitesRoute } from './get-organisation-member-invites';
import { getOrganisationSessionRoute } from './get-organisation-session';
import { getOrganisationsRoute } from './get-organisations';
import { leaveOrganisationRoute } from './leave-organisation';
import { resendOrganisationMemberInviteRoute } from './resend-organisation-member-invite';
import { updateOrganisationRoute } from './update-organisation';
import { updateOrganisationGroupRoute } from './update-organisation-group';
@ -28,6 +29,7 @@ export const organisationRouter = router({
create: createOrganisationRoute,
update: updateOrganisationRoute,
delete: deleteOrganisationRoute,
leave: leaveOrganisationRoute,
member: {
find: findOrganisationMembersRoute,
update: updateOrganisationMemberRoute,

View File

@ -39,7 +39,7 @@ export const updateOrganisationRoute = authenticatedProcedure
},
data: {
name: data.name,
url: data.url, // Todo: (orgs) check url unique
url: data.url, // Todo: orgs check url unique
},
});
});

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { ZTeamUrlSchema } from '../team-router/schema';
import { ZCreateOrganisationRequestSchema } from './create-organisation.types';
// export const updateOrganisationMeta: TrpcOpenApiMeta = {
@ -15,7 +16,8 @@ import { ZCreateOrganisationRequestSchema } from './create-organisation.types';
export const ZUpdateOrganisationRequestSchema = z.object({
data: ZCreateOrganisationRequestSchema.pick({
name: true,
url: true,
}).extend({
url: ZTeamUrlSchema,
}),
organisationId: z.string(),
});

View File

@ -1,6 +1,4 @@
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
import { createBillingPortal } from '@documenso/lib/server-only/user/create-billing-portal';
import { createCheckoutSession } from '@documenso/lib/server-only/user/create-checkout-session';
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
@ -8,7 +6,6 @@ import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
import { adminProcedure, authenticatedProcedure, router } from '../trpc';
import {
ZCreateCheckoutSessionRequestSchema,
ZFindUserSecurityAuditLogsSchema,
ZRetrieveUserByIdQuerySchema,
ZSetProfileImageMutationSchema,
@ -31,31 +28,6 @@ export const profileRouter = router({
return await getUserById({ id });
}),
createBillingPortal: authenticatedProcedure.mutation(async ({ ctx }) => {
return await createBillingPortal({
user: {
id: ctx.user.id,
customerId: ctx.user.customerId,
email: ctx.user.email,
name: ctx.user.name,
},
});
}),
createCheckoutSession: authenticatedProcedure
.input(ZCreateCheckoutSessionRequestSchema)
.mutation(async ({ ctx, input }) => {
return await createCheckoutSession({
user: {
id: ctx.user.id,
customerId: ctx.user.customerId,
email: ctx.user.email,
name: ctx.user.name,
},
priceId: input.priceId,
});
}),
updateProfile: authenticatedProcedure
.input(ZUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => {

View File

@ -13,10 +13,6 @@ export const ZRetrieveUserByIdQuerySchema = z.object({
export type TRetrieveUserByIdQuerySchema = z.infer<typeof ZRetrieveUserByIdQuerySchema>;
export const ZCreateCheckoutSessionRequestSchema = z.object({
priceId: z.string().min(1),
});
export const ZUpdateProfileMutationSchema = z.object({
name: z.string().min(1),
signature: z.string(),

View File

@ -1,6 +1,7 @@
import { adminRouter } from './admin-router/router';
import { apiTokenRouter } from './api-token-router/router';
import { authRouter } from './auth-router/router';
import { billingRouter } from './billing/router';
import { documentRouter } from './document-router/router';
import { fieldRouter } from './field-router/router';
import { organisationRouter } from './organisation-router/router';
@ -14,6 +15,7 @@ import { webhookRouter } from './webhook-router/router';
export const appRouter = router({
auth: authRouter,
billing: billingRouter,
profile: profileRouter,
document: documentRouter,
field: fieldRouter,

View File

@ -24,15 +24,6 @@ export const ZCreateTeamRequestSchema = z.object({
),
});
export const ZCreateTeamResponseSchema = z.union([
z.object({
paymentRequired: z.literal(false),
}),
z.object({
paymentRequired: z.literal(true),
pendingTeamId: z.number(),
}),
]);
export const ZCreateTeamResponseSchema = z.void();
export type TCreateTeamRequest = z.infer<typeof ZCreateTeamRequestSchema>;
export type TCreateTeamResponse = z.infer<typeof ZCreateTeamResponseSchema>;

View File

@ -1,13 +1,9 @@
import { TRPCError } from '@trpc/server';
import { getTeamPrices } from '@documenso/ee/server-only/stripe/get-team-prices';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createTeamPendingCheckoutSession } from '@documenso/lib/server-only/team/create-team-checkout-session';
import { createTeamEmailVerification } from '@documenso/lib/server-only/team/create-team-email-verification';
import { deleteTeamEmail } from '@documenso/lib/server-only/team/delete-team-email';
import { deleteTeamEmailVerification } from '@documenso/lib/server-only/team/delete-team-email-verification';
import { deleteTeamPending } from '@documenso/lib/server-only/team/delete-team-pending';
import { findTeamsPending } from '@documenso/lib/server-only/team/find-teams-pending';
import { getTeamEmailByEmail } from '@documenso/lib/server-only/team/get-team-email-by-email';
import { resendTeamEmailVerification } from '@documenso/lib/server-only/team/resend-team-email-verification';
import { updateTeamEmail } from '@documenso/lib/server-only/team/update-team-email';
@ -27,11 +23,8 @@ import { getTeamRoute } from './get-team';
import { getTeamMembersRoute } from './get-team-members';
import {
ZCreateTeamEmailVerificationMutationSchema,
ZCreateTeamPendingCheckoutMutationSchema,
ZDeleteTeamEmailMutationSchema,
ZDeleteTeamEmailVerificationMutationSchema,
ZDeleteTeamPendingMutationSchema,
ZFindTeamsPendingQuerySchema,
ZResendTeamEmailVerificationMutationSchema,
ZUpdateTeamEmailMutationSchema,
ZUpdateTeamPublicProfileMutationSchema,
@ -65,41 +58,61 @@ export const teamRouter = router({
},
// Old routes (to be migrated)
// Internal endpoint for now.
createTeamEmailVerification: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/email/create',
// summary: 'Create team email',
// description: 'Add an email to a team and send an email request to verify it',
// tags: ['Teams'],
// },
// })
.input(ZCreateTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
return await createTeamEmailVerification({
teamId: input.teamId,
userId: ctx.user.id,
data: {
email: input.email,
name: input.name,
},
});
// Todo: Refactor into routes.
email: {
get: authenticatedProcedure.query(async ({ ctx }) => {
return await getTeamEmailByEmail({ email: ctx.user.email });
}),
update: authenticatedProcedure
.input(ZUpdateTeamEmailMutationSchema)
.mutation(async ({ input, ctx }) => {
return await updateTeamEmail({
userId: ctx.user.id,
...input,
});
}),
delete: authenticatedProcedure
.input(ZDeleteTeamEmailMutationSchema)
.mutation(async ({ input, ctx }) => {
return await deleteTeamEmail({
userId: ctx.user.id,
userEmail: ctx.user.email,
...input,
});
}),
verification: {
send: authenticatedProcedure
.input(ZCreateTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
return await createTeamEmailVerification({
teamId: input.teamId,
userId: ctx.user.id,
data: {
email: input.email,
name: input.name,
},
});
}),
resend: authenticatedProcedure
.input(ZResendTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
await resendTeamEmailVerification({
userId: ctx.user.id,
...input,
});
}),
delete: authenticatedProcedure
.input(ZDeleteTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
return await deleteTeamEmailVerification({
userId: ctx.user.id,
...input,
});
}),
},
},
// Todo: Public endpoint.
updateTeamPublicProfile: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/profile',
// summary: 'Update a team public profile',
// description: '',
// tags: ['Teams'],
// },
// })
.input(ZUpdateTeamPublicProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
@ -129,137 +142,4 @@ export const teamRouter = router({
});
}
}),
// Todo
getTeamEmailByEmail: authenticatedProcedure.query(async ({ ctx }) => {
return await getTeamEmailByEmail({ email: ctx.user.email });
}),
// Internal endpoint for now.
updateTeamEmail: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/email',
// summary: 'Update a team email',
// description: '',
// tags: ['Teams'],
// },
// })
.input(ZUpdateTeamEmailMutationSchema)
.mutation(async ({ input, ctx }) => {
return await updateTeamEmail({
userId: ctx.user.id,
...input,
});
}),
// Internal endpoint for now.
deleteTeamEmail: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/email/delete',
// summary: 'Delete team email',
// description: '',
// tags: ['Teams'],
// },
// })
.input(ZDeleteTeamEmailMutationSchema)
.mutation(async ({ input, ctx }) => {
return await deleteTeamEmail({
userId: ctx.user.id,
userEmail: ctx.user.email,
...input,
});
}),
// Internal endpoint for now.
resendTeamEmailVerification: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/email/resend',
// summary: 'Resend team email verification',
// tags: ['Teams'],
// },
// })
.input(ZResendTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
await resendTeamEmailVerification({
userId: ctx.user.id,
...input,
});
}),
// Internal endpoint for now.
deleteTeamEmailVerification: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/{teamId}/email/verify/delete',
// summary: 'Delete team email verification',
// tags: ['Teams'],
// },
// })
.input(ZDeleteTeamEmailVerificationMutationSchema)
.mutation(async ({ input, ctx }) => {
return await deleteTeamEmailVerification({
userId: ctx.user.id,
...input,
});
}),
// Internal endpoint for now.
createTeamPendingCheckout: authenticatedProcedure
.input(ZCreateTeamPendingCheckoutMutationSchema)
.mutation(async ({ input, ctx }) => {
return await createTeamPendingCheckoutSession({
userId: ctx.user.id,
...input,
});
}),
// Internal endpoint for now.
getTeamPrices: authenticatedProcedure.query(async () => {
return await getTeamPrices();
}),
// Internal endpoint for now.
findTeamsPending: authenticatedProcedure
// .meta({
// openapi: {
// method: 'GET',
// path: '/team/pending',
// summary: 'Find pending teams',
// description: 'Find teams that are pending payment',
// tags: ['Teams'],
// },
// })
.input(ZFindTeamsPendingQuerySchema)
.query(async ({ input, ctx }) => {
return await findTeamsPending({
userId: ctx.user.id,
...input,
});
}),
// Internal endpoint for now.
deleteTeamPending: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/team/pending/{pendingTeamId}/delete',
// summary: 'Delete pending team',
// description: '',
// tags: ['Teams'],
// },
// })
.input(ZDeleteTeamPendingMutationSchema)
.mutation(async ({ input, ctx }) => {
return await deleteTeamPending({
userId: ctx.user.id,
...input,
});
}),
});

View File

@ -2,7 +2,6 @@ import { TeamMemberRole } from '@prisma/client';
import { z } from 'zod';
import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams';
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
export const MAX_PROFILE_BIO_LENGTH = 256;
@ -49,11 +48,6 @@ export const ZCreateTeamEmailVerificationMutationSchema = z.object({
email: z.string().trim().email().toLowerCase().min(1, 'Please enter a valid email.'),
});
export const ZCreateTeamPendingCheckoutMutationSchema = z.object({
interval: z.union([z.literal('monthly'), z.literal('yearly')]),
pendingTeamId: z.number(),
});
export const ZDeleteTeamEmailMutationSchema = z.object({
teamId: z.number(),
});
@ -62,16 +56,6 @@ export const ZDeleteTeamEmailVerificationMutationSchema = z.object({
teamId: z.number(),
});
export const ZDeleteTeamMutationSchema = z.object({
teamId: z.number(),
});
export const ZDeleteTeamPendingMutationSchema = z.object({
pendingTeamId: z.number(),
});
export const ZFindTeamsPendingQuerySchema = ZFindSearchParamsSchema;
export const ZGetTeamMembersQuerySchema = z.object({
teamId: z.number(),
});
@ -119,13 +103,7 @@ export type TCreateTeamEmailVerificationMutationSchema = z.infer<
typeof ZCreateTeamEmailVerificationMutationSchema
>;
export type TCreateTeamPendingCheckoutMutationSchema = z.infer<
typeof ZCreateTeamPendingCheckoutMutationSchema
>;
export type TDeleteTeamEmailMutationSchema = z.infer<typeof ZDeleteTeamEmailMutationSchema>;
export type TDeleteTeamMutationSchema = z.infer<typeof ZDeleteTeamMutationSchema>;
export type TDeleteTeamPendingMutationSchema = z.infer<typeof ZDeleteTeamPendingMutationSchema>;
export type TFindTeamsPendingQuerySchema = z.infer<typeof ZFindTeamsPendingQuerySchema>;
export type TGetTeamMembersQuerySchema = z.infer<typeof ZGetTeamMembersQuerySchema>;
export type TUpdateTeamEmailMutationSchema = z.infer<typeof ZUpdateTeamEmailMutationSchema>;
export type TResendTeamEmailVerificationMutationSchema = z.infer<