Reference Source Test

lib/template/index.js

'use strict';
const https = require('https');
const { readFileSync } = require('fs');
const { basename } = require('path');
const {
  responseHandler,
  errorHandler,
  buildRequestOptions,
  isCSVFile,
  getFormDataBoundary,
} = require('../common');
const {
  remove,
  invite,
  view,
} = require('../document');
const { promisify } = require('../../utils');

/**
 * @type {function}
 * @param {DocumentRemoveParams} data - remove document payload
 * @param {function(err: ApiErrorResponse, res: DocumentRemoveResponse)} [callback] - error first node.js callback
 */
const removeDocument = remove;

/**
 * @type {function}
 * @param {DocumentInviteParams} data - create document invite payload
 * @param {function(err: ApiErrorResponse, res: DocumentInviteResponse)} [callback] - error first node.js callback
 */
const sendDocumentInvite = invite;

/**
 * @type {function}
 * @param {DocumentViewParams} data - view document with field extract payload
 * @param {function(err: ApiErrorResponse, res: DocumentViewResponse)} [callback] - error first node.js callback
 */
const getDocumentDetails = view;

/**
 * Update Signing Role payload
 * @typedef {Object} TemplateUpdateSigningRoleParams
 * @property {string} id - an ID of Actor (role)
 * @property {string} token - your auth token
 * @property {number} signing_order - signing order number
 */

/**
 * Update Signing Role response data
 * @typedef {Object} TemplateUpdateSigningRoleResponse
 * @property {string} id - unique ID of Actor
 * @property {string} document_id - unique ID of Template
 * @property {number} signing_order - signing order number
 */

/**
 * Updates Signing Role with order of actor
 * @param {TemplateUpdateSigningRoleParams} data - update signing role payload
 * @param {function(err: ApiErrorResponse, res: TemplateUpdateSigningRoleResponse)} [callback] - error first node.js callback
 */
const updateSigningRole = ({
  id,
  token,
  signing_order,
}, callback) => {
  const JSONData = JSON.stringify({ signing_order });
  const req = https
    .request(buildRequestOptions({
      method: 'PUT',
      path: `/role/${id}`,
      authorization: {
        type: 'Bearer',
        token,
      },
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(JSONData),
      },
    }), responseHandler(callback))
    .on('error', errorHandler(callback));

  req.write(JSONData);
  req.end();
};

/**
 * Template methods
 */
class Template {

  /**
   * Document invite optional settings
   * @typedef {Object} TemplateCreateOptions
   * @property {boolean} [removeOriginalDocument=false] - if true original document will be removed
   */

  /**
   * Create template payload
   * @typedef {Object} TemplateCreateParams
   * @property {string} document_id - an id of original document
   * @property {string} document_name - a name of new template
   * @property {TemplateCreateOptions} options - create template optional settings
   * @property {string} token - your auth token
   */

  /**
   * Create template response data
   * @typedef {Object} TemplateCreateResponse
   * @property {string} id - an id of created template
   */

  /**
   * Creates a template by copying an existing unfulfilled and not requested for sign document.
   * By default original document is not removed after template creation. To remove original document set `removeOriginalDocument` option to `true`.
   * @param {TemplateCreateParams} data - create template payload
   * @param {function(err: ApiErrorResponse, res: TemplateCreateResponse)} [originalCallback] - error first node.js callback
   */
  static create ({
    document_id,
    document_name,
    options: { removeOriginalDocument = false } = {},
    token,
  }, originalCallback) {
    const JSONData = JSON.stringify({
      document_id,
      document_name,
    });

    const callbackWithDocumentDeletion = (createErr, createRes) => {
      if (createErr) {
        originalCallback(createErr);
        return;
      } else {
        removeDocument({
          id: document_id,
          token,
        }, removeErr => {
          if (removeErr) {
            originalCallback(removeErr);
            return;
          } else {
            originalCallback(null, createRes);
            return;
          }
        });
      }
    };

    const callback = removeOriginalDocument
      ? callbackWithDocumentDeletion
      : originalCallback;

    const req = https
      .request(buildRequestOptions({
        method: 'POST',
        path: '/template',
        authorization: {
          type: 'Bearer',
          token,
        },
        headers: {
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(JSONData),
        },
      }), responseHandler(callback))
      .on('error', errorHandler(originalCallback));

    req.write(JSONData);
    req.end();
  }

