UNPKG

hipay-professional-sdk

Version:
357 lines 16.7 kB
"use strict"; 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