Skip to content
This repository was archived by the owner on Jan 23, 2025. It is now read-only.

Pull Request for both Docusign APIs #173

Merged
merged 9 commits into from
Apr 8, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 259 additions & 16 deletions actions/docusign.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,248 @@
* @author TCSASSEMBLER
*/
"use strict";
/*jslint unparam: true */

var async = require('async');
var _ = require('underscore');
var _ = require("underscore");
var async = require("async");
var S = require("string");
var config = require("../config").config;
var request = require('request');

var NotFoundError = require('../errors/NotFoundError');
var IllegalArgumentError = require('../errors/IllegalArgumentError');
var UnauthorizedError = require('../errors/UnauthorizedError');

/**
* The TermsOfUse Handler
* Note that just like the Java code, it can be reused for different templates and termsOfUseId
* The contrcutor takes the termsOfUseId during initialization
*/
function TermsOfUseHandler(termsOfUseId) {
this.termsOfUseId = termsOfUseId;
}
TermsOfUseHandler.prototype.termsOfUseId = 0;
/**
* The function that actually handles the document
* All future document handlers must also follow the same method signature as used here (akin to a Java interface)
* @param userId The user for which to handle the document
* @param tabs Arrays of objects which have tabLabel and tabValue parameters.
* This is actually not used here but is needed because the method signature needs to consistent for all document handlers
* @param api The actionhero api object
* @param dbConnectionMap The DB connection map
* @param done The callback to call once done. It will be called with argument if there is error, otherwise will be called without argument.
* The argument must have a message property. If the error is temporary, then it should also have a temporary property set to true.
*/
TermsOfUseHandler.prototype.handleDocument = function (userId, tabs, api, dbConnectionMap, done) {
var sqlParams = {
termsOfUseId: this.termsOfUseId,
userId: userId
};
async.waterfall([
function (cb) {
api.dataAccess.executeQuery("get_terms_of_use", sqlParams, dbConnectionMap, cb);
}, function (rows, cb) {
if (rows.length === 0) {
done({
message: "No terms of use exists for id: " + sqlParams.termsOfUseId,
});
return;
}
api.dataAccess.executeQuery("check_user_terms_of_use_ban", sqlParams, dbConnectionMap, cb);
}, function (rows, cb) {
if (rows.length !== 0) {
api.log("User with id: " + userId + " is not allowed to accept terms of use with id: " + sqlParams.termsOfUseId, 'error');
done();
return;
}
api.dataAccess.executeQuery("check_user_terms_of_use_exist", sqlParams, dbConnectionMap, cb);
}, function (rows, cb) {
if (rows.length !== 0) {
api.log("User with id: " + userId + " has already accepted terms of use with id: " + sqlParams.termsOfUseId, 'warn');
done();
return;
}
api.dataAccess.executeQuery("insert_user_terms_of_use", sqlParams, dbConnectionMap, cb);
}, function (notUsed, cb) {
cb();
}
], function (err) {
if (err) {
//If we have an error here, it is because of unexpected error (like DB connection died)
//So this needs to be considered a temporary failure, so that Docusign Connect can call us again later
done({
message: "Unable to process terms of use. Try again later.",
temporary: true
});
} else {
done();
}
});
};


/**
* Contains the template name, id and the handlers for the template
* Note that there can be more than 1 handlers per template
* Handlers that are not required for this contest are left empty
*/
var templates = [{
name: 'W9',
templateId: config.docusign.w9TemplateId,
handlers: []
}, {
name: 'W-8BEN',
templateId: config.docusign.w8benTemplateId,
handlers: []
}, {
name: 'TopCoder Assignment v2.0',
templateId: config.docusign.assignmentV2TemplateId,
handlers: [
new TermsOfUseHandler(config.docusign.assignmentDocTermsOfUseId)
]
}, {
name: 'Appirio Mutual NDA',
templateId: config.docusign.appirioMutualNDATemplateId,
handlers: []
}, {
name: 'Affidavit',
templateId: config.docusign.affidavitTemplateId,
handlers: []
}];

/**
* Convenience function that writes the response and calls the actionhero next
* @param connection actionhero connection
* @param statusCode the status code to write
* @param next The actionhero next callback
* @param message If exists then this message is set to body, otherwise body is simply 'success'
*/
function writeResponse(connection, statusCode, next, message) {
connection.rawConnection.responseHttpCode = statusCode;
connection.response = {
message: message || 'success'
};
next(connection, true);
}