  /**
   * Copy template payload
   * @typedef {Object} TemplateDuplicateParams
   * @property {string} id - an id of original template
   * @property {string} name - a name of new document
   * @property {string} token - your auth token
   */

  /**
   * Copy template response data
   * @typedef {Object} TemplateDuplicateResponse
   * @property {string} id - an id of created document
   * @property {string} name - a name of created document
   */

  /**
   * Creates a new document by copying a template.
   * If a name is not supplied then document name will be set to the original template's name by default.
   * @param {TemplateDuplicateParams} data - duplicate template payload
   * @param {function(err: ApiErrorResponse, res: TemplateDuplicateResponse)} [callback] - error first node.js callback
   */
  static duplicate ({
    id: template_id,
    name: document_name,
    token,
  }, callback) {
    const JSONData = JSON.stringify({
      template_id,
      document_name,
    });

    const req = https
      .request(buildRequestOptions({
        method: 'POST',
        path: `/template/${template_id}/copy`,
        authorization: {
          type: 'Bearer',
          token,
        },
        headers: {
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(JSONData),
        },
      }), responseHandler(callback))
      .on('error', errorHandler(callback));

    req.write(JSONData);
    req.end();
  }

  /**
   * Create invite with template payload
   * @typedef {DocumentInviteParams} TemplateInviteParams
   */

  /**
   * Create invite with template response data
   * @typedef {DocumentInviteResponse} TemplateInviteResponse
   */

  /**
   * Creates an invite to sign a template.
   * You can create a simple free form invite(s) or a role-based invite(s).
   * The method copies a template into document and creates an invite using created document.
   * @param {TemplateInviteParams} data - create template invite payload
   * @param {function(err: ApiErrorResponse, res: TemplateInviteResponse)} [callback] - error first node.js callback
   */
  static invite ({
    id,
    data,
    options,
    token,
  }, callback) {
    Template.duplicate({
      id,
      token,
    }, (copyErr, copyRes) => {
      if (copyErr) {
        callback(copyErr);
        return;
      } else if (Array.isArray(data.to)) {
        const { id: documentId } = copyRes;

        getDocumentDetails({
          id: documentId,
          token,
        }, (detailErr, detailRes) => {
          if (detailErr) {
            callback(detailErr);
            return;
          } else {
            const inviteData = Object.assign({}, data, { document_id: documentId });
            const { roles } = detailRes;

            for (const signer of inviteData.to) {
              try {
                signer.role_id = roles.find(role => role.name === signer.role).unique_id;
              } catch (err) {
                callback(err);
                return;
              }
            }

            sendDocumentInvite({
              id: documentId,
              data: inviteData,
              options,
              token,
            }, callback);
          }
        });
      } else {
        const { id: documentId } = copyRes;
        const inviteData = Object.assign({}, data, { document_id: documentId });

        sendDocumentInvite({
          id: documentId,
          data: inviteData,
          options,
          token,
        }, callback);
      }
    });
  }

  /**
   * Remove template payload
   * @typedef {DocumentRemoveParams} TemplateRemoveParams
   */

  /**
   * Remove template response data
   * @typedef {DocumentRemoveResponse} TemplateRemoveResponse
   */

  /**
   * Removes a specified template from folder
   * @param {TemplateRemoveParams} data - remove template payload
   * @param {function(err: ApiErrorResponse, res: TemplateRemoveResponse)} [callback] - error first node.js callback
   */
  static remove ({
    id,
    token,
    options,
  }, callback) {
    removeDocument({
      id,
      token,
      options,
    }, callback);
  }

  /**
   * Get Template Routing Details payload
   * @typedef {Object} TemplateGetRoutingDetailsParams
   * @property {string} id - an ID of specific Template
   * @property {string} token - your auth token
   */

  /**
   * Template Routing Details item data
   * @typedef {Object} TemplateRoutingDetailsItem
   * @property {string} default_email - default email for routing detail
   * @property {boolean} inviter_role - always `false`
   * @property {string} name - signer role (actor) name
   * @property {string} role_id - signer role (actor) unique ID
   * @property {number} signing_order - order number of signing step
   */

  /**
   * Template Routing Details CC step info
   * @typedef {Object} TemplateRoutingDetailsCCStep
   * @property {string} email - email of cc step
   * @property {number} step - step order number
   * @property {string} name - name of cc step
   */

  /**
   * Get Template Routing Details response data
   * @typedef {Object} TemplateGetRoutingDetailsResponse
   * @property {TemplateRoutingDetailsItem[]} routing_details - array with routing details
   * @property {string[]} cc - CC emails
   * @property {TemplateRoutingDetailsCCStep[]} cc_step - array of CC`s steps
   * @property {string} invite_link_instructions - invite link instructions
   */

  /**
   * Retrieves Routing Details of specified Template
   * @param {TemplateGetRoutingDetailsParams} data - get Routing Details payload
   * @param {function(err: ApiErrorResponse, res: TemplateGetRoutingDetailsResponse)} [callback] - error first node.js callback
   */
  static getRoutingDetails ({
    id,
    token,
  }, callback) {
    https
      .request(buildRequestOptions({
        method: 'GET',
        path: `/document/${id}/template/routing/detail`,
        authorization: {
          type: 'Bearer',
          token,
        },
      }), responseHandler(callback))
      .on('error', errorHandler(callback))
      .end();
  }

  /**
   * Template Update Routing Details item data
   * @typedef {Object} TemplateUpdateRoutingDetailsItem
   * @property {string} default_email - default email for routing detail
   * @property {boolean} inviter_role - always `false`
   * @property {string} name - signer role (actor) name
   * @property {string} role_id - signer role (actor) unique ID
   * @property {number} signing_order - order number of signing step
   * @property {boolean} decline_by_signature - decline by signature flag
   */

  /**
   * Update Template Routing Details settings
   * @typedef {Object} TemplateUpdateRoutingDetailsSettings
   * @property {TemplateUpdateRoutingDetailsItem[]} template_data - array with routing details
   * @property {string[]} cc - CC emails
   * @property {TemplateRoutingDetailsCCStep[]} cc_step - array of CC steps
   * @property {string} [invite_link_instructions] - invite signing instructions
   */

  /**
   * Update Template Routing Details payload
   * @typedef {Object} TemplateUpdateRoutingDetailsParams
   * @property {string} id - an ID of specific Template
   * @property {string} token - your auth token
   * @property {TemplateUpdateRoutingDetailsSettings} data - update Template Routing Details settings
   */

  /**
   * Update Template Routing Details response data
   * @typedef {Object} TemplateUpdateRoutingDetailsResponse
   * @property {string} id - unique ID of template routing details
   * @property {string} document_id - unique ID of Template
   * @property {TemplateUpdateRoutingDetailsItem[]} data - array with routing details
   * @property {string[]} cc - CC emails
   * @property {TemplateRoutingDetailsCCStep[]} cc_step - array of CC steps
   * @property {string} invite_link_instructions - invite signing instructions
   */

  /**
   * Updates Routing Details of specified template
   * @param {TemplateUpdateRoutingDetailsParams} data - update Template Routing Details payload
   * @param {function(err: ApiErrorResponse, res: TemplateUpdateRoutingDetailsResponse)} [callback] - error first node.js callback
   */
  static updateRoutingDetails ({
    id,
    token,
    data,
  }, callback) {
    const {
      template_data,
      cc,
      cc_step,
      invite_link_instructions,
    } = data;
    const JSONData = JSON.stringify({
      template_data,
      cc,
      cc_step,
      invite_link_instructions,
    });

    const updateCallback = (err, res) => {
      if (err) {
        callback(err);
        return;
      } else {
        const { data: routingDetails } = res;
        const rolesUpdated = routingDetails
          .map(route => {
            const { role_id, signing_order } = route;
            return promisify(updateSigningRole)({
              id: role_id,
              signing_order,
              token,
            });
          });

        Promise.all(rolesUpdated)
          .then(roleUpdatesRes => {
            return roleUpdatesRes.reduce((generalStatus, roleUpdateRes) => (generalStatus && !!roleUpdateRes.id), true);
          })
          .then(allRolesUpdated => {
            if (allRolesUpdated) {
              callback(null, res);
              return;
            } else {
              throw new Error('Some roles (actors) where not updated');
            }
          })
          .catch(roleUpdatesErr => {
            callback(roleUpdatesErr);
            return;
          });
      }
    };

    const req = https
      .request(buildRequestOptions({
        method: 'PUT',
        path: `/document/${id}/template/routing/detail`,
        authorization: {
          type: 'Bearer',
          token,
        },
        headers: {
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(JSONData),
        },
      }), responseHandler(updateCallback))
      .on('error', errorHandler(callback));

    req.write(JSONData);
    req.end();
  }

  /**
   * Template Bulk Invite settings
   * @typedef {Object} TemplateBulkInviteSettings
   * @property {string} file - path to .csv file with roles and their emails. Max file size should be <= 1 Megabyte
   * @property {string} folder_id - an ID of folder where signed documents will be saved
   * @property {number} [client_timestamp] - client timestamp in seconds
   * @property {string} [document_name] - setup prefix for each document name
   * @property {string} [subject] - custom email subject
   * @property {string} [email_message] - custom email message
   */

  /**
   * Send Template Bulk Invite payload
   * @typedef {Object} TemplateBulkInviteParams
   * @property {string} id - an ID of specific Template
   * @property {string} token - your auth token
   * @property {TemplateBulkInviteSettings} data - Template Bulk Invite settings
   */

  /**
   * Send Template Bulk Invite response data
   * @typedef {Object} TemplateBulkInviteResponse
   * @property {string} status - status of sending bulk invites (e.g. "job queued")
   */

  /**
   * Sends invites to sign a specified template to multiple signers at once
   * @experimental internal implementation of this method might be changed
   * @param {TemplateBulkInviteParams} data - Bulk Invite payload
   * @param {function(err: ApiErrorResponse, res: TemplateBulkInviteResponse)} [callback] - error first node.js callback
   */
  static bulkInvite ({
    id,
    token,
    data,
  }, callback) {
    const {
      file,
      folder_id,
      client_timestamp,
      document_name,
      subject,
      email_message,
    } = data;

    if (!isCSVFile(file)) {
      callback('The file that was tried to be uploaded is not a CSV file');
      return;
    }

    const metaData = {
      folder_id,
      client_timestamp,
      document_name,
      subject,
      email_message,
    };

    const formDataBoundary = getFormDataBoundary();
    const formDataBeginning = Object
      .entries(metaData)
      .reduce((formData, [
        name,
        value,
      ]) => `${formData}--${formDataBoundary}\r\nContent-Disposition: form-data; name="${name}";\r\n\r\n${value}\r\n`, '')
      .concat(`--${formDataBoundary}\r\n`)
      .concat(`Content-Disposition: form-data; name="file"; filename="${basename(file)}"\r\n`)
      .concat('Content-Type: text/csv\r\n\r\n');
    const formDataEnding = `\r\n--${formDataBoundary}\r\n`;
    let fileContent;

    try {
      fileContent = readFileSync(file);
    } catch (err) {
      callback(err);
      return;
    }

    const payload = Buffer.concat([
      Buffer.from(formDataBeginning, 'utf8'),
      Buffer.from(fileContent, 'binary'),
      Buffer.from(formDataEnding, 'utf8'),
    ]);

    const req = https
      .request(buildRequestOptions({
        method: 'POST',
        path: `/template/${id}/bulkinvite`,
        authorization: {
          type: 'Bearer',
          token,
        },
        headers: {
          'Content-Type': `multipart/form-data; boundary=${formDataBoundary}`,
          'Content-Length': Buffer.byteLength(payload),
        },
      }), responseHandler(callback))
      .on('error', errorHandler(callback));

    req.write(payload);
    req.end();
  }

}

module.exports = Template;