UNPKG

node-ecpay-aio

Version:

A production-ready ECPay AIO SDK for Node.js with TypeScript support.

311 lines (310 loc) 13.1 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()); }); }; 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; }