import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field'; import type { Envelope } from '@prisma/client'; import { type Field, RecipientRole, SigningStatus } from '@prisma/client'; import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app'; import { AppError, AppErrorCode } from '../errors/app-error'; import type { TRecipientLite } from '../types/recipient'; import { extractLegacyIds } from '../universal/id'; import { zEmail } from './zod'; /** * Roles that require fields to be assigned before a document can be distributed. * * Currently only SIGNER requires a signature field. */ export const RECIPIENT_ROLES_THAT_REQUIRE_FIELDS = [RecipientRole.SIGNER] as const; /** * Returns recipients who are missing required fields for their role. * * Currently only SIGNERs are validated - they must have at least one signature field. */ export const getRecipientsWithMissingFields = >( recipients: T[], fields: Pick[], ): T[] => { return recipients.filter((recipient) => { if (recipient.role === RecipientRole.SIGNER) { const hasSignatureField = fields.some( (field) => field.recipientId === recipient.id && isSignatureFieldType(field.type), ); return !hasSignatureField; } return false; }); }; export const formatSigningLink = (token: string) => `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}`; /** * Whether a recipient can be modified by the document owner. */ export const canRecipientBeModified = ( recipient: TRecipientLite, fields: Pick[], ) => { if (!recipient) { return false; } // CCers can always be modified (unless document is completed). if (recipient.role === RecipientRole.CC) { return true; } // Deny if the recipient has already signed the document. if (recipient.signingStatus === SigningStatus.SIGNED) { return false; } // Deny if the recipient has inserted any fields. if (fields.some((field) => field.recipientId === recipient.id && field.inserted)) { return false; } return true; }; /** * Whether a recipient can have their fields modified by the document owner. * * A recipient can their fields modified if all the conditions are met: * - They are not a Viewer or CCer * - They can be modified (canRecipientBeModified) */ export const canRecipientFieldsBeModified = ( recipient: TRecipientLite, fields: Pick[], ) => { if (!canRecipientBeModified(recipient, fields)) { return false; } return recipient.role !== RecipientRole.VIEWER && recipient.role !== RecipientRole.CC; }; export const mapRecipientToLegacyRecipient = ( recipient: TRecipientLite, envelope: Pick, ) => { const legacyId = extractLegacyIds(envelope); return { ...recipient, ...legacyId, }; }; export const findRecipientByEmail = ({ recipients, userEmail, teamEmail, }: { recipients: T[]; userEmail: string; teamEmail?: string | null; }) => recipients.find((r) => r.email === userEmail || (teamEmail && r.email === teamEmail)); export const isRecipientEmailValidForSending = (recipient: Pick) => { return zEmail().safeParse(recipient.email).success; }; /** * Whether the recipient's signing window has expired. */ export const isRecipientExpired = (recipient: { expiresAt: Date | null }) => { return Boolean(recipient.expiresAt && new Date(recipient.expiresAt) <= new Date()); }; /** * Asserts that the recipient's signing window has not expired. * * Throws an AppError with RECIPIENT_EXPIRED if the expiration date has passed. */ export const assertRecipientNotExpired = (recipient: { expiresAt: Date | null }) => { if (isRecipientExpired(recipient)) { throw new AppError(AppErrorCode.RECIPIENT_EXPIRED, { message: 'Recipient signing window has expired', }); } };