feat: add direct templates links (#1165)

## Description

Direct templates links is a feature that provides template owners the
ability to allow users to create documents based of their templates.

## General outline

This works by allowing the template owner to configure a "direct
recipient" in the template.

When a user opens the direct link to the template, it will create a flow
where they sign the fields configured by the template owner for the
direct recipient. After these fields are signed the following will
occur:

- A document will be created where the owner is the template owner
- The direct recipient fields will be signed
- The document will be sent to any other recipients configured in the
template
- If there are none the document will be immediately completed

## Notes

There's a custom prisma migration to migrate all documents to have
'DOCUMENT' as the source, then sets the column to required.

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
This commit is contained in:
David Nguyen
2024-06-02 15:49:09 +10:00
committed by GitHub
parent c346a3fd6a
commit d11a68fc4c
71 changed files with 3636 additions and 283 deletions

View File

@ -3,14 +3,17 @@ import type { TLimitsSchema } from './schema';
export const FREE_PLAN_LIMITS: TLimitsSchema = {
documents: 5,
recipients: 10,
directTemplates: 3,
};
export const TEAM_PLAN_LIMITS: TLimitsSchema = {
documents: Infinity,
recipients: Infinity,
directTemplates: Infinity,
};
export const SELFHOSTED_PLAN_LIMITS: TLimitsSchema = {
documents: Infinity,
recipients: Infinity,
directTemplates: Infinity,
};

View File

@ -10,6 +10,10 @@ export const ZLimitsSchema = z.object({
.preprocess((v) => (v === null ? Infinity : Number(v)), z.number())
.optional()
.default(0),
directTemplates: z
.preprocess((v) => (v === null ? Infinity : Number(v)), z.number())
.optional()
.default(0),
});
export type TLimitsSchema = z.infer<typeof ZLimitsSchema>;

View File

@ -2,11 +2,12 @@ import { DateTime } from 'luxon';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
import { DocumentSource, SubscriptionStatus } from '@documenso/prisma/client';
import { getDocumentRelatedPrices } from '../stripe/get-document-related-prices.ts';
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS, TEAM_PLAN_LIMITS } from './constants';
import { ERROR_CODES } from './errors';
import type { TLimitsResponseSchema } from './schema';
import { ZLimitsSchema } from './schema';
export type GetServerLimitsOptions = {
@ -14,7 +15,10 @@ export type GetServerLimitsOptions = {
teamId?: number | null;
};
export const getServerLimits = async ({ email, teamId }: GetServerLimitsOptions) => {
export const getServerLimits = async ({
email,
teamId,
}: GetServerLimitsOptions): Promise<TLimitsResponseSchema> => {
if (!IS_BILLING_ENABLED()) {
return {
quota: SELFHOSTED_PLAN_LIMITS,
@ -74,19 +78,37 @@ const handleUserLimits = async ({ email }: HandleUserLimitsOptions) => {
remaining = structuredClone(quota);
}
}
// Assume all active subscriptions provide unlimited direct templates.
remaining.directTemplates = Infinity;
}
const documents = await prisma.document.count({
where: {
userId: user.id,
teamId: null,
createdAt: {
gte: DateTime.utc().startOf('month').toJSDate(),
const [documents, directTemplates] = await Promise.all([
prisma.document.count({
where: {
userId: user.id,
teamId: null,
createdAt: {
gte: DateTime.utc().startOf('month').toJSDate(),
},
source: {
not: DocumentSource.TEMPLATE_DIRECT_LINK,
},
},
},
});
}),
prisma.template.count({
where: {
userId: user.id,
teamId: null,
directLink: {
isNot: null,
},
},
}),
]);
remaining.documents = Math.max(remaining.documents - documents, 0);
remaining.directTemplates = Math.max(remaining.directTemplates - directTemplates, 0);
return {
quota,
@ -127,10 +149,12 @@ const handleTeamLimits = async ({ email, teamId }: HandleTeamLimitsOptions) => {
quota: {
documents: 0,
recipients: 0,
directTemplates: 0,
},
remaining: {
documents: 0,
recipients: 0,
directTemplates: 0,
},
};
}

View File

@ -8,6 +8,7 @@ import { alphaid, nanoid } from '@documenso/lib/universal/id';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { prisma } from '@documenso/prisma';
import {
DocumentSource,
DocumentStatus,
FieldType,
ReadStatus,
@ -86,6 +87,7 @@ export const onEarlyAdoptersCheckout = async ({ session }: OnEarlyAdoptersChecko
status: DocumentStatus.COMPLETED,
userId: newUser.id,
documentDataId,
source: DocumentSource.DOCUMENT,
},
});