diff --git a/README.md b/README.md index 1c1aa62..1e53765 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A production-ready 綠界全方位金流(ECPay All-In-One, AIO) SDK for Node.js with TypeScript Support [![build](https://github.com/simenkid/node-ecpay-aio/actions/workflows/build.yml/badge.svg)](https://github.com/simenkid/node-ecpay-aio/actions/workflows/build.yml) -![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/simenkid/6cd8ec3f4115bc7b0fc0cb646da2dd77/raw/d473b387740594dc486c5b8032ad8ba7adb7b91b/node-ecpay-aio__heads_main.json) +![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/simenkid/6cd8ec3f4115bc7b0fc0cb646da2dd77/raw/37458fd300efcea7ef2d3adbc4598e47a76a34d9/node-ecpay-aio__heads_main.json) [![npm](https://img.shields.io/npm/v/node-ecpay-aio.svg?cacheSeconds=3600)](https://www.npmjs.com/package/node-ecpay-aio) [![npm](https://img.shields.io/npm/l/node-ecpay-aio.svg?cacheSeconds=3600)](https://github.com/simenkid/node-ecpay-aio/blob/main/LICENSE) [![node version](https://img.shields.io/node/v/node-ecpay-aio)](https://img.shields.io/node/v/node-ecpay-aio) @@ -65,3 +65,7 @@ npm install --save node-ecpay-aio ## License Licensed under [MIT](https://github.com/simenkid/node-ecpay-aio/blob/main/LICENSE). + +
+
+
diff --git a/package.json b/package.json index 2673a56..1a4ac8d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ecpay-aio", - "version": "0.1.9", + "version": "0.2.0", "description": "A production-ready ECPay AIO SDK for Node.js with TypeScript support.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/__tests__/Merchant.test.ts b/src/__tests__/Merchant.test.ts index d4c7d31..be8e34b 100644 --- a/src/__tests__/Merchant.test.ts +++ b/src/__tests__/Merchant.test.ts @@ -19,7 +19,6 @@ import { TradeV2Query, FundingReconDetailQuery, } from '../feature/Query'; - import { CreditCardPeriodAction, DoAction } from '../feature/Action'; import { ECPayServiceUrls } from '../config'; import { TEST_MERCHANT_CONFIG, TEST_BASE_PARAMS } from './test_setting'; diff --git a/src/__tests__/Payment.test.ts b/src/__tests__/Payment.test.ts index 6961a4e..26f9439 100644 --- a/src/__tests__/Payment.test.ts +++ b/src/__tests__/Payment.test.ts @@ -1,5 +1,4 @@ //@ts-nocheck - import { Merchant } from '../feature/Merchant'; import { CreditOneTimePayment } from '../feature/Payment'; import { TEST_MERCHANT_CONFIG } from './test_setting'; diff --git a/src/__tests__/payments/ALLPayPayment.test.ts b/src/__tests__/payments/ALLPayPayment.test.ts index 7df70d1..19a37c9 100644 --- a/src/__tests__/payments/ALLPayPayment.test.ts +++ b/src/__tests__/payments/ALLPayPayment.test.ts @@ -1,6 +1,7 @@ //@ts-nocheck import { Merchant } from '../../feature/Merchant'; import { ALLPayment } from '../../feature/Payment'; +import { getCurrentTaipeiTimeString } from '../../utils'; import { TEST_MERCHANT_CONFIG, TEST_BASE_PARAMS } from '../test_setting'; describe('AndroidPayment: Check Params Types', () => { @@ -119,3 +120,22 @@ describe('AndroidPayment: Check Params Types', () => { }).toThrowError('must be less than or equal to 999'); }); }); + +describe('ALLPayment: Redirect Post Form', () => { + const merchant = new Merchant('Test', TEST_MERCHANT_CONFIG); + + const baseParams: BasePaymentParams = { + MerchantTradeNo: `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`, + MerchantTradeDate: getCurrentTaipeiTimeString(), + TotalAmount: 999, + TradeDesc: 'node-ecpay-aio testing order for ALLPayment', + ItemName: 'test item name', + }; + + test('Checkout with ', async () => { + const payment = merchant.createPayment(ALLPayment, baseParams, {}); + + const html = await payment.checkout(); + expect(html.startsWith('
{ @@ -24,3 +25,24 @@ describe('CreditDividePayment: Check Params Types', () => { }).toThrowError('must be one of'); }); }); + +describe('CreditDividePayment: Redirect Post Form', () => { + const merchant = new Merchant('Test', TEST_MERCHANT_CONFIG); + + const baseParams: BasePaymentParams = { + MerchantTradeNo: `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`, + MerchantTradeDate: getCurrentTaipeiTimeString(), + TotalAmount: 999, + TradeDesc: 'node-ecpay-aio testing order for CreditDividePayment', + ItemName: 'test item name', + }; + + test('Checkout with ', async () => { + const payment = merchant.createPayment(CreditDividePayment, baseParams, { + CreditInstallment: '3', + }); + + const html = await payment.checkout(); + expect(html.startsWith(' { @@ -58,36 +59,25 @@ describe('CreditOneTimePayment: Check Credit Base Params Types', () => { }); }); -// describe('CreditOneTimePayment: html', () => { -// const merchant = new Merchant('Test', TEST_MERCHANT_CONFIG); +describe('CreditOneTimePayment: Redirect Post Form', () => { + const merchant = new Merchant('Test', TEST_MERCHANT_CONFIG); -// const baseParams = { -// MerchantTradeNo: 'nodeecpayaio0011', -// MerchantTradeDate: '2021/05/22 11:20:20', -// TotalAmount: 999, -// TradeDesc: 'node-ecpay-aio testing order for CreditOneTimePayment', -// ItemName: 'test item name', -// }; + const baseParams: BasePaymentParams = { + MerchantTradeNo: `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`, + MerchantTradeDate: getCurrentTaipeiTimeString(), + TotalAmount: 999, + TradeDesc: 'node-ecpay-aio testing order for CreditOneTimePayment', + ItemName: 'test item name', + }; -// test('Checkout with ', async () => { -// const payment = merchant.createPayment( -// CreditOneTimePayment, -// baseParams, -// {} -// ); + test('Checkout with ', async () => { + const payment = merchant.createPayment( + CreditOneTimePayment, + baseParams, + {} + ); -// const html = await payment.checkout({ -// RelateNumber: 'rl-no', -// TaxType: '1', -// Donation: '0', -// Print: '0', -// InvoiceItemName: 'item1|item2', -// InvoiceItemCount: '2|5', -// InvoiceItemWord: '台|張', -// InvoiceItemPrice: '100|50', -// InvoiceRemark: '測試發票備註', -// CustomerPhone: '0911111111', -// }); -// console.log(html); -// }); -// }); + const html = await payment.checkout(); + expect(html.startsWith(' { }); }); -describe('CreditPeriodPayment: html', () => { +describe('CreditPeriodPayment: Redirect Post Form', () => { const merchant = new Merchant('Test', TEST_MERCHANT_CONFIG); const baseParams: BasePaymentParams = { - MerchantTradeNo: 'necacc0001', - MerchantTradeDate: '2022/05/13 15:33:20', + MerchantTradeNo: `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`, + MerchantTradeDate: getCurrentTaipeiTimeString(), TotalAmount: 999, TradeDesc: 'node-ecpay-aio testing order for CreditPeriodPayment', ItemName: 'test item name', @@ -355,19 +356,8 @@ describe('CreditPeriodPayment: html', () => { ExecTimes: 99, // PeriodReturnURL: 'https://ap.example.com/api', }); + const html = await payment.checkout(); - // const html = await payment.checkout({ - // RelateNumber: 'rl-no-1', - // TaxType: '1', - // Donation: '0', - // Print: '0', - // InvoiceItemName: 'item1|item2', - // InvoiceItemCount: '2|5', - // InvoiceItemWord: '台|張', - // InvoiceItemPrice: '100|50', - // InvoiceRemark: '測試發票備註', - // CustomerPhone: '0911111111', - // }); - // console.log(html); + expect(html.startsWith(' { @@ -13,3 +14,22 @@ describe('WebATMPayment: Check Params Types', () => { }).not.toThrowError(); }); }); + +describe('WebATMPayment: Redirect Post Form', () => { + const merchant = new Merchant('Test', TEST_MERCHANT_CONFIG); + + const baseParams: BasePaymentParams = { + MerchantTradeNo: `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`, + MerchantTradeDate: getCurrentTaipeiTimeString(), + TotalAmount: 999, + TradeDesc: 'node-ecpay-aio testing order for WebATMPayment', + ItemName: 'test item name', + }; + + test('Checkout with ', async () => { + const payment = merchant.createPayment(WebATMPayment, baseParams, {}); + + const html = await payment.checkout(); + expect(html.startsWith(' { }); describe('getCurrentTaipeiTimeString', () => { - const timestamp = 1652577669234; + const timestamp = 1652693484763; test('Get datetime string', () => { const tpeDatetime = getCurrentTaipeiTimeString({ timestamp }); - expect(tpeDatetime).toEqual('2022/05/15 09:21:38'); + expect(tpeDatetime).toEqual('2022/05/16 17:31:24'); }); test('Get date string', () => { @@ -284,7 +284,7 @@ describe('getCurrentTaipeiTimeString', () => { timestamp, format: 'Date', }); - expect(tpeDatetime).toEqual('2022/05/15'); + expect(tpeDatetime).toEqual('2022/05/16'); }); test('Get serial time string', () => { @@ -292,6 +292,6 @@ describe('getCurrentTaipeiTimeString', () => { timestamp, format: 'Serial', }); - expect(tpeDatetime).toEqual('20220518172109234'); + expect(tpeDatetime).toEqual('20220516173124763'); }); }); diff --git a/src/feature/Action.ts b/src/feature/Action.ts index e292685..29fa427 100644 --- a/src/feature/Action.ts +++ b/src/feature/Action.ts @@ -1,4 +1,5 @@ import { Merchant } from './Merchant'; +import { ActionError, CheckMacValueError } from './Error'; import { generateCheckMacValue, getCurrentUnixTimestampOffset, @@ -11,13 +12,11 @@ import { DoActionParamsSchema, } from '../schema'; import { - ActionResponseData, CreditCardPeriodActionParams, CreditCardPeriodActionResponseData, DoActionParams, DoActionResponseData, } from '../types'; -import { ActionError, CheckMacValueError } from './Error'; export class Action { merchant: Merchant; diff --git a/src/feature/Payment.ts b/src/feature/Payment.ts index ffa936a..5ac34be 100644 --- a/src/feature/Payment.ts +++ b/src/feature/Payment.ts @@ -1,12 +1,10 @@ -import { request, get } from 'https'; import { Merchant } from './Merchant'; +import { PaymentInfoQuery } from './Query'; import { generateCheckMacValue, generateRedirectPostForm, getEncodedInvoice, placeOrderRequest, - getQueryStringFromParams, - // PlaceCachedOrderRequest, } from '../utils'; import { ALLPaymentParamsSchema, @@ -37,7 +35,6 @@ import { ECPayPaymentType, PaymentInfoData, } from '../types'; -import { PaymentInfoQuery } from './Query'; export class Payment { merchant: Merchant; diff --git a/src/feature/Query.ts b/src/feature/Query.ts index c757d14..86f0465 100644 --- a/src/feature/Query.ts +++ b/src/feature/Query.ts @@ -1,4 +1,5 @@ import { Merchant } from './Merchant'; +import { CheckMacValueError, QueryError } from './Error'; import { generateCheckMacValue, getCurrentUnixTimestampOffset, @@ -24,7 +25,6 @@ import { TradeInfoData, TradeV2Data, } from '../types'; -import { CheckMacValueError, QueryError } from './Error'; const QUERY_RESULT_BASE_INT_FIELDS = [ 'TradeAmt', diff --git a/src/index.ts b/src/index.ts index e6022c2..1c3565a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,16 @@ export { FundingReconDetailQuery, } from './feature/Query'; +export { + QueryError, + ActionError, + CheckMacValueError, + PlaceOrderError, +} from './feature/Error'; + export { CreditCardPeriodAction, DoAction } from './feature/Action'; -export { getCurrentTaipeiTimeString } from './utils'; +export { + getCurrentTaipeiTimeString, + isValidReceivedCheckMacValue, +} from './utils'; diff --git a/src/utils/index.ts b/src/utils/index.ts index 10f6d43..579e091 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,11 +1,11 @@ -import { createHash } from 'crypto'; -import { URL, URLSearchParams } from 'url'; import { Buffer } from 'buffer'; import { request, get } from 'https'; - +import { URL, URLSearchParams } from 'url'; +import { createHash } from 'crypto'; import { decodeStream } from 'iconv-lite'; -import { InvoiceParams } from '../types'; + import { CheckMacValueError, PlaceOrderError } from '../feature/Error'; +import { InvoiceParams } from '../types'; export function generateCheckMacValue( params: any, @@ -283,17 +283,17 @@ export function getCurrentTaipeiTimeString(config?: { }) { const { timestamp = Date.now(), format = 'Datetime' } = config || {}; - const tzMinutesOffset = new Date(timestamp).getTimezoneOffset(); - const tpeTimestamp = timestamp + 80 * 60 * 60 * 1000; + const tzDiff = getTzOffsetFromTaipei(); + const tpeTimestamp = timestamp + tzDiff; const date = new Date(tpeTimestamp); const [year, month, day, hour, minute, second, ms] = [ date.getFullYear(), - `0${date.getMonth() + 1}`.slice(-2), - `0${date.getDate()}`.slice(-2), - `0${date.getHours()}`.slice(-2), - `0${date.getMinutes()}`.slice(-2), - `0${date.getSeconds()}`.slice(-2), - `00${date.getMilliseconds()}`.slice(-3), + `${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' @@ -303,16 +303,6 @@ export function getCurrentTaipeiTimeString(config?: { : `${year}${month}${day}${hour}${minute}${second}${ms}`; } -export function parseCachedOrder(html: string) { - // const hidx = html.indexOf('?timeStamp='); - // const tidx = html.indexOf('">'); - // const str = html.slice(hidx, tidx).replace(/&/g, '&'); - // return fromEntries(new URLSearchParams(str)); - const hidx = html.indexOf('/Cashier'); - const tidx = html.indexOf('">'); - return html.slice(hidx, tidx).replace(/&/g, '&'); -} - export function isValidReceivedCheckMacValue( data: { CheckMacValue: string }, hashKey: string, @@ -326,3 +316,51 @@ export function isValidReceivedCheckMacValue( const computedCMV = generateCheckMacValue(data, hashKey, hashIV); return data.CheckMacValue === computedCMV; } + +interface TimeDetail { + year: string; + month: string; + day: string; + hour: string; + minute: string; + second: string; +} + +function getTzOffsetFromTaipei() { + const options: Intl.DateTimeFormatOptions = { + 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: TimeDetail = parts.reduce((prev, curr) => { + prev[curr.type as keyof TimeDetail] = curr.value; + return prev; + }, {} as TimeDetail); + + 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; +}