Skip to content

Commit 04d4153

Browse files
author
Simen Li
committed
bump to v0.1.9
1 parent 4f5889e commit 04d4153

12 files changed

Lines changed: 393 additions & 194 deletions

File tree

README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# node-ecpay-aio
22

3-
綠界全方位金流(ECPay All-In-One, AIO) SDK for Node.js with TypeScript Support
3+
A production-ready 綠界全方位金流(ECPay All-In-One, AIO) SDK for Node.js with TypeScript Support
44

55
[![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)
66
![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/simenkid/6cd8ec3f4115bc7b0fc0cb646da2dd77/raw/d473b387740594dc486c5b8032ad8ba7adb7b91b/node-ecpay-aio__heads_main.json)
@@ -13,13 +13,13 @@
1313

1414
## Documentation
1515

16-
本模組詳細使用說明請見 [User Guide](https://github.com/simenkid/node-ecpay-aio/wiki/User-Guide)
16+
本模組詳細使用說明請見 [User Guide](https://github.com/simenkid/node-ecpay-aio/wiki)
1717

1818
<br />
1919

2020
## Overview
2121

22-
本模組這是根據綠界官方 AIO 規格的全新實作,並非官方維護的 SDK 或是 fork 改寫版本。此模組的設計初衷是為了與官方最新的 API 規格一致、更貼近 JS 開發風格、更完善的文件說明、提供 TypeScript 支援以及盡可能完善的自動化測試來保證 SDK 穩定性
22+
本模組是根據綠界官方 AIO 規格的全新實作,並非官方維護的 SDK 或是 fork 改寫版本。此模組的設計初衷是為了與官方最新的 API 規格一致、更貼近 JS 開發風格、提供 TypeScript 支援、盡可能完善的自動化測試以及更完善的文件說明
2323

2424
<br />
2525

@@ -41,23 +41,23 @@ npm install --save node-ecpay-aio
4141
## Features
4242

4343
- 內建 TypeScript 支援
44-
- 按職責區分以下類別
45-
- Merchant (特店)
46-
- Payment (付款方式)
47-
- Query (查詢)
48-
- Action (操作)
49-
- 使用 [yup](https://github.com/jquense/yup) 對 API 要求參數與相依條件進行防衛驗證與拋出驗證失敗訊息
44+
- 按職責區分 Merchant (特店)、Payment (付款方式)、Query (查詢)、Action (操作)等類別
45+
- ATM、超商代碼與超商條碼可於後端建立訂單時一併獲得取號結果
46+
- 結構化的錯誤訊息,更易於程式流程控制
47+
- 使用 [yup](https://github.com/jquense/yup) 對 API 要求參數與相依條件進行防衛驗證
48+
- 自動為 REQ/RSP 產生與驗證 CheckMacValue,若驗證到接收響應之校驗碼不符會自動拋錯
49+
- 遠端查詢或操作作業失敗,會自動拋錯並附帶綠界傳回的錯誤碼、錯誤訊息與原始響應資料
5050
- 自動設定 derivable 的參數,如 `ChoosePayment`, `InvoiceMark`
51-
- 自動為 REQ/RSP 產生與驗證 CheckMacValue,接收時若驗證到校驗碼不符會自動拋錯
5251
- 支援 Node.js v10.x 以上 (自動測試環境包含 v10.x, v12.x, v14.x 與 v16.x)
5352
- Serverless (Lambda) friendly
5453

5554
<br />
5655

5756
## Notes
5857

58+
- 使用前建議先讀過官方文件與本模組 [User Guide](https://github.com/simenkid/node-ecpay-aio/wiki)
5959
- 歡迎發 Issue,但我不一定有時間修
60-
- 更歡迎發 PR (帶測試佳)或共同維護
60+
- 更歡迎發 PR(請帶測試,因本模組已在生產環境中使用)或共同維護
6161
- 本模組基本穩定,但有些功能綠界沒有提供 Testing stage,自動化測試跑不到那些 cases。
6262

6363
<br />

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "node-ecpay-aio",
3-
"version": "0.1.4",
4-
"description": "ECPay AIO SDK for Node.js with TypeScript support.",
3+
"version": "0.1.9",
4+
"description": "A production-ready ECPay AIO SDK for Node.js with TypeScript support.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
77
"files": [

src/__tests__/payments/ATMPayment.test.ts

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe('ATMPayment: Check Params Constraints', () => {
7878
});
7979
});
8080

81-
describe('ATMPayment: html', () => {
81+
describe('ATMPayment: Redirect Post Form', () => {
8282
const merchant = new Merchant('Test', TEST_MERCHANT_CONFIG);
8383

8484
const baseParams: BasePaymentParams = {
@@ -91,62 +91,73 @@ describe('ATMPayment: html', () => {
9191

9292
test('Checkout with ', async () => {
9393
const payment = merchant.createPayment(ATMPayment, baseParams, {
94-
ClientRedirectURL:
95-
'https://payment-stage.ecpay.com.tw/PaymentRule/ATMPaymentInfo',
96-
ChooseSubPayment: 'BOT',
9794
ExpireDate: 7,
9895
});
96+
9997
const html = await payment.checkout();
100-
// const html = await payment.checkout({
101-
// RelateNumber: 'rl-no-1',
102-
// TaxType: '1',
103-
// Donation: '0',
104-
// Print: '0',
105-
// InvoiceItemName: 'item1|item2',
106-
// InvoiceItemCount: '2|5',
107-
// InvoiceItemWord: '台|張',
108-
// InvoiceItemPrice: '100|50',
109-
// InvoiceRemark: '測試發票備註',
110-
// CustomerPhone: '0911111111',
111-
// });
112-
// console.log(html);
98+
expect(html.startsWith('<form id="_form_aio_checkout"')).toBe(true);
11399
});
114100
});
115101

116-
// describe('ATMPayment: placeOrder', () => {
117-
// const merchant = new Merchant('Test', {
118-
// MerchantID: '2000132',
119-
// HashKey: '5294y06JbISpM5x9',
120-
// HashIV: 'v77hoKGq4kWxNNIS',
121-
// ReturnURL: 'https://api.test.com/our/hook',
122-
// });
123-
124-
// const baseParams: BasePaymentParams = {
125-
// MerchantTradeNo: `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`,
126-
// MerchantTradeDate: getCurrentTaipeiTimeString(),
127-
// TotalAmount: 500,
128-
// TradeDesc: 'node-ecpay-aio testing order for ATMPayment',
129-
// ItemName: 'test item name',
130-
// };
131-
132-
// test('Placed Order with ', async () => {
133-
// const payment = merchant.createPayment(ATMPayment, baseParams, {
134-
// ExpireDate: 3,
135-
// });
136-
137-
// const rsp = await payment.placeOrder({
138-
// RelateNumber: 'rl-no-1',
139-
// TaxType: '1',
140-
// Donation: '0',
141-
// Print: '0',
142-
// InvoiceItemName: 'item1|item2',
143-
// InvoiceItemCount: '2|5',
144-
// InvoiceItemWord: '台|張',
145-
// InvoiceItemPrice: '100|50',
146-
// InvoiceRemark: '測試發票備註',
147-
// CustomerPhone: '0911111111',
148-
// });
149-
150-
// console.log(rsp);
151-
// });
152-
// });
102+
describe('ATMayment: Place Order', () => {
103+
jest.setTimeout(60000);
104+
const merchant = new Merchant('Test', MERCHANT_CONFIG_ASYNC);
105+
106+
const baseParams: BasePaymentParams = {
107+
MerchantTradeDate: getCurrentTaipeiTimeString(),
108+
TotalAmount: 300,
109+
TradeDesc: 'node-ecpay-aio testing order for CVSPayment',
110+
ItemName: 'test item name',
111+
};
112+
113+
const invoice = {
114+
RelateNumber: 'nea-ci',
115+
TaxType: '1',
116+
Donation: '0',
117+
Print: '0',
118+
InvoiceItemName: 'item1|item2',
119+
InvoiceItemCount: '2|5',
120+
InvoiceItemWord: '台|張',
121+
InvoiceItemPrice: '100|50',
122+
InvoiceRemark: '測試發票備註',
123+
CustomerPhone: '0911111111',
124+
};
125+
126+
test('Must pass when placing a new order', async () => {
127+
const mTradeNo = `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`;
128+
const payment = merchant.createPayment(ATMPayment, {
129+
MerchantTradeNo: mTradeNo,
130+
...baseParams,
131+
});
132+
133+
const rsp = await payment.placeOrder(invoice);
134+
expect(rsp.RtnCode).toBe(2);
135+
expect(rsp.MerchantTradeNo).toBe(mTradeNo);
136+
});
137+
138+
test('Must be rejected when placing Order with a duplicated MerchantTradeNo', async () => {
139+
await new Promise((r) => setTimeout(r, 500));
140+
141+
const mTradeNo = `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`;
142+
const payment = merchant.createPayment(
143+
ATMPayment,
144+
{
145+
MerchantTradeNo: mTradeNo,
146+
...baseParams,
147+
},
148+
{
149+
ExpireDate: 3,
150+
}
151+
);
152+
153+
try {
154+
const rsp = await payment.placeOrder(invoice);
155+
const duplicatedRsp = await payment.placeOrder(invoice);
156+
} catch (e) {
157+
expect(e.name).toBe('PlaceOrderError');
158+
expect(e.message).toBe(
159+
'Duplicated MerchantTradeNo, create order failed.'
160+
);
161+
}
162+
});
163+
});

src/__tests__/payments/BARCODEPayment.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//@ts-nocheck
22
import { Merchant } from '../../feature/Merchant';
33
import { BARCODEPayment } from '../../feature/Payment';
4+
import { getCurrentTaipeiTimeString } from '../../utils';
45
import { TEST_MERCHANT_CONFIG, TEST_BASE_PARAMS } from '../test_setting';
56

67
const MERCHANT_CONFIG_ASYNC = {
@@ -84,3 +85,88 @@ describe('BARCODEPayment: Check Params Constraints', () => {
8485
}).toThrowError('must be less than or equal to 30');
8586
});
8687
});
88+
89+
describe('BARCODEPayment: Redirect Post Form', () => {
90+
const merchant = new Merchant('Test', TEST_MERCHANT_CONFIG);
91+
92+
const baseParams: BasePaymentParams = {
93+
MerchantTradeNo: `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`,
94+
MerchantTradeDate: getCurrentTaipeiTimeString(),
95+
TotalAmount: 999,
96+
TradeDesc: 'node-ecpay-aio testing order for BARCODEPayment',
97+
ItemName: 'test item name',
98+
};
99+
100+
test('Checkout with ', async () => {
101+
const payment = merchant.createPayment(BARCODEPayment, baseParams, {
102+
ExpireDate: 7,
103+
});
104+
105+
const html = await payment.checkout();
106+
expect(html.startsWith('<form id="_form_aio_checkout"')).toBe(true);
107+
});
108+
});
109+
110+
describe('BARCODEPayment: Place Order', () => {
111+
jest.setTimeout(60000);
112+
113+
const merchant = new Merchant('Test', MERCHANT_CONFIG_ASYNC);
114+
115+
const baseParams: BasePaymentParams = {
116+
MerchantTradeDate: getCurrentTaipeiTimeString(),
117+
TotalAmount: 1000,
118+
TradeDesc: 'node-ecpay-aio testing order for BARCODEPayment',
119+
ItemName: 'test item name',
120+
};
121+
122+
const invoice = {
123+
RelateNumber: 'nea-ci',
124+
TaxType: '1',
125+
Donation: '0',
126+
Print: '0',
127+
InvoiceItemName: 'item1|item2',
128+
InvoiceItemCount: '2|5',
129+
InvoiceItemWord: '台|張',
130+
InvoiceItemPrice: '100|50',
131+
InvoiceRemark: '測試發票備註',
132+
CustomerPhone: '0911111111',
133+
};
134+
135+
test('Must pass when placing a new order', async () => {
136+
const mTradeNo = `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`;
137+
const payment = merchant.createPayment(BARCODEPayment, {
138+
MerchantTradeNo: mTradeNo,
139+
...baseParams,
140+
});
141+
142+
const rsp = await payment.placeOrder(invoice);
143+
expect(rsp.RtnCode).toBe(10100073);
144+
expect(rsp.MerchantTradeNo).toBe(mTradeNo);
145+
});
146+
147+
test('Must be rejected when placing Order with a duplicated MerchantTradeNo', async () => {
148+
await new Promise((r) => setTimeout(r, 500));
149+
150+
const mTradeNo = `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`;
151+
const payment = merchant.createPayment(
152+
BARCODEPayment,
153+
{
154+
MerchantTradeNo: mTradeNo,
155+
...baseParams,
156+
},
157+
{
158+
ExpireDate: 3,
159+
}
160+
);
161+
162+
try {
163+
const rsp = await payment.placeOrder(invoice);
164+
const duplicatedRsp = await payment.placeOrder(invoice);
165+
} catch (e) {
166+
expect(e.name).toBe('PlaceOrderError');
167+
expect(e.message).toBe(
168+
'Duplicated MerchantTradeNo, create order failed.'
169+
);
170+
}
171+
});
172+
});

src/__tests__/payments/CVSPayment.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//@ts-nocheck
22
import { Merchant } from '../../feature/Merchant';
33
import { CVSPayment } from '../../feature/Payment';
4+
import { getCurrentTaipeiTimeString } from '../../utils';
45
import { TEST_MERCHANT_CONFIG, TEST_BASE_PARAMS } from '../test_setting';
56

67
const MERCHANT_CONFIG_ASYNC = {
@@ -82,3 +83,87 @@ describe('CVSPayment: Check Params Constraints', () => {
8283
}).toThrowError('must be less than or equal to 43200');
8384
});
8485
});
86+
87+
describe('CVSPayment: Redirect Post Form', () => {
88+
const merchant = new Merchant('Test', TEST_MERCHANT_CONFIG);
89+
90+
const baseParams: BasePaymentParams = {
91+
MerchantTradeNo: `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`,
92+
MerchantTradeDate: getCurrentTaipeiTimeString(),
93+
TotalAmount: 999,
94+
TradeDesc: 'node-ecpay-aio testing order for CVSPayment',
95+
ItemName: 'test item name',
96+
};
97+
98+
test('Checkout with ', async () => {
99+
const payment = merchant.createPayment(CVSPayment, baseParams, {
100+
ExpireDate: 7,
101+
});
102+
103+
const html = await payment.checkout();
104+
expect(html.startsWith('<form id="_form_aio_checkout"')).toBe(true);
105+
});
106+
});
107+
108+
describe('CVSPayment: Place Order', () => {
109+
jest.setTimeout(120000);
110+
const merchant = new Merchant('Test', MERCHANT_CONFIG_ASYNC);
111+
112+
const baseParams: BasePaymentParams = {
113+
MerchantTradeDate: getCurrentTaipeiTimeString(),
114+
TotalAmount: 300,
115+
TradeDesc: 'node-ecpay-aio testing order for CVSPayment',
116+
ItemName: 'test item name',
117+
};
118+
119+
const invoice = {
120+
RelateNumber: 'nea-ci',
121+
TaxType: '1',
122+
Donation: '0',
123+
Print: '0',
124+
InvoiceItemName: 'item1|item2',
125+
InvoiceItemCount: '2|5',
126+
InvoiceItemWord: '台|張',
127+
InvoiceItemPrice: '100|50',
128+
InvoiceRemark: '測試發票備註',
129+
CustomerPhone: '0911111111',
130+
};
131+
132+
test('Must pass when placing a new order', async () => {
133+
const mTradeNo = `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`;
134+
const payment = merchant.createPayment(CVSPayment, {
135+
MerchantTradeNo: mTradeNo,
136+
...baseParams,
137+
});
138+
139+
const rsp = await payment.placeOrder(invoice);
140+
expect(rsp.RtnCode).toBe(10100073);
141+
expect(rsp.MerchantTradeNo).toBe(mTradeNo);
142+
});
143+
144+
test('Must be rejected when placing Order with a duplicated MerchantTradeNo', async () => {
145+
await new Promise((r) => setTimeout(r, 500));
146+
147+
const mTradeNo = `nea${getCurrentTaipeiTimeString({ format: 'Serial' })}`;
148+
const payment = merchant.createPayment(
149+
CVSPayment,
150+
{
151+
MerchantTradeNo: mTradeNo,
152+
...baseParams,
153+
},
154+
{
155+
ExpireDate: 3,
156+
}
157+
);
158+
159+
try {
160+
const rsp = await payment.placeOrder(invoice);
161+
const duplicatedRsp = await payment.placeOrder(invoice);
162+
} catch (e) {
163+
expect(e.name).toBe('PlaceOrderError');
164+
expect(e.message).toBe(
165+
'Duplicated MerchantTradeNo, create order failed.'
166+
);
167+
}
168+
});
169+
});

0 commit comments

Comments
 (0)