var request = require('request');
var Promise = require('bluebird');
var uuid = require('uuid');
var configurations = require('./configurations');
var MercadopagoResponse = require('./utils/mercadopagoResponse');
var MercadoPagoError = require('./utils/mercadopagoError');
var validation = require('./validation');
var ETagRequest = require('request-etag');
var preConditions = require('./precondition');
var requestManager = module.exports = {
JSON_MIME_TYPE: 'application/json',
FORM_MIME_TYPE: 'application/x-www-form-urlencoded',
REST_CLIENT: new ETagRequest({
max: configurations.cache_max_size
}, request)
};
requestManager.describe = function (options) {
// This method will have the context of the class that is calling this (Will have the context of the class)
return function () {
var optMethod = requestManager.clone({}, options);
var calledArgs = arguments;
return new Promise(function (resolve, reject) {
var callback = calledArgs[calledArgs.length - 1]; // Last argument will always be the callback
var pathParameters = requestManager.getPathParamsKeyNames(optMethod.path);
var missingPayloadProperties = []; // Stores the missing payload path params (if there is any). POST, PUT, PATCH
var schema = this.schema; // Schema from resource
var needIdempotency = !!this.idempotency; // Idempotency from resource
var needPartnersHeaders = !!this.partnersHeaders;
var config = {};
var payload = {};
var error;
var totalFunctionParams;
var haveConfig = false;
// If callback doesn't exists add it to the arguments (Prevent code to fail)
if (typeof callback !== 'function' || callback === undefined) {
// Arguments is not a pure array. You need to make a normal array out of it. If not arguments.length won't work
calledArgs = Array.prototype.slice.call(calledArgs);
calledArgs.push(callback = function () {});
}
// If it is GET or DELETE the path variables needs to come from arguments
if (optMethod.method === 'GET' || optMethod.method === 'DELETE') {
haveConfig = (typeof calledArgs[calledArgs.length - 2] === 'object');
totalFunctionParams = (haveConfig) ? (pathParameters.length + 2) : (pathParameters.length + 1);
// Set the configurations
if (haveConfig) config = calledArgs[calledArgs.length - 2];
// Verify arguments quantity (invalid function call)
if (totalFunctionParams > calledArgs.length) {
error = new Error('Expecting parameters: ' + pathParameters.join(', ').replace(/:/g, ''));
reject(error);
return callback.apply(null, [error, null]);
}
// Replace the path parameters for the variables from the args(same Index that the one declarated on the path)
pathParameters.forEach(function (param, index) {
optMethod.path = optMethod.path.replace(param, calledArgs[index]);
});
} else {
haveConfig = (calledArgs.length > 2);
// If configurations are sent, set configurations and payload depending on the correspondent argument index
if (haveConfig) {
if (typeof calledArgs[calledArgs.length - 2] === 'object') config = calledArgs[calledArgs.length - 2];
if (typeof calledArgs[calledArgs.length - 3] === 'object') payload = calledArgs[calledArgs.length - 3];
} else if (typeof calledArgs[calledArgs.length - 2] === 'object') {
payload = calledArgs[calledArgs.length - 2];
}
// Replace the path parameters from the ones on the payload
pathParameters.forEach(function (param) {
var propertyFromPayload = param.replace(':', '');
if (payload && payload[propertyFromPayload]) {
optMethod.path = optMethod.path.replace(param, payload[propertyFromPayload]);
// Remove it from the payload or MercadoPago API will return an error for invalid parameter
delete payload[propertyFromPayload];
} else {
missingPayloadProperties.push(propertyFromPayload);
}
});
// If there are any missing properties show an error (invalid function call)
if (missingPayloadProperties.length > 0) {
error = new Error('The JSON is missing the following properties: ' + missingPayloadProperties.join(', '));
reject(error);
return callback.apply(null, [error, null]);
}
}
// If the path requires /sandbox prefix on sandbox mode, prepend it
if (optMethod.path_sandbox_prefix !== undefined && optMethod.path_sandbox_prefix && configurations.sandbox) {
optMethod.path = '/sandbox' + optMethod.path;
}
// Generate the AccessToken first (required to work with MercadoPago API)
return requestManager.generateAccessToken().then(function (accessToken) {
return requestManager.exec({
schema: schema,
base_url: (optMethod.base_url !== undefined) ? optMethod.base_url : '', // Overrides the base URI
path: optMethod.path,
method: optMethod.method,
config: config, // Configurations object
payload: payload, // Payload to send
idempotency: needIdempotency, // Needs the idempotency header
// If the merchant provides an access_token, it should override the access_token configured on init
access_token: config.access_token ? config.access_token : accessToken,
platformId: needPartnersHeaders && configurations.getPlatformId(),
corporationId: needPartnersHeaders && configurations.getCorporationId(),
integratorId: needPartnersHeaders && configurations.getIntegratorId(),
});
}).then(function (response) {
resolve(response);
return callback.apply(null, [null, response]);
}).catch(function (err) {
reject(err);
return callback.apply(null, [err, null]);
});
}.bind(this));
};
};
// Generate the access_token using the client_id and client_secret
requestManager.generateAccessToken = function (callback) {
var error;
callback = preConditions.getCallback(callback);
return new Promise(function (resolve, reject) {
// If the access_token is already set, return it from configurations
if (configurations.getAccessToken()) {
resolve(configurations.getAccessToken());
return callback.apply(null, [null, configurations.getAccessToken()]);
}
// If the SDK is not yet configure
if (!configurations.getClientId() || !configurations.getClientSecret()) {
error = new MercadoPagoError('Must set client_id and client_secret', '', 500, '');
reject(error);
return callback.apply(null, [error, null]);
}
return requestManager.exec({
path: '/oauth/token',
method: 'POST',
payload: {
client_id: configurations.getClientId(),
client_secret: configurations.getClientSecret(),
grant_type: 'client_credentials'
}
}).then(function (response) {
// Save token on configurations
// configurations.setAccessToken(response.body.access_token).setRefreshToken(response.body.refresh_token);
resolve(response.body.access_token);
return callback.apply(null, [null, response.body.access_token]);
}).catch(function (err) {
reject(err);
return callback.apply(null, [err, null]);
});
});
};
// Set the new access_token using the previous one & the refresh_token
requestManager.refreshAccessToken = function (callback) {
var error;
callback = preConditions.getCallback(callback);
return new Promise(function (resolve, reject) {
// Check if the refresh token is configure (require to refresh the access_token)
if (!configurations.getRefreshToken()) {
error = new MercadoPagoError('You need the refresh_token to refresh the access_token', '', 500, '');
reject(error);
return callback.apply(null, [error, null]);
}
return requestManager.exec({
path: '/oauth/token',
method: 'POST',
payload: {
client_secret: configurations.getAccessToken(),
grant_type: 'refresh_token'
}
}).then(function (response) {
configurations.setAccessToken(response.body.access_token)
.setRefreshToken(response.body.refresh_token);
resolve(response.body.access_token);
return callback.apply(null, [null, response.body.access_token]);
}).catch(function (err) {
reject(err);
return callback.apply(null, [err, null]);
});
});
};
/*
* Get user access_token (mpconnect) using the access_token, code, redirect_uri
* @param clientSecret - access_token from MercadoPago
* @param authorizationCode - authrozication_code obtain from redirectURI
* @param redirectURI - The one you use for obtaining the authrozication_code
* @param callback
*/
requestManager.getUserCredentials = function (clientSecret, authorizationCode, redirectURI, callback) {
callback = preConditions.getCallback(callback);
return new Promise(function (resolve, reject) {
return requestManager.exec({
path: '/oauth/token',
method: 'POST',
payload: {
client_secret: clientSecret,
code: authorizationCode,
redirect_uri: redirectURI,
grant_type: 'authorization_code'
}
}).then(function (response) {
resolve(response);
return callback.apply(null, [null, response]);
}).catch(function (err) {
reject(err);
return callback.apply(null, [err, null]);
});
});
};
/**
* Build the request using the options send and the configurations
* @param options
* @returns {object}
*/
requestManager.buildRequest = function (options) {
var req = {};
var schemaErrors = [];
var headersNames = [];
var headerName;
var i;
var accessToken = ((options.config && options.config.access_token) ? options.config.access_token : options.access_token);
req.uri = (options.base_url) ? options.base_url + options.path : configurations.getBaseUrl() + options.path;
req.method = options.method;
req.headers = {
'user-agent': configurations.getUserAgent(),
'x-product-id': configurations.getProductId(),
'x-tracking-id': configurations.getTrackingId(),
accept: requestManager.JSON_MIME_TYPE,
'content-type': requestManager.JSON_MIME_TYPE
};
req.qs = (options.config && options.config.qs) ? options.config.qs : {}; // Always set the querystring object
req.json = true; // Autoparse the response to JSON
req.headers['Authorization'] = `Bearer ${accessToken}`;
if(options.integratorId) {
req.headers['x-integrator-id'] = options.integratorId;
}
if(options.corporationId) {
req.headers['x-corporation-id'] = options.corporationId;
}
if(options.platformId) {
req.headers['x-platform-id'] = options.platformId;
}
if (options.config && options.config.headers && typeof options.config.headers === 'object') {
headersNames = Object.keys(options.config.headers);
for (i = 0; i < headersNames.length; i += 1) {
headerName = headersNames[i];
if (headerName !== 'user-agent' && headerName !== 'x-idempotency-key'
&& Object.prototype.hasOwnProperty.call(options.config.headers, headerName)) {
req.headers[headerName] = options.config.headers[headerName];
}
}
}
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
// Set idempotency header if the resource needs idempotency of the config specified one
if (options.idempotency || (options.config && options.config.idempotency)) {
req.headers['x-idempotency-key'] = options.config.idempotency || uuid.v4();
}
if (req.headers['content-type'] === requestManager.JSON_MIME_TYPE) {
// If there is a schema available, validate the payload before continue
if (options.schema) {
schemaErrors = validation.validate(options.schema, options.payload);
if (schemaErrors.length > 0) {
throw new Error(validation.generateErrorMessage(schemaErrors));
}
}
req.json = options.payload;
} else {
req.form = options.payload;
}
}
// Requires SSL certificates be valid
req.strictSSL = true;
return req;
};
/*
* Executes the request build with the options sent
* @param options
* @param callback
*/
requestManager.exec = function (options, callback) {
callback = preConditions.getCallback(callback);
return new Promise(function (resolve, reject) {
var req;
var mpResponse;
var mpError;
try {
req = requestManager.buildRequest(options);
} catch (e) {
reject(e);
return callback.apply(null, [e, null]);
}
return requestManager.REST_CLIENT(req, function (error, response, body) {
if (error) {
// Create a mercadopagoError allowing to retry the operation
mpError = new MercadoPagoError(error.message, null, null, req.headers['x-idempotency-key'], options, this);
reject(mpError);
return callback.apply(null, [mpError, null]);
}
if (response.statusCode < 200 || (response.statusCode >= 300 && response.statusCode !== 304)) {
// Create a mercadopagoError allowing to retry the operation
mpError = new MercadoPagoError(body.message, body.cause, response.statusCode, req.headers['x-idempotency-key'],
options, this);
reject(mpError);
return callback.apply(null, [mpError, null]);
}
// Create a mercadopagoResponse to be returned
mpResponse = new MercadopagoResponse(body, response.statusCode, req.headers['x-idempotency-key'],
body.paging, options, this);
resolve(mpResponse);
return callback.apply(null, [null, mpResponse]);
});
}.bind(this));
};
/*
* Get path params key names from a String containing the path. Exp: '/v1/payments/:id' (Generate an array with :id)
* @param path
* @returns {Array}
*/
requestManager.getPathParamsKeyNames = function (path) {
return path.match(/(:[a-z|A-Z|_|-]*)/g) || [];
};
/*
* Object.assign polyfill
* @param target
* @returns {any}
*/
requestManager.clone = function (target) {
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // pasamos si es undefined o null
for (var nextKey in nextSource) {
// Evita un error cuando 'hasOwnProperty' ha sido sobrescrito
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};