mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-20 14:42:37 +02:00
@@ -3,13 +3,6 @@ import fs from 'node:fs';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { formatDateTime } from '../../../Utils.js';
|
||||
|
||||
const formatDateStr = (dateStr, DateFormat, timezone, Is12Hr) => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return dateStr;
|
||||
return formatDateTime(date, DateFormat, timezone, Is12Hr);
|
||||
};
|
||||
|
||||
export default async function GenerateCertificate(docDetails) {
|
||||
const timezone = docDetails?.ExtUserPtr?.Timezone || '';
|
||||
const Is12Hr = docDetails?.ExtUserPtr?.Is12HourTime || false;
|
||||
@@ -19,9 +12,7 @@ export default async function GenerateCertificate(docDetails) {
|
||||
const fontBytes = fs.readFileSync('./font/times.ttf'); //
|
||||
pdfDoc.registerFontkit(fontkit);
|
||||
const timesRomanFont = await pdfDoc.embedFont(fontBytes, { subset: true });
|
||||
const pngUrl = fs.readFileSync('./images/logo.png').buffer;
|
||||
const naSignUrl = fs.readFileSync('./images/na_sign.png').buffer;
|
||||
const nasign = await pdfDoc.embedPng(naSignUrl);
|
||||
const pngUrl = fs.readFileSync('./logo.png').buffer;
|
||||
const pngImage = await pdfDoc.embedPng(pngUrl);
|
||||
const page = pdfDoc.addPage();
|
||||
const { width, height } = page.getSize();
|
||||
@@ -38,7 +29,7 @@ export default async function GenerateCertificate(docDetails) {
|
||||
const textKeyColor = rgb(0.12, 0.12, 0.12);
|
||||
const textValueColor = rgb(0.3, 0.3, 0.3);
|
||||
const completedAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
|
||||
const completedAtperTimezone = formatDateStr(completedAt, DateFormat, timezone, Is12Hr);
|
||||
const completedAtperTimezone = formatDateTime(completedAt, DateFormat, timezone, Is12Hr);
|
||||
const completedUTCtime = completedAtperTimezone;
|
||||
const signersCount = docDetails?.Signers?.length || 1;
|
||||
const generateAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
|
||||
@@ -52,19 +43,9 @@ export default async function GenerateCertificate(docDetails) {
|
||||
const company = docDetails?.ExtUserPtr?.Company || '';
|
||||
const documentHash = docDetails?.DocumentHash || '';
|
||||
const createdAt = docDetails?.DocSentAt?.iso || docDetails.createdAt;
|
||||
const createdAtperTimezone = formatDateStr(createdAt, DateFormat, timezone, Is12Hr);
|
||||
const createdAtperTimezone = formatDateTime(createdAt, DateFormat, timezone, Is12Hr);
|
||||
const IsEnableOTP = docDetails?.IsEnableOTP || false;
|
||||
const placeholders = Array.isArray(docDetails?.Placeholders) ? docDetails.Placeholders : [];
|
||||
const filteredaudit = docDetails?.AuditTrail?.filter(x => {
|
||||
if (!x?.UserPtr?.objectId) return false;
|
||||
return true;
|
||||
});
|
||||
const toTs = v => {
|
||||
if (!v) return 0;
|
||||
if (typeof v === 'object' && v?.iso) return new Date(v.iso).getTime() || 0;
|
||||
const t = new Date(v).getTime();
|
||||
return Number.isFinite(t) ? t : 0;
|
||||
};
|
||||
const filteredaudit = docDetails?.AuditTrail?.filter(x => x?.UserPtr?.objectId);
|
||||
const auditTrail =
|
||||
docDetails?.Signers?.length > 0
|
||||
? filteredaudit?.map(x => {
|
||||
@@ -75,7 +56,6 @@ export default async function GenerateCertificate(docDetails) {
|
||||
SignedOn: x?.SignedOn || generatedUTCTime,
|
||||
ViewedOn: x?.ViewedOn || x?.SignedOn || generatedUTCTime,
|
||||
Signature: x?.Signature || '',
|
||||
_signedOnTs: toTs(x?.SignedOn),
|
||||
};
|
||||
})
|
||||
: [
|
||||
@@ -85,12 +65,8 @@ export default async function GenerateCertificate(docDetails) {
|
||||
SignedOn: filteredaudit[0]?.SignedOn || generatedUTCTime,
|
||||
ViewedOn: filteredaudit[0]?.ViewedOn || filteredaudit[0]?.SignedOn || generatedUTCTime,
|
||||
Signature: filteredaudit[0]?.Signature || '',
|
||||
_signedOnTs: toTs(filteredaudit[0]?.SignedOn),
|
||||
},
|
||||
];
|
||||
if (Array.isArray(auditTrail)) {
|
||||
auditTrail.sort((a, b) => (a?._signedOnTs || 0) - (b?._signedOnTs || 0));
|
||||
}
|
||||
|
||||
const ownerName = docDetails?.SenderName || docDetails.ExtUserPtr?.Name || 'n/a';
|
||||
const ownerEmail = docDetails?.SenderMail || docDetails.ExtUserPtr?.Email || 'n/a';
|
||||
@@ -327,63 +303,24 @@ export default async function GenerateCertificate(docDetails) {
|
||||
let yPosition7 = yPosition6 - 20;
|
||||
let yPosition8 = yPosition7 - 35;
|
||||
|
||||
// A signer/approver block spans from yPosition1 down to yPosition8 (the
|
||||
// separator line at the bottom of the block). The signature image's
|
||||
// bottom edge sits at yPosition7 - 30 and must remain inside the page
|
||||
// border (whose bottom edge is at startY). Use the lowest of those two
|
||||
// values when deciding whether the next block fits on the current page.
|
||||
const minY = startY + 5;
|
||||
const blockBottom = () => Math.min(yPosition7 - 30, yPosition8);
|
||||
|
||||
// Helper that resets the y-positions to the top of a freshly added page so
|
||||
// the next block starts cleanly under the border.
|
||||
const startNewPage = () => {
|
||||
const newPage = pdfDoc.addPage();
|
||||
newPage.drawRectangle({
|
||||
x: startX,
|
||||
y: startY,
|
||||
width: width - 2 * startX,
|
||||
height: height - 2 * startY,
|
||||
borderColor: borderColor,
|
||||
borderWidth: 1,
|
||||
});
|
||||
yPosition1 = newPage.getHeight() - 40;
|
||||
yPosition2 = yPosition1 - 20;
|
||||
yPosition3 = yPosition2 - 20;
|
||||
yPosition4 = yPosition3 - 20;
|
||||
yPosition5 = yPosition4 - 20;
|
||||
yPosition6 = yPosition5 - 20;
|
||||
yPosition7 = yPosition6 - 20;
|
||||
yPosition8 = yPosition7 - 35;
|
||||
return newPage;
|
||||
};
|
||||
|
||||
let currentPage = page;
|
||||
for (let i = 0; i < auditTrail.length; i++) {
|
||||
const x = auditTrail[i];
|
||||
// If the next block would overflow the bottom border, move to a new page.
|
||||
if (blockBottom() < minY) {
|
||||
currentPage = startNewPage();
|
||||
}
|
||||
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : nasign;
|
||||
const headerLabel = `${i + 1}. ${'Signer'}`;
|
||||
const signedOnLabel = 'Signed on :';
|
||||
|
||||
currentPage.drawText(headerLabel, {
|
||||
auditTrail.slice(0, 3).forEach(async (x, i) => {
|
||||
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : '';
|
||||
page.drawText(`Signer ${i + 1}`, {
|
||||
x: 30,
|
||||
y: yPosition1,
|
||||
size: subtitle,
|
||||
font: timesRomanFont,
|
||||
color: titleColor,
|
||||
});
|
||||
currentPage.drawText('Name :', {
|
||||
page.drawText('Name :', {
|
||||
x: 30,
|
||||
y: yPosition2,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(x?.Name || '', {
|
||||
|
||||
page.drawText(x?.Name, {
|
||||
x: 75,
|
||||
y: yPosition2,
|
||||
size: signertext,
|
||||
@@ -392,14 +329,14 @@ export default async function GenerateCertificate(docDetails) {
|
||||
});
|
||||
|
||||
if (IsEnableOTP) {
|
||||
currentPage.drawText('Security level :', {
|
||||
page.drawText('Security level :', {
|
||||
x: half + 120,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText('Email, OTP Auth', {
|
||||
page.drawText('Email, OTP Auth', {
|
||||
x: half + 190,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
@@ -408,14 +345,15 @@ export default async function GenerateCertificate(docDetails) {
|
||||
});
|
||||
}
|
||||
|
||||
currentPage.drawText('Email :', {
|
||||
page.drawText('Email :', {
|
||||
x: 30,
|
||||
y: yPosition3,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(x?.Email || '', {
|
||||
|
||||
page.drawText(x?.Email, {
|
||||
x: 75,
|
||||
y: yPosition3,
|
||||
size: signertext,
|
||||
@@ -423,14 +361,15 @@ export default async function GenerateCertificate(docDetails) {
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('Viewed on :', {
|
||||
page.drawText('Viewed on :', {
|
||||
x: 30,
|
||||
y: yPosition4,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(`${formatDateStr(x?.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
|
||||
page.drawText(`${formatDateTime(x.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: 97,
|
||||
y: yPosition4,
|
||||
size: signertext,
|
||||
@@ -438,30 +377,31 @@ export default async function GenerateCertificate(docDetails) {
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(signedOnLabel, {
|
||||
page.drawText('Signed on :', {
|
||||
x: 30,
|
||||
y: yPosition5,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
const signedOnValueX = 30 + timesRomanFont.widthOfTextAtSize(signedOnLabel, signertext) + 5;
|
||||
currentPage.drawText(`${formatDateStr(x?.SignedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: signedOnValueX,
|
||||
|
||||
page.drawText(`${formatDateTime(x.SignedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: 95,
|
||||
y: yPosition5,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('IP address :', {
|
||||
page.drawText('IP address :', {
|
||||
x: 30,
|
||||
y: yPosition6,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(x?.ipAddress || '', {
|
||||
|
||||
page.drawText(x?.ipAddress, {
|
||||
x: 95,
|
||||
y: yPosition6,
|
||||
size: signertext,
|
||||
@@ -469,14 +409,15 @@ export default async function GenerateCertificate(docDetails) {
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('Signature :', {
|
||||
page.drawText('Signature :', {
|
||||
x: 30,
|
||||
y: yPosition7,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawRectangle({
|
||||
|
||||
page.drawRectangle({
|
||||
x: 98,
|
||||
y: yPosition7 - 30,
|
||||
width: 104,
|
||||
@@ -485,14 +426,14 @@ export default async function GenerateCertificate(docDetails) {
|
||||
borderWidth: 1,
|
||||
});
|
||||
if (embedPng) {
|
||||
currentPage.drawImage(embedPng, {
|
||||
page.drawImage(embedPng, {
|
||||
x: 100,
|
||||
y: yPosition7 - 27,
|
||||
width: 100,
|
||||
height: 40,
|
||||
});
|
||||
}
|
||||
currentPage.drawLine({
|
||||
page.drawLine({
|
||||
start: { x: 30, y: yPosition8 },
|
||||
end: { x: width - 30, y: yPosition8 },
|
||||
color: rgb(0.12, 0.12, 0.12),
|
||||
@@ -507,6 +448,185 @@ export default async function GenerateCertificate(docDetails) {
|
||||
yPosition6 = yPosition5 - 20;
|
||||
yPosition7 = yPosition6 - 20;
|
||||
yPosition8 = yPosition8 - 174;
|
||||
});
|
||||
|
||||
if (auditTrail.length > 3) {
|
||||
let currentPageIndex = 1;
|
||||
let currentPage = page;
|
||||
auditTrail.slice(3).forEach(async (x, i) => {
|
||||
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : '';
|
||||
|
||||
// Calculate remaining space on current page
|
||||
const remainingSpace = yPosition8;
|
||||
|
||||
// If there's not enough space for the next entry, create a new page
|
||||
if (remainingSpace < 90) {
|
||||
// Adjust the value as needed
|
||||
currentPageIndex++;
|
||||
currentPage = pdfDoc.addPage();
|
||||
currentPage.drawRectangle({
|
||||
x: startX,
|
||||
y: startY,
|
||||
width: width - 2 * startX,
|
||||
height: height - 2 * startY,
|
||||
borderColor: borderColor,
|
||||
borderWidth: 1,
|
||||
});
|
||||
yPosition1 = currentPage.getHeight() - 40;
|
||||
yPosition2 = yPosition1 - 20;
|
||||
yPosition3 = yPosition2 - 20;
|
||||
yPosition4 = yPosition3 - 20;
|
||||
yPosition5 = yPosition4 - 20;
|
||||
yPosition5 = yPosition4 - 20;
|
||||
yPosition6 = yPosition5 - 20;
|
||||
yPosition7 = yPosition6 - 20;
|
||||
yPosition8 = currentPage.getHeight() - 190;
|
||||
}
|
||||
|
||||
currentPage.drawText(`Signer ${4 + i}`, {
|
||||
x: 30,
|
||||
y: yPosition1,
|
||||
size: subtitle,
|
||||
font: timesRomanFont,
|
||||
color: titleColor,
|
||||
});
|
||||
currentPage.drawText('Name :', {
|
||||
x: 30,
|
||||
y: yPosition2,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(x?.Name, {
|
||||
x: 75,
|
||||
y: yPosition2,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
if (IsEnableOTP) {
|
||||
currentPage.drawText('Security level :', {
|
||||
x: half + 120,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawText(`Email, OTP Auth`, {
|
||||
x: half + 190,
|
||||
y: yPosition2,
|
||||
size: timeText,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
}
|
||||
|
||||
currentPage.drawText('Email :', {
|
||||
x: 30,
|
||||
y: yPosition3,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(x?.Email, {
|
||||
x: 75,
|
||||
y: yPosition3,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('Viewed on :', {
|
||||
x: 30,
|
||||
y: yPosition4,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(`${formatDateTime(x.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: 97,
|
||||
y: yPosition4,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
currentPage.drawText('Signed on :', {
|
||||
x: 30,
|
||||
y: yPosition5,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(`${formatDateTime(x.SignedOn, DateFormat, timezone, Is12Hr)}`, {
|
||||
x: 95,
|
||||
y: yPosition5,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('IP address :', {
|
||||
x: 30,
|
||||
y: yPosition6,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
|
||||
currentPage.drawText(x?.ipAddress, {
|
||||
x: 100,
|
||||
y: yPosition6,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textValueColor,
|
||||
});
|
||||
|
||||
currentPage.drawText('Signature :', {
|
||||
x: 30,
|
||||
y: yPosition7,
|
||||
size: signertext,
|
||||
font: timesRomanFont,
|
||||
color: textKeyColor,
|
||||
});
|
||||
currentPage.drawRectangle({
|
||||
x: 98,
|
||||
y: yPosition7 - 27,
|
||||
width: 104,
|
||||
height: 44,
|
||||
borderColor: rgb(0.22, 0.18, 0.47),
|
||||
borderWidth: 1,
|
||||
});
|
||||
if (embedPng) {
|
||||
currentPage.drawImage(embedPng, {
|
||||
x: 100,
|
||||
y: yPosition7 - 25,
|
||||
width: 100,
|
||||
height: 40,
|
||||
});
|
||||
}
|
||||
|
||||
currentPage.drawLine({
|
||||
start: { x: 30, y: yPosition8 },
|
||||
end: { x: width - 30, y: yPosition8 },
|
||||
color: rgb(0.12, 0.12, 0.12),
|
||||
thickness: 0.5,
|
||||
});
|
||||
|
||||
// Update y positions for the next entry
|
||||
yPosition1 = yPosition8 - 20;
|
||||
yPosition2 = yPosition1 - 20;
|
||||
yPosition3 = yPosition2 - 20;
|
||||
yPosition4 = yPosition3 - 20;
|
||||
yPosition5 = yPosition4 - 20;
|
||||
yPosition6 = yPosition5 - 20;
|
||||
yPosition7 = yPosition6 - 20;
|
||||
yPosition8 = yPosition8 - 174;
|
||||
});
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
|
||||
@@ -17,22 +17,6 @@ import { SignPdf } from '@signpdf/signpdf';
|
||||
import { P12Signer } from '@signpdf/signer-p12';
|
||||
import { buildDownloadFilename, parseUploadFile } from '../../../utils/fileUtils.js';
|
||||
import sendMailWithAttachment from '../sendMailWithAttachment.js';
|
||||
import sendSystemMail from '../sendSystemMail.js';
|
||||
import {
|
||||
COMPLETION_ACTIVITIES,
|
||||
findPlaceholderIndex,
|
||||
findPendingPriorSigner,
|
||||
isCompletionRelevant,
|
||||
} from '../../../utils/workflowUtils.js';
|
||||
|
||||
// Audit-trail activities that count toward document completion. The free
|
||||
// build only counts 'Signed'; EE additionally counts 'Approved'.
|
||||
|
||||
// A placeholder participates in completion unless it is a prefill entry.
|
||||
// EE additionally excludes viewers (who never act on the document).
|
||||
|
||||
// Strict-order gating: returns the signerObjId of the prior placeholder
|
||||
// still pending, or null when the strict-order requirement is satisfied.
|
||||
|
||||
const serverUrl = cloudServerUrl; // process.env.SERVER_URL;
|
||||
const APPID = serverAppId;
|
||||
@@ -78,24 +62,13 @@ async function uploadFile(pdfName, filepath) {
|
||||
}
|
||||
|
||||
// `updateDoc` is used to update signedUrl, AuditTrail, Iscompleted in document
|
||||
async function updateDoc(
|
||||
docId,
|
||||
url,
|
||||
userId,
|
||||
ipAddress,
|
||||
data,
|
||||
className,
|
||||
sign,
|
||||
documentHash,
|
||||
activity
|
||||
) {
|
||||
async function updateDoc(docId, url, userId, ipAddress, data, className, sign, documentHash) {
|
||||
try {
|
||||
const UserPtr = { __type: 'Pointer', className: className, objectId: userId };
|
||||
const auditActivity = 'Signed';
|
||||
const obj = {
|
||||
UserPtr: UserPtr,
|
||||
SignedUrl: url,
|
||||
Activity: auditActivity,
|
||||
Activity: 'Signed',
|
||||
ipAddress: ipAddress,
|
||||
SignedOn: new Date(),
|
||||
Signature: sign,
|
||||
@@ -115,14 +88,13 @@ async function updateDoc(
|
||||
updateAuditTrail = [obj];
|
||||
}
|
||||
|
||||
// Count both Signed and Approved entries; only signer/approver
|
||||
// placeholders count toward completion (viewers and prefill excluded).
|
||||
const auditTrail = updateAuditTrail.filter(x => COMPLETION_ACTIVITIES.includes(x.Activity));
|
||||
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
|
||||
let isCompleted = false;
|
||||
if (data.Signers && data.Signers.length > 0) {
|
||||
const completionRelevant =
|
||||
data.Placeholders?.length > 0 ? data.Placeholders.filter(isCompletionRelevant) : [];
|
||||
if (auditTrail.length >= completionRelevant.length && completionRelevant.length > 0) {
|
||||
//'removePrefill' is used to remove prefill role from placeholders filed then compare length to change status of document
|
||||
const removePrefill =
|
||||
data.Placeholders.length > 0 && data.Placeholders.filter(x => x.Role !== 'prefill');
|
||||
if (auditTrail.length === removePrefill?.length) {
|
||||
isCompleted = true;
|
||||
}
|
||||
} else {
|
||||
@@ -152,11 +124,10 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
|
||||
const logo =
|
||||
"<img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' style='padding:20px'/>";
|
||||
|
||||
const auditTrailCount =
|
||||
doc?.AuditTrail?.filter(x => COMPLETION_ACTIVITIES.includes(x.Activity))?.length || 0;
|
||||
const completionRelevant =
|
||||
doc?.Placeholders?.length > 0 ? doc.Placeholders.filter(isCompletionRelevant) : [];
|
||||
const signersCount = completionRelevant?.length;
|
||||
const auditTrailCount = doc?.AuditTrail?.filter(x => x.Activity === 'Signed')?.length || 0;
|
||||
const removePrefill =
|
||||
doc?.Placeholders?.length > 0 && doc?.Placeholders?.filter(x => x?.Role !== 'prefill');
|
||||
const signersCount = removePrefill?.length;
|
||||
const remainingSign = signersCount - auditTrailCount;
|
||||
if (remainingSign > 1 && doc?.NotifyOnSignatures) {
|
||||
const sender = doc.ExtUserPtr;
|
||||
@@ -183,7 +154,7 @@ async function sendNotifyMail(doc, signUser, mailProvider, publicUrl) {
|
||||
html: body,
|
||||
mailProvider: mailProvider,
|
||||
};
|
||||
await sendSystemMail({ params });
|
||||
await axios.post(serverUrl + '/functions/sendmailv3', params, { headers });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err in sendnotifymail', err);
|
||||
@@ -268,7 +239,6 @@ async function sendCompletedMail(obj) {
|
||||
body = replaceVar.body;
|
||||
}
|
||||
const Bcc = doc?.Bcc?.length > 0 ? doc.Bcc.map(x => x.Email) : [];
|
||||
const Cc = doc?.Cc?.length > 0 ? doc.Cc.map(x => x.Email) : [];
|
||||
const updatedBcc = doc?.SenderMail ? [...Bcc, doc?.SenderMail] : Bcc;
|
||||
const formatId = doc?.ExtUserPtr?.DownloadFilenameFormat;
|
||||
const filename = pdfName?.length > 100 ? pdfName?.slice(0, 100) : pdfName;
|
||||
@@ -288,7 +258,6 @@ async function sendCompletedMail(obj) {
|
||||
html: body,
|
||||
mailProvider: obj.mailProvider,
|
||||
bcc: updatedBcc?.length > 0 ? updatedBcc : '',
|
||||
cc: Cc?.length > 0 ? Cc : '',
|
||||
certificatePath: `./exports/signed_certificate_${doc.objectId}.pdf`,
|
||||
filename: docName,
|
||||
};
|
||||
@@ -391,14 +360,11 @@ async function PDF(req) {
|
||||
const isCustomMail = req.params.isCustomCompletionMail || false;
|
||||
const mailProvider = req.params.mailProvider || '';
|
||||
const sign = req.params.signature || '';
|
||||
const auditActivity = 'Signed';
|
||||
const publicUrl = req.headers.public_url;
|
||||
// below bode is used to get info of docId
|
||||
const docQuery = new Parse.Query('contracts_Document');
|
||||
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc,Cc,CreatedBy');
|
||||
docQuery.include('ExtUserPtr,Signers,ExtUserPtr.TenantId,Bcc,CreatedBy');
|
||||
docQuery.equalTo('objectId', docId);
|
||||
docQuery.notEqualTo('IsDeclined', true);
|
||||
docQuery.notEqualTo('IsArchive', true);
|
||||
const resDoc = await docQuery.first({ useMasterKey: true });
|
||||
if (!resDoc) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Document not found.');
|
||||
@@ -425,27 +391,7 @@ async function PDF(req) {
|
||||
className = 'contracts_Users';
|
||||
signUser = _resDoc.ExtUserPtr;
|
||||
}
|
||||
// Strict-order gating: when both `SendinOrder` and `SendInOrderStrict`
|
||||
// are enabled the document creator wants the signing flow locked to a
|
||||
// strict sequence — a signer/approver may only act once every previous
|
||||
// signer/approver placeholder has a Signed/Approved audit entry. We
|
||||
// skip this check entirely for the document owner (className=Users)
|
||||
// because owners never sign through this path.
|
||||
if (reqUserId && _resDoc?.SendinOrder === true && _resDoc?.SendInOrderStrict === true) {
|
||||
const placeholders = Array.isArray(_resDoc?.Placeholders)
|
||||
? _resDoc.Placeholders.filter(p => p?.Role !== 'prefill')
|
||||
: [];
|
||||
const myIdx = findPlaceholderIndex(placeholders, reqUserId);
|
||||
if (myIdx > 0) {
|
||||
const pendingId = findPendingPriorSigner(placeholders, myIdx, _resDoc?.AuditTrail);
|
||||
if (pendingId) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Strict signing order is enabled — please wait for the previous signers to complete their action before signing.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const username = signUser.Name;
|
||||
const userEmail = signUser.Email;
|
||||
if (req.params.pdfFile) {
|
||||
@@ -462,7 +408,7 @@ async function PDF(req) {
|
||||
const P12Buffer = Buffer.from(pfxFile, 'base64');
|
||||
fs.writeFileSync(pfxname, P12Buffer);
|
||||
const UserPtr = { __type: 'Pointer', className: className, objectId: signUser.objectId };
|
||||
const obj = { UserPtr: UserPtr, SignedUrl: '', Activity: auditActivity, ipAddress: userIP };
|
||||
const obj = { UserPtr: UserPtr, SignedUrl: '', Activity: 'Signed', ipAddress: userIP };
|
||||
let updateAuditTrail;
|
||||
if (_resDoc.AuditTrail && _resDoc.AuditTrail.length > 0) {
|
||||
updateAuditTrail = [..._resDoc.AuditTrail, obj];
|
||||
@@ -470,17 +416,14 @@ async function PDF(req) {
|
||||
updateAuditTrail = [obj];
|
||||
}
|
||||
|
||||
// Both Signed and Approved entries count toward completion. Only
|
||||
// signer/approver placeholders are counted; viewers and prefill are
|
||||
// excluded.
|
||||
const auditTrail = updateAuditTrail.filter(x => COMPLETION_ACTIVITIES.includes(x.Activity));
|
||||
const auditTrail = updateAuditTrail.filter(x => x.Activity === 'Signed');
|
||||
let isCompleted = false;
|
||||
if (_resDoc.Signers && _resDoc.Signers.length > 0) {
|
||||
const completionRelevant =
|
||||
_resDoc?.Placeholders?.length > 0
|
||||
? _resDoc.Placeholders.filter(isCompletionRelevant)
|
||||
: [];
|
||||
if (auditTrail.length >= completionRelevant.length && completionRelevant.length > 0) {
|
||||
const removePrefill =
|
||||
_resDoc?.Placeholders?.length > 0 &&
|
||||
_resDoc?.Placeholders?.filter(x => x?.Role !== 'prefill');
|
||||
if (auditTrail.length === removePrefill?.length) {
|
||||
// if (auditTrail.length === _resDoc.Signers.length) {
|
||||
isCompleted = true;
|
||||
}
|
||||
} else {
|
||||
@@ -533,8 +476,7 @@ async function PDF(req) {
|
||||
_resDoc, // auditTrail, signers, etc data
|
||||
className, // className based on flow
|
||||
sign, // sign base64
|
||||
isCompleted ? documentHash : undefined,
|
||||
auditActivity
|
||||
isCompleted ? documentHash : undefined
|
||||
);
|
||||
sendNotifyMail(_resDoc, signUser, mailProvider, publicUrl);
|
||||
saveFileUsage(pdfSize, data.imageUrl, _resDoc?.CreatedBy?.objectId);
|
||||
|
||||
Reference in New Issue
Block a user