hipay-professional-sdk
Version:
HiPay Professional SDK for JavaScript
357 lines • 16.7 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HipayException = exports.HipayClient = void 0;
const axios_1 = __importDefault(require("axios"));
const crypto_1 = __importDefault(require("crypto"));
const url_1 = require("url");
const xml_js_1 = require("xml-js");
const Types_1 = require("./Types");
const utils_1 = require("./utils");
const validateStatus = (status) => status === 200 || status === 500;
const transformResponse = (data) => data;
class HipayClient {
/**
* Create a new HipayClient.
*
* Get your API credentials (login/password) from the dashboard [Toolbox](https://professional.hipay.com/toolbox/).
*
* Important: If you wan't to use the stage environment (for testing) use the sandbox site:
* [test-professional.hipay.com](https://test-professional.hipay.com/toolbox/)!
* Test accounts are validated automatically, just enter random (but valid) information at each step
* (to validate bank information use Bank Name: "HSBC" and IBAN: "FR7630056009271234567890182").
*
* @param opts
*/
constructor(opts) {
this._environment = opts.env;
this._endpoint = HipayClient.getEndpoint(opts.env);
this._defaultData = {
wsLogin: opts.login,
wsPassword: opts.password,
};
if (opts.subAccountLogin) {
this._defaultData.wsSubAccountLogin = opts.subAccountLogin;
}
if (opts.subAccountId) {
this._defaultData.wsSubAccountId = opts.subAccountId;
}
this._defaultReqOpts = Object.assign({ timeout: 30 * 1000 }, opts.defaultReqOpts);
}
static getEndpoint(env) {
if (env === 'production') {
return 'https://ws.hipay.com/';
}
else if (env === 'stage') {
return 'https://test-ws.hipay.com/';
}
try {
const url = (0, url_1.parse)(env);
if ((url.protocol === 'http:' || url.protocol === 'https:') && url.slashes && !url.search) {
url.search = null;
url.query = null;
url.hash = null;
return (0, url_1.format)(url);
}
}
catch (ignored) { }
throw new Error('env must be "production", "stage" or a valid http(s) URL');
}
/**
* Returns client environment
*/
getEnvironment() {
return this._environment;
}
/**
* Returns client API endpoint
*/
getEndpoint() {
return this._endpoint;
}
request(uri, data, dataType, opts) {
return __awaiter(this, void 0, void 0, function* () {
const req = Object.assign(Object.assign({ baseURL: this._endpoint, url: uri, method: 'post', responseType: 'text', data: (0, utils_1.createBody)(this._endpoint, Object.assign(Object.assign({}, this._defaultData), data), dataType), validateStatus,
transformResponse }, this._defaultReqOpts), opts);
if (!req.headers) {
req.headers = {};
}
if (typeof req.headers['User-Agent'] === 'undefined') {
req.headers['User-Agent'] = 'hipay-professional-sdk-js/' + (0, utils_1.getPackageVersion)();
}
if (typeof req.headers['Content-Type'] === 'undefined') {
req.headers['Content-Type'] = 'text/xml;charset=UTF-8';
}
if (typeof req.headers.Accept === 'undefined') {
req.headers.Accept = 'text/xml;charset=UTF-8';
}
let httpResponse;
try {
httpResponse = yield axios_1.default.request(req);
}
catch (e) {
throw new HipayException('Error during HTTP requests to Hipay', e, e.isAxiosError ? e.response : undefined);
}
return this.parseResponse(httpResponse.data, dataType, httpResponse);
});
}
parseResponse(data, dataType, httpResponse) {
let error, result;
try {
let res = (0, xml_js_1.xml2js)(data, {
compact: true,
ignoreDeclaration: true,
ignoreInstruction: true,
ignoreAttributes: true,
ignoreComment: true,
ignoreCdata: true,
ignoreDoctype: true,
});
res = (0, utils_1.objectGetOrThrow)(res, 'SOAP-ENV:Envelope', 'SOAP-ENV:Body', 'ns1:' + dataType.reqType + 'Response', dataType.reqType + 'Result');
result = {};
for (const k in res) {
if (Object.prototype.hasOwnProperty.call(res, k) &&
!Object.prototype.hasOwnProperty.call(Object.prototype, k)) {
result[k] = res[k]._text;
}
}
if (result.code !== '0') {
error = { code: parseInt(result.code), description: result.description };
}
else {
delete result.code;
delete result.description;
}
}
catch (e) {
throw new HipayException("Error while parsing Hipay's response", e, httpResponse);
}
const ret = (error ? { httpResponse, error } : { httpResponse, result });
Object.defineProperty(ret, 'httpResponse', { enumerable: false });
return ret;
}
/**
* Create a new order.
*
* At the time of payment you must create a new order then redirect the customer to the secure payment page hosted
* by HiPay.
* When the customer makes the payment the order is authorized and you can {@link HipayClient.captureOrder
* capture it}.
*
* [HiPay documentation](https://developer.hipay.com/getting-started/platform-hipay-professional/overview/#soap-api-resources-request-a-new-order)
*
* @param req Requests parameters.
* @param opts Requests options (you can set default values when creating the client:
* {@link HipayClientOptions.defaultReqOpts}).
* @return
* - *resolved* with an {@link HipayResponse} when the request complete (with {@link HipayResponse.error
* an error} or {@link HipayResponse.result the result})
* - *rejected* with an {@link HipayException} when an exception occurs (network error, malformed response, ...)
*/
createOrder(req, opts) {
return this.request('/soap/payment-v2/generate', req, Types_1.definitions.CreateOrderRequest, opts);
}
/**
* Capture an order.
*
* Instruct the payment gateway to capture a previously-authorized transaction, i.e. transfer the funds from the
* customer's bank account to the merchant's bank account. This transaction is always preceded by an authorization.
*
* [HiPay documentation](https://developer.hipay.com/getting-started/platform-hipay-professional/overview/#soap-api-resources-maintenance-operations)
*
* @param req Requests parameters.
* @param opts Requests options (you can set default values when creating the client:
* {@link HipayClientOptions.defaultReqOpts}).
* @return
* - *resolved* with an {@link HipayResponse} when the request complete (with {@link HipayResponse.error
* an error} or {@link HipayResponse.result the result})
* - *rejected* with an {@link HipayException} when an exception occurs (network error, malformed response, ...)
*/
captureOrder(req, opts) {
return this.request('/soap/transaction-v2/confirm', req, Types_1.definitions.CaptureOrderRequest, opts);
}
/**
* Cancel an order.
*
* [HiPay documentation](https://developer.hipay.com/getting-started/platform-hipay-professional/overview/#soap-api-resources-maintenance-operations)
*
* @param req Requests parameters.
* @param opts Requests options (you can set default values when creating the client:
* {@link HipayClientOptions.defaultReqOpts}).
* @return
* - *resolved* with an {@link HipayResponse} when the request complete (with {@link HipayResponse.error
* an error} or {@link HipayResponse.result the result})
* - *rejected* with an {@link HipayException} when an exception occurs (network error, malformed response, ...)
*/
cancelOrder(req, opts) {
return this.request('/soap/transaction-v2/cancel', req, Types_1.definitions.CancelOrderRequest, opts);
}
/**
* Refund an order.
*
* [HiPay documentation](https://developer.hipay.com/getting-started/platform-hipay-professional/overview/#soap-api-resources-refund-an-order)
*
* @param req Requests parameters.
* @param opts Requests options (you can set default values when creating the client:
* {@link HipayClientOptions.defaultReqOpts}).
* @return
* - *resolved* with an {@link HipayResponse} when the request complete (with {@link HipayResponse.error
* an error} or {@link HipayResponse.result the result})
* - *rejected* with an {@link HipayException} when an exception occurs (network error, malformed response, ...)
*/
refundOrder(req, opts) {
return this.request('/soap/refund-v2/card', req, Types_1.definitions.RefundOrderRequest, opts);
}
/**
* Parse Notification (callback) inputs.
*
* After a successful purchase, HiPay calls twice your Notification (callback) URL in background with comprehensive
* information about the payment (the first time for the authorization notification and the second one for the
* capture notification).
* Information are passed through an `xml` field in the body of an http POST request (of type
* `application/x-www-form-urlencoded`).
* This method parses the contents of this xml field and validates the checksum of the request.
*
* Checksum or Signature verification:
* TODO (wait for HiPay support information about documentations errors)
*
* [HiPay documentation](https://developer.hipay.com/getting-started/platform-hipay-professional/overview/#server-to-server-notifications-what-is-a-server-to-server-notification)
*
* @param xmlStr The value of the field `xml` (from POST request body)
* @param opts
* @return
* - *resolved* with an {@link HipayNotificationResponse} when no error is encountered
* - *rejected* with an {@link Error} when any error occurs (invalid format, bad signature, ...)
*/
parseNotification(xmlStr, opts) {
return __awaiter(this, void 0, void 0, function* () {
// NOTE: This method returns a Promise in case of parsing or signature verification become async a day!
let xml;
try {
xml = (0, xml_js_1.xml2js)(xmlStr, {
compact: true,
ignoreDeclaration: true,
ignoreInstruction: true,
ignoreAttributes: true,
ignoreComment: true,
ignoreCdata: true,
ignoreDoctype: true,
});
}
catch (e) {
throw new Error("Can't decode XML content");
}
if (typeof xml !== 'object' ||
typeof xml.mapi !== 'object' ||
typeof xml.mapi.mapiversion !== 'object' ||
typeof xml.mapi.mapiversion._text !== 'string' ||
typeof xml.mapi.md5content !== 'object' ||
typeof xml.mapi.md5content._text !== 'string' ||
typeof xml.mapi.result !== 'object') {
throw new Error('Incomplete XML content');
}
const checkMd5Content = !opts || opts.checkMd5Content !== false; // defaults to true
const checkSignature = !!opts && opts.checkSignature === true; // defaults to false
if (checkMd5Content || checkSignature) {
const md5content = Buffer.from(xml.mapi.md5content._text, 'hex');
if (!md5content || md5content.length !== 16) {
throw new Error('md5content is invalid');
}
const resultBegin = xmlStr.search(/<result(\s|>)/);
// istanbul ignore next
if (resultBegin === -1) {
throw new Error('Unable to find result begin');
}
let resultEnd = xmlStr.lastIndexOf('</result>');
// istanbul ignore next
if (resultEnd === -1) {
throw new Error('Unable to find result end');
}
resultEnd += 9; // '</result>'.length
const checkMd5Content = (withPassword) => {
const hash = crypto_1.default.createHash('md5');
hash.update(Buffer.from(xmlStr.substring(resultBegin, resultEnd), 'utf8'));
if (withPassword) {
hash.update(Buffer.from(this._defaultData.wsPassword, 'utf8'));
}
const signature = hash.digest();
if (!crypto_1.default.timingSafeEqual(md5content, signature)) {
throw new Error(md5content.toString('hex') + '(current) != ' + signature.toString('hex') + '(expected)');
}
};
try {
checkMd5Content(checkSignature);
}
catch (e) {
if (checkSignature) {
throw new Error('Bad signature: ' + e.message);
}
else {
// if the legacy md5content check fail, we need to try the new signature check
try {
checkMd5Content(true);
}
catch (e2) {
throw new Error('Bad md5content: ' + e.message + ', ' + e2.message);
}
}
}
}
const result = {};
for (const k in xml.mapi.result) {
if (Object.prototype.hasOwnProperty.call(xml.mapi.result, k) &&
!Object.prototype.hasOwnProperty.call(Object.prototype, k)) {
const v = xml.mapi.result[k];
if (k === 'merchantDatas') {
result[k] = {};
for (const dataKey in v) {
if (Object.prototype.hasOwnProperty.call(v, dataKey) && dataKey.indexOf('_aKey_') === 0) {
const k2 = dataKey.substr(6); // '_aKey_'.length = 6
if (!Object.prototype.hasOwnProperty.call(Object.prototype, k2)) {
result[k][k2] = v[dataKey]._text;
}
}
}
}
else if (typeof v._text !== 'undefined') {
result[k] = v._text;
}
}
}
return {
mapiversion: xml.mapi.mapiversion._text,
md5content: xml.mapi.md5content._text,
result,
};
});
}
toString() {
return 'HipayClient{environment=' + this._environment + '}';
}
}
exports.HipayClient = HipayClient;
/**
* API request exception.
*
* Unlike {@link HipayError errors}, exceptions are unexpected and unanticipated events (network errors, ...).
*/
class HipayException extends Error {
constructor(message, cause, httpResponse) {
super(message + (cause && cause.message ? ' (' + cause.message + ')' : ''));
this.cause = cause;
this.httpResponse = httpResponse;
}
}
exports.HipayException = HipayException;
//# sourceMappingURL=HipayClient.js.map