one-span-sign
Version:
Node module for the One Span E-Sign API.
202 lines (180 loc) • 6.25 kB
JavaScript
const _ = require('lodash');
const { alwaysArray, splitOrArray } = require('../util/object');
const { InvalidSignerError, InvalidDocumentError, InvalidPackageError } = require('../util/error');
const PACKAGE_TEMPLATE = () => _.cloneDeep(require('../templates/package.json'));
const DOCUMENT_TEMPLATE = () => _.cloneDeep(require('../templates/document.json'));
const ROLE_TEMPLATE = () => _.cloneDeep(require('../templates/role.json'));
function fieldReducer(ar, template) {
return _.reduce(ar, (sum, field) => {
sum[field.role] = sum[field.role] || []; // eslint-disable-line no-param-reassign
sum[field.role].push(_.merge({}, template, { name: field.name }, field.template));
return sum;
}, {});
}
function mergeReductions(...args) {
return _.reduce(args, (sum, rolesToFields) => {
_.each(rolesToFields, (fields, role) => {
const existing = _.find(sum, { role });
if (existing) {
existing.fields = existing.fields.concat(fields);
} else {
sum.push({
role,
fields
});
}
});
return sum;
}, []);
}
// Not the most efficient, but this issue was introduced later via https://developer.esignlive.com/forums/topic/problems-with-multiple-signature-and-initial-fields/#post-4173
function splitApprovals(ar) {
return _.chain(ar)
.map((approval) =>
_.reduce(approval.fields, (sum, field) => {
const lastApproval = _.last(sum);
// Either we do not have a lastApproval or there is already a signature in the one that exists
if (!lastApproval || (field.type === 'SIGNATURE' && _.find(lastApproval.fields, { type: 'SIGNATURE' }))) {
sum.push({
role: approval.role,
fields: [field]
});
} else {
lastApproval.fields.push(field);
}
return sum;
}, [])
)
.flatten()
.value();
}
function docApprovals({ initialFields, dateFields, signatureFields } = {}) {
return splitApprovals(
mergeReductions(
fieldReducer(initialFields, {
type: 'SIGNATURE',
extract: true,
subtype: 'INITIALS',
required: true,
name: undefined
}),
fieldReducer(dateFields, {
type: 'INPUT',
extract: true,
subtype: 'LABEL',
required: true,
binding: '{approval.signed}'
}),
fieldReducer(signatureFields, {
type: 'SIGNATURE',
extract: true,
subtype: 'FULLNAME',
required: true,
name: undefined
})
));
}
// This ensures that if an optional role was not filled, their approvals are not added
function clearApprovalsMissingRoles(pkg) {
const indexedRoles = _.groupBy(pkg.roles, _.property('id'));
pkg.documents = _.map(pkg.documents, (doc) => { // eslint-disable-line no-param-reassign
doc.approvals = _.filter(doc.approvals, (approval) => indexedRoles[approval.role]); // eslint-disable-line no-param-reassign
return doc;
});
return pkg;
}
// NOTE - Builder is a legacy pattern for now, we remove this in a future iteration
class Builder {
constructor() {
this.steps = {};
this.payload = PACKAGE_TEMPLATE();
}
package(name, description, status) {
_.merge(this.payload, {
name,
description,
status
});
this.steps.package = true;
return this;
}
roles(customerFirstName, customerLastName, customerEmail, identifier, sendEmail = true, optional = false) {
if (customerFirstName && customerLastName && customerEmail) {
const role = ROLE_TEMPLATE();
_.merge(role.signers[0], {
firstName: customerFirstName || '',
lastName: customerLastName || '',
email: _.last(splitOrArray(customerEmail)),
id: identifier,
delivery: {
email: sendEmail,
provider: sendEmail,
download: sendEmail
}
});
role.name = identifier;
role.id = identifier;
// We set a custom email message for dealers only
role.emailMessage.content = 'This is email message content for this role.';
this.payload.roles.push(role);
this.steps.roles = true;
} else if (!optional) {
throw new InvalidPackageError('First name, last name and email are required arguments for a new package.');
}
return this;
}
documents(docs) {
this.payload.documents = _.map(alwaysArray(docs), (dn) => {
const doc = DOCUMENT_TEMPLATE();
doc.name = dn.name;
doc.approvals = docApprovals(dn);
return doc;
});
this.steps.documents = true;
return this;
}
build() {
if (this.steps.package && this.steps.roles && this.steps.documents) {
return clearApprovalsMissingRoles(this.payload);
} else {
throw new InvalidPackageError('You have skipped a package, roles, documents, or approval step in building your package.');
}
}
}
function isPackageValid({ name, documents, signers }) {
if (_.isEmpty(name)) {
throw new InvalidPackageError('A name is required for a new package.');
}
if (_.isEmpty(documents)) {
throw new InvalidPackageError('Documents are required for a new package.');
}
const invalidDoc = _.find(documents, (doc) => !doc.hasRequiredProperties());
if (invalidDoc) {
throw new InvalidDocumentError(`Document missing required properties: ${_.join(invalidDoc.missingProperties(), ', ')}.`);
}
if (_.isEmpty(signers)) {
throw new InvalidPackageError('Signers are required for a new package.');
}
const invalidSigner = _.find(signers, (signer) => !signer.hasRequiredProperties());
if (invalidSigner) {
throw new InvalidSignerError(`Signer missing required properties: ${_.join(invalidSigner.missingProperties(), ', ')}.`);
}
return true;
}
function assemblePackage({ name, description, documents, signers, status }) {
if (isPackageValid({
name,
description,
documents,
signers
})) {
let builder = new Builder()
.package(name, description || name, status)
.documents(documents);
_.each(signers, (signer) => {
builder = builder.roles(signer.firstName, signer.lastName, signer.email, signer.role);
});
return builder.build()
}
}
module.exports = assemblePackage;