node-ecpay-aio
Version:
A production-ready ECPay AIO SDK for Node.js with TypeScript support.
311 lines (310 loc) • 13.1 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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isValidReceivedCheckMacValue = exports.getCurrentTaipeiTimeString = exports.parseIntegerFileds = exports.fromEntries = exports.getQueryStringFromParams = exports.placeOrderRequest = exports.postRequest = exports.getCurrentUnixTimestampOffset = exports.getEncodedInvoice = exports.generateRedirectPostForm = exports.generateCheckMacValue = void 0;
const buffer_1 = require("buffer");
const https_1 = require("https");
const url_1 = require("url");
const crypto_1 = require("crypto");
const iconv_lite_1 = require("iconv-lite");
const Error_1 = require("../feature/Error");
function generateCheckMacValue(params, hashKey, hashIV) {
const _params = Object.assign({}, params);
const excludedParams = ['HashKey', 'HashIV', 'CheckMacValue'];
excludedParams.forEach((p) => delete _params[p]);
// Rip undefined fields off
Object.keys(_params).forEach((p) => {
if (_params[p] === undefined)
delete _params[p];
});
const mac = Object.keys(_params)
.sort((a, b) => {
if (a.toUpperCase() < b.toUpperCase())
return -1;
if (a.toUpperCase() > b.toUpperCase())
return 1;
return 0;
})
.reduce((prev, curr) => (prev += `${curr}=${_params[curr]}&`), '');
// TBD: 手冊 p.27 CarruerNum 條目說明待驗證
// => 若手機條碼有 + 號, 先轉成空白再產生驗證碼(會在.net URLEncode的 +)
// if (_params.CarruerType && _params.CarruerType === '3') {
// _params.CarruerNum.replace(/+/g, ' ');
// }
// API文件: 附錄四 URLEncode 轉換表, .net URLEncode
// src_string => (step-1) encodeURIComponent
// => (step-2) turn certain chars back => .net URLEncoded string
const urlParamsStr = encodeURIComponent(`HashKey=${hashKey}&${mac}HashIV=${hashIV}`)
.toLowerCase()
.replace(/%2d/g, '-')
.replace(/%5f/g, '_')
.replace(/%2e/g, '.')
.replace(/%21/g, '!')
.replace(/%2a/g, '*')
.replace(/%28/g, '(')
.replace(/%29/g, ')')
.replace(/%20/g, '+');
return (0, crypto_1.createHash)('sha256').update(urlParamsStr).digest('hex').toUpperCase();
}
exports.generateCheckMacValue = generateCheckMacValue;
function generateRedirectPostForm(endpoint, params) {
const formId = '_form_aio_checkout';
const inputs = Object.keys(params)
.sort() // ensure every test to have the form with key=value pairs in the same order
.reduce((prev, curr) => {
let currVal = params[curr];
// ignore undefined fileds
return currVal === undefined
? prev
: (prev += `<input type="hidden" name="${curr}" id="${curr}" value="${currVal}" />`);
}, '') +
`<script type="text/javascript">document.getElementById("${formId}").submit();</script>`;
return `<form id="${formId}" action="${endpoint}" method="post">${inputs}</form>`;
}
exports.generateRedirectPostForm = generateRedirectPostForm;
function getEncodedInvoice(invoice) {
if (!invoice)
return;
const _invoice = Object.assign({}, invoice);
// remove any 'key=undefined' pair before encodeURIComponent()
Object.keys(_invoice).forEach((k) => {
if (_invoice[k] === undefined)
delete _invoice[k];
});
const encodingFileds = [
'CustomerName',
'CustomerAddr',
'CustomerEmail',
'InvoiceItemName',
'InvoiceItemWord',
'InvoiceRemark',
];
encodingFileds.forEach((key) => {
let val = _invoice[key];
if (val)
_invoice[key] = encodeURIComponent(val);
});
return _invoice;
}
exports.getEncodedInvoice = getEncodedInvoice;
function getCurrentUnixTimestampOffset(seconds) {
seconds = seconds || 0;
return Math.floor(new Date().getTime() / 1000) + seconds;
}
exports.getCurrentUnixTimestampOffset = getCurrentUnixTimestampOffset;
function postRequest(config) {
return __awaiter(this, void 0, void 0, function* () {
const { apiUrl, params, responseEncoding = 'utf8' } = config;
const _url = new url_1.URL(apiUrl);
const postData = getQueryStringFromParams(params, true);
const options = {
protocol: _url.protocol,
hostname: _url.hostname,
hash: _url.hash,
search: _url.search,
pathname: _url.pathname,
path: `${_url.pathname}${_url.search}`,
href: _url.toString(),
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': buffer_1.Buffer.byteLength(postData),
},
};
return new Promise((resolve, reject) => {
const req = (0, https_1.request)(options, (rsp) => {
const decodedRsp = (0, iconv_lite_1.decodeStream)(responseEncoding);
rsp.pipe(decodedRsp);
let dataStr = '';
// rsp.setEncoding('binary'); // default is binary
decodedRsp.on('data', (chunk) => (dataStr += chunk));
decodedRsp.on('end', () => {
try {
//if QueryCreditCardPeriodInfo: parse json, otherwise parse x=y&m=n to object
let data;
if (apiUrl.endsWith('QueryCreditCardPeriodInfo')) {
// No Response CheckMacValue
data = JSON.parse(dataStr);
}
else if (
// No Response CheckMacValue
apiUrl.endsWith('TradeNoAio') ||
apiUrl.endsWith('FundingReconDetail')) {
data = dataStr;
}
else {
data = fromEntries(new url_1.URLSearchParams(dataStr));
}
resolve(data);
}
catch (err) {
reject(err);
}
});
decodedRsp.on('error', reject);
});
req.on('error', reject);
req.write(postData);
req.end();
});
});
}
exports.postRequest = postRequest;
function placeOrderRequest(config) {
return __awaiter(this, void 0, void 0, function* () {
const { apiUrl, params, aioBaseUrl } = config;
const _url = new url_1.URL(apiUrl);
const postData = getQueryStringFromParams(params, true);
const options = {
protocol: _url.protocol,
hostname: _url.hostname,
hash: _url.hash,
search: _url.search,
pathname: _url.pathname,
path: `${_url.pathname}${_url.search}`,
href: _url.toString(),
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': buffer_1.Buffer.byteLength(postData),
},
};
return new Promise((resolve, reject) => {
const req = (0, https_1.request)(options, (rsp) => {
const decodedRsp = (0, iconv_lite_1.decodeStream)('utf8');
let dataStr = '';
if (rsp.statusCode === 302) {
// redirect to create order
dataStr = '';
const redirectReq = (0, https_1.get)(`${aioBaseUrl}${rsp.headers.location}`, (redirectRsp) => {
redirectRsp.pipe(decodedRsp);
decodedRsp.on('data', (chunk) => (dataStr += chunk));
decodedRsp.on('end', () => {
resolve(dataStr);
});
decodedRsp.on('error', reject);
});
redirectReq.on('error', reject);
redirectReq.end();
}
else {
rsp.pipe(decodedRsp);
decodedRsp.on('data', (chunk) => {
dataStr += chunk;
});
// not redirect: cannot create order automatically
decodedRsp.on('end', () => {
if (dataStr.includes('10300028'))
reject(new Error_1.PlaceOrderError('Duplicated MerchantTradeNo, create order failed.'));
else
reject(new Error_1.PlaceOrderError('Create order failed.'));
});
}
decodedRsp.on('error', reject);
});
req.on('error', reject);
req.write(postData);
req.end();
});
});
}
exports.placeOrderRequest = placeOrderRequest;
function getQueryStringFromParams(data, sort) {
const _params = new url_1.URLSearchParams(data);
// remove any 'key=undefined' pair
Object.keys(data).forEach((k) => {
if (data[k] === undefined)
_params.delete(k);
});
if (sort)
_params.sort();
return _params.toString();
}
exports.getQueryStringFromParams = getQueryStringFromParams;
function fromEntries(entries) {
return [...entries].reduce((obj, [key, val]) => {
obj[key] = val;
return obj;
}, {});
}
exports.fromEntries = fromEntries;
function parseIntegerFileds(input, fields) {
const result = Object.assign({}, input);
fields.forEach((key) => {
let val = result[key];
if (typeof val === 'string' && val !== '') {
let parsed = parseInt(val, 10);
if (!isNaN(parsed))
result[key] = parsed;
}
});
return result;
}
exports.parseIntegerFileds = parseIntegerFileds;
function getCurrentTaipeiTimeString(config) {
const { timestamp = Date.now(), format = 'Datetime' } = config || {};
const tzDiff = getTzOffsetFromTaipei();
const tpeTimestamp = timestamp + tzDiff;
const date = new Date(tpeTimestamp);
const [year, month, day, hour, minute, second, ms] = [
date.getFullYear(),
`${date.getMonth() + 1}`.padStart(2, '0'),
`${date.getDate()}`.padStart(2, '0'),
`${date.getHours()}`.padStart(2, '0'),
`${date.getMinutes()}`.padStart(2, '0'),
`${date.getSeconds()}`.padStart(2, '0'),
`${date.getMilliseconds()}`.padStart(3, '00'),
];
return format === 'Datetime'
? `${year}/${month}/${day} ${hour}:${minute}:${second}`
: format === 'Date'
? `${year}/${month}/${day}`
: `${year}${month}${day}${hour}${minute}${second}${ms}`;
}
exports.getCurrentTaipeiTimeString = getCurrentTaipeiTimeString;
function isValidReceivedCheckMacValue(data, hashKey, hashIV) {
if (!data.CheckMacValue)
throw new Error_1.CheckMacValueError('No CheckMacValue field within data.', data);
if (typeof data.CheckMacValue !== 'string')
return false;
const computedCMV = generateCheckMacValue(data, hashKey, hashIV);
return data.CheckMacValue === computedCMV;
}
exports.isValidReceivedCheckMacValue = isValidReceivedCheckMacValue;
function getTzOffsetFromTaipei() {
const options = {
timeZone: 'Asia/Taipei',
calendar: 'iso8601',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
};
const serverDate = new Date();
const serverTzOffsetFromUTC = serverDate.getTimezoneOffset() * 60 * 1000;
const dateTimeFormat = new Intl.DateTimeFormat(undefined, options); // Taipei
const parts = dateTimeFormat.formatToParts(serverDate);
const td = parts.reduce((prev, curr) => {
prev[curr.type] = curr.value;
return prev;
}, {});
td.hour = td.hour === '24' ? '00' : td.hour;
const { year, month, day, hour, minute, second } = td;
const ms = serverDate.getMilliseconds().toString().padStart(3, '00');
const taipeiTime = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}.${ms}Z`);
//@ts-ignore
const taipeiTzOffsetFromUTC = -(taipeiTime - serverDate);
const diff = serverTzOffsetFromUTC - taipeiTzOffsetFromUTC;
return diff;
}