fix: handle completion when already signed (#3159)

Previously attempting to complete a document which is already completed
you'd get a generic error toast. Now when completing a document that you
have already completed you are redirected to the completed page.

Handles cases where two mutations managed to fire racing eachother.
This commit is contained in:
Lucas Smith
2026-08-10 09:25:41 +10:00
committed by GitHub
parent d6cf3fec4b
commit fc95ee9ead
5 changed files with 77 additions and 18 deletions
@@ -80,22 +80,29 @@ export const completeDocumentWithToken = async ({
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
if (envelope.status !== DocumentStatus.PENDING) {
throw new Error(`Document ${envelope.id} must be pending`);
}
if (envelope.recipients.length === 0) {
throw new Error(`Document ${envelope.id} has no recipient with token ${token}`);
}
const [recipient] = envelope.recipients;
assertRecipientNotExpired(recipient);
// A retried or duplicate completion request for an already signed
// recipient throws a code the router resolves idempotently. This must be
// checked before the envelope status guard since the envelope may have
// been completed and sealed by the recipient's original request.
if (recipient.signingStatus === SigningStatus.SIGNED) {
throw new Error(`Recipient ${recipient.id} has already signed`);
throw new AppError(AppErrorCode.RECIPIENT_ALREADY_SIGNED, {
message: `Recipient ${recipient.id} has already signed`,
statusCode: 400,
});
}
if (envelope.status !== DocumentStatus.PENDING) {
throw new Error(`Document ${envelope.id} must be pending`);
}
assertRecipientNotExpired(recipient);
if (recipient.signingStatus === SigningStatus.REJECTED) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
message: 'Recipient has already rejected the document',
@@ -276,9 +283,15 @@ export const completeDocumentWithToken = async ({
}
await prisma.$transaction(async (tx) => {
await tx.recipient.update({
// Conditional update so two concurrent completion requests can't both
// proceed: only the request that transitions the recipient to SIGNED
// continues, the loser sees a count of 0 and aborts.
const { count: updatedRecipientCount } = await tx.recipient.updateMany({
where: {
id: recipient.id,
signingStatus: {
not: SigningStatus.SIGNED,
},
},
data: {
signingStatus: SigningStatus.SIGNED,
@@ -288,6 +301,16 @@ export const completeDocumentWithToken = async ({
},
});
// A concurrent request completed the recipient between our initial read
// and this transaction. Abort so the winning request handles all side
// effects, the router resolves this code idempotently.
if (updatedRecipientCount === 0) {
throw new AppError(AppErrorCode.RECIPIENT_ALREADY_SIGNED, {
message: `Recipient ${recipient.id} has already signed`,
statusCode: 400,
});
}
if (recipientEmail !== recipient.email || recipientName !== recipient.name) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({