/**
* The error to throw if connect key is missing or invalid
*/
var CONNECT_KEY_MISSING = 'Connect Key is missing or invalid.';

/**
* The Docusign Callback Action which accepts JSON.
* Performs the logic common to all Docusign documents.
*/
exports.docusignCallback = {
name: 'docusignCallback',
description: 'docusignCallback',
blockedConnectionTypes: [],
outputExample: {},
version: 'v2',
transaction : 'write',
cacheEnabled : false,
databases : ["informixoltp", "common_oltp"],
inputs: {
required: ['envelopeStatus', 'envelopeId', 'tabs', 'connectKey'],
optional: [],
},
run: function (api, connection, next) {
api.log("Execute docusignCallback#run", 'debug');
var dbConnectionMap = connection.dbConnectionMap,
envelopeStatus = connection.params.envelopeStatus,
envelopeId = connection.params.envelopeId,
connectKey = connection.params.connectKey,
tabs = connection.params.tabs,
sqlParams = {},
envelopeInfo;

async.waterfall([
function (cb) {
if (connectKey !== config.docusign.callbackConnectKey) {
api.log(CONNECT_KEY_MISSING, 'error');
writeResponse(connection, 404, next, CONNECT_KEY_MISSING);
return;
}

if (envelopeStatus !== 'Completed') {
api.log('Status is not completed.', 'info');
writeResponse(connection, 200, next);
return;
}

if (new S(envelopeId).isEmpty()) {
api.log('envelopeId is null or empty', 'error');
writeResponse(connection, 200, next);
return;
}

//Set completed = 1 for the envelope id
sqlParams.envelopeId = envelopeId;
api.dataAccess.executeQuery("complete_docusign_envelope", sqlParams, dbConnectionMap, cb);
}, function (updatedCount, cb) {
//updatedCount is the number of rows that were updated.
if (updatedCount === 1) {
//Get the docusign data (we need the templateId) for the envelope
api.dataAccess.executeQuery("get_docusign_envelope_by_envelope_id", sqlParams, dbConnectionMap, cb);
} else {
api.log('No enevelope with id: ' + envelopeId + ' was found.', 'error');
writeResponse(connection, 200, next);
return;
}
}, function (rows, cb) {
envelopeInfo = rows[0];

//Find the template for the envelope
var template = _.findWhere(templates, {templateId: envelopeInfo.docusign_template_id});
if (template === undefined) {
api.log('No Template was found for template id: ' + envelopeInfo.docusign_template_id, 'warn');
writeResponse(connection, 200, next);
return;
}

//Call the handlers for the template, one after the other
async.eachSeries(template.handlers, function (handler, cbx) {
handler.handleDocument(envelopeInfo.user_id, tabs, api, dbConnectionMap, cbx);
}, function (err) {
if (err) {
cb(err);
return;
}
cb();
});
}
], function (err) {
if (err) {
//All errors need to be communicated to the support staff
api.tasks.enqueue("sendEmail", {
subject : config.docusign.callbackFailedEmailSubject,
template : 'docusign_callback_failure_email',
toAddress : config.docusign.supportEmailAddress,
fromAddress : config.docusign.fromEmailAddress,
userId : envelopeInfo.user_id,
templateId: envelopeInfo.docusign_template_id,
envelopeId : envelopeInfo.docusign_envelope_id,
message : err.message
}, 'default');

//Only temporary errors are to return 500, otherwise 200
if (err.temporary === true) {
writeResponse(connection, 500, next, err.message);
} else {
writeResponse(connection, 200, next, err.message);
}
} else {
writeResponse(connection, 200, next);
}
});
}
};

