feat: dictate next signers in signing ordeR

This commit is contained in:
Ephraim Atta-Duncan
2025-02-14 10:32:18 +00:00
parent 2ff330f9d4
commit 4189a34de0
15 changed files with 985 additions and 150 deletions

View File

@ -28,6 +28,7 @@ export type CreateDocumentMetaOptions = {
distributionMethod?: DocumentDistributionMethod;
typedSignatureEnabled?: boolean;
language?: SupportedLanguageCodes;
modifyNextSigner?: boolean;
requestMetadata: ApiRequestMetadata;
};
@ -46,6 +47,7 @@ export const upsertDocumentMeta = async ({
distributionMethod,
typedSignatureEnabled,
language,
modifyNextSigner,
requestMetadata,
}: CreateDocumentMetaOptions) => {
const document = await prisma.document.findFirst({
@ -98,6 +100,7 @@ export const upsertDocumentMeta = async ({
distributionMethod,
typedSignatureEnabled,
language,
modifyNextSigner,
},
update: {
subject,
@ -111,6 +114,7 @@ export const upsertDocumentMeta = async ({
distributionMethod,
typedSignatureEnabled,
language,
modifyNextSigner,
},
});

View File

@ -28,6 +28,10 @@ export type CompleteDocumentWithTokenOptions = {
userId?: number;
authOptions?: TRecipientActionAuth;
requestMetadata?: RequestMetadata;
nextSigner?: {
email: string;
name: string;
};
};
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
@ -51,11 +55,56 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
});
};
export const delegateNextSigner = async ({
documentId,
currentRecipientId,
nextSigner,
}: {
documentId: number;
currentRecipientId: number;
nextSigner: { email: string; name: string };
}) => {
const document = await prisma.document.findUnique({
where: { id: documentId },
include: {
recipients: {
orderBy: [{ signingOrder: 'asc' }, { id: 'asc' }],
},
},
});
if (!document) {
throw new Error('Document not found');
}
const currentRecipient = document.recipients.find((r) => r.id === currentRecipientId);
const nextRecipient = document.recipients.find(
(r) => r.signingOrder === (currentRecipient?.signingOrder ?? 0) + 1,
);
if (!nextRecipient) {
throw new Error('Next recipient not found');
}
await prisma.recipient.update({
where: { id: nextRecipient.id },
data: {
email: nextSigner.email,
name: nextSigner.name,
},
});
return nextRecipient;
};
export const completeDocumentWithToken = async ({
token,
documentId,
requestMetadata,
nextSigner,
}: CompleteDocumentWithTokenOptions) => {
console.log('completeDocumentWithToken == document-router', token, documentId, nextSigner);
const document = await getDocument({ token, documentId });
if (document.status !== DocumentStatus.PENDING) {
@ -112,6 +161,20 @@ export const completeDocumentWithToken = async ({
// throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values');
// }
if (
nextSigner &&
document.documentMeta?.modifyNextSigner &&
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL
) {
console.log('delegateNextSigner == document-router', document.id, recipient.id, nextSigner);
await delegateNextSigner({
documentId: document.id,
currentRecipientId: recipient.id,
nextSigner,
});
}
await prisma.$transaction(async (tx) => {
await tx.recipient.update({
where: {

View File

@ -55,6 +55,7 @@ export const ZDocumentSchema = DocumentSchema.pick({
typedSignatureEnabled: true,
language: true,
emailSettings: true,
modifyNextSigner: true,
}).nullable(),
recipients: ZRecipientLiteSchema.array(),
fields: ZFieldSchema.array(),

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "DocumentMeta" ADD COLUMN "modifyNextSigner" BOOLEAN NOT NULL DEFAULT false;

View File

@ -390,6 +390,7 @@ model DocumentMeta {
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
redirectUrl String?
signingOrder DocumentSigningOrder @default(PARALLEL)
modifyNextSigner Boolean @default(false)
typedSignatureEnabled Boolean @default(true)
language String @default("en")
distributionMethod DocumentDistributionMethod @default(EMAIL)

View File

@ -266,15 +266,14 @@ export const documentRouter = router({
/**
* @public
*
* Todo: Refactor to updateDocument.
*/
setSettingsForDocument: authenticatedProcedure
updateDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/update',
summary: 'Update document',
description: 'Update an existing document',
tags: ['Document'],
},
})
@ -286,9 +285,9 @@ export const documentRouter = router({
const userId = ctx.user.id;
if (Object.values(meta).length > 0) {
if (Object.keys(meta).length > 0) {
await upsertDocumentMeta({
userId: ctx.user.id,
userId,
teamId,
documentId,
subject: meta.subject,
@ -301,6 +300,7 @@ export const documentRouter = router({
distributionMethod: meta.distributionMethod,
signingOrder: meta.signingOrder,
emailSettings: meta.emailSettings,
modifyNextSigner: meta.modifyNextSigner,
requestMetadata: ctx.metadata,
});
}

View File

@ -251,6 +251,7 @@ export const ZUpdateDocumentRequestSchema = z.object({
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
modifyNextSigner: z.boolean().optional(),
})
.optional(),
});

View File

@ -437,13 +437,22 @@ export const recipientRouter = router({
completeDocumentWithToken: procedure
.input(ZCompleteDocumentWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
const { token, documentId, authOptions } = input;
const { token, documentId, authOptions, nextSigner } = input;
console.log(
'completeDocumentWithToken == recipient-router',
token,
documentId,
authOptions,
nextSigner,
);
return await completeDocumentWithToken({
token,
documentId,
authOptions,
userId: ctx.user?.id,
nextSigner,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),

View File

@ -212,6 +212,12 @@ export const ZCompleteDocumentWithTokenMutationSchema = z.object({
token: z.string(),
documentId: z.number(),
authOptions: ZRecipientActionAuthSchema.optional(),
nextSigner: z
.object({
email: z.string().email(),
name: z.string(),
})
.optional(),
});
export type TCompleteDocumentWithTokenMutationSchema = z.infer<

View File

@ -49,6 +49,7 @@ export type AddSignersFormProps = {
recipients: Recipient[];
fields: Field[];
signingOrder?: DocumentSigningOrder | null;
modifyNextSigner?: boolean | null;
isDocumentEnterprise: boolean;
onSubmit: (_data: TAddSignersFormSchema) => void;
isDocumentPdfLoaded: boolean;
@ -59,6 +60,7 @@ export const AddSignersFormPartial = ({
recipients,
fields,
signingOrder,
modifyNextSigner,
isDocumentEnterprise,
onSubmit,
isDocumentPdfLoaded,
@ -107,6 +109,7 @@ export const AddSignersFormPartial = ({
)
: defaultRecipients,
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
modifyNextSigner: modifyNextSigner ?? false,
},
});
@ -404,6 +407,35 @@ export const AddSignersFormPartial = ({
</FormItem>
)}
/>
{isSigningOrderSequential && (
<FormField
control={form.control}
name="modifyNextSigner"
render={({ field }) => (
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
<FormControl>
<Checkbox
id="modifyNextSigner"
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
disabled={isSubmitting || hasDocumentBeenSent}
/>
</FormControl>
<FormLabel
htmlFor="modifyNextSigner"
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<Trans>Modify next signer</Trans>
</FormLabel>
</FormItem>
)}
/>
)}
<DragDropContext
onDragEnd={onDragEnd}
sensors={[

View File

@ -25,6 +25,7 @@ export const ZAddSignersFormSchema = z
}),
),
signingOrder: z.nativeEnum(DocumentSigningOrder),
modifyNextSigner: z.boolean(),
})
.refine(
(schema) => {