/**
* Creates the options object used for making an HTTP request.
* Sets the HTTP method, url, body and the Docusign Authorization header
* @param <Object> api The infrastructure api object from which we read the configuration
* @param <Object> api The api object from which to read configuration
* @param <String> url The url to set for the HTTP request
* @param <String> method The verb to set for the HTTP request
* @param <String> body The body to set for the HTTP request in case method is POST. It must be a String not an Object.
Expand All @@ -42,7 +271,7 @@ function initializeRequest(api, url, method, body) {
/**
* The method that exposes the Get Docusign Recipient View URL API.
*/
exports.action = {
exports.generateDocusignViewURL = {
name: 'generateDocusignViewURL',
description: 'generateDocusignViewURL',
blockedConnectionTypes: [],
Expand Down Expand Up @@ -106,9 +335,14 @@ exports.action = {


//Perform login to docusign
options = initializeRequest(api, api.config.docusign.serverURL + "login_information", 'GET', '');
options = initializeRequest(api, config.docusign.serverURL + "login_information", 'GET', '');
request(options, function (err, res, body) {
var resp = JSON.parse(body);
var resp;
try {
resp = JSON.parse(body);
} catch (e) {
err = 'Invalid JSON received from server. Most likely the server url is incorrect.';
}
if (err || (res.statusCode !== 200 && res.statusCode !== 201)) {
//In case of system integration failure, we log the error (if we have one)...
//but we only show generic message to end user
Expand Down Expand Up @@ -167,7 +401,12 @@ exports.action = {
url = baseURL + "/envelopes";
options = initializeRequest(api, url, 'POST', JSON.stringify(reqParams));
request(options, function (err, res, body) {
var resp = JSON.parse(body);
var resp;
try {
resp = JSON.parse(body);
} catch (e) {
err = 'Invalid JSON received from server. Most likely the server url is incorrect.';
}
if (err || (res.statusCode !== 200 && res.statusCode !== 201)) {
//This is client's fault that they sent in a wrong template id
if (resp && resp.errorCode && resp.errorCode === 'TEMPLATE_ID_INVALID') {
Expand All @@ -185,14 +424,13 @@ exports.action = {
//persist the new envelope to database
sqlParams.envelopeId = resp.envelopeId;
sqlParams.complete = 0;
api.dataAccess.executeQuery('insert_docusign_envelope', sqlParams, dbConnectionMap,
function (err) {
if (err) {
cb(err);
return;
}
cb(null, resp.envelopeId);
});
api.dataAccess.executeQuery('insert_docusign_envelope', sqlParams, dbConnectionMap, function (err) {
if (err) {
cb(err);
return;
}
cb(null, resp.envelopeId);
});
});
} else {
//The envelope already exists
Expand All @@ -216,7 +454,12 @@ exports.action = {
};
options = initializeRequest(api, url, 'POST', JSON.stringify(reqParams));
request(options, function (err, res, body) {
var resp = JSON.parse(body);
var resp;
try {
resp = JSON.parse(body);
} catch (e) {
err = 'Invalid JSON received from server. Most likely the server url is incorrect.';
}
if (err || (res.statusCode !== 200 && res.statusCode !== 201)) {
//In case of system integration failure, we log error, but we only show generic message to user
if (resp && resp.message) {
Expand Down
46 changes: 46 additions & 0 deletions apiary.apib
Original file line number Diff line number Diff line change
Expand Up @@ -6369,3 +6369,49 @@ Payments APIs
"value":"503",
"description":"Servers are up but overloaded. Try again later."
}

## Docusign Callback [/terms/docusignCallback]
### Docusign Callback [POST]

+ Parameters
+ envelopeStatus (required, String, `Complete`) ... The status of the envelope
+ envelopeId (required, UUID, `9103DC77-D8F1-4D7B-BED1-6116604EE98C`) ... The envelope to process
+ tabs (required, Array, [{tabLabel: 'Handle', tabValue: 'anix'}, {...}]) ... The tab values. Can be empty
+ connectKey (required, String, 'ABCDED-12435-EDFADSEC') The conenct key

+ Response 200 (application/json)
{
"message": "some message"
}

+ Response 400 (application/json)

{
"name":"Bad Request",
"value":"400",
"description":"The request was invalid. An accompanying message will explain why."
}

+ Response 404 (application/json)

{
"name":"Not Found",
"value":"404",
"description":"This message will explain why the URI requested is invalid or the resource does not exist."
}

+ Response 500 (application/json)

{
"name":"Internal Server Error",
"value":"500",
"description":"Unknown server error. Please contact support."
}

+ Response 503 (application/json)

{
"name":"Service Unavailable",
"value":"503",
"description":"Servers are up but overloaded. Try again later."
}
Loading