| name | payment-adapters |
| description | Taiwan payment integration (台灣支付整合). Use when working with ECPay (綠界), NewebPay (藍新), HwaNan Bank (華南銀行), CTBC (中信), iCash Pay, or Happy Card payment services. Covers credit card (信用卡), virtual account (虛擬帳號), ATM, CVS payment (超商代碼/條碼), card binding (卡片綁定), installments (分期付款), recurring payments (訂閱付款), and NestJS integration. |
Taiwan Payment Adapters
This skill provides comprehensive guidance for using @rytass/payments-adapter-* packages to integrate Taiwan payment service providers.
Overview
All adapters implement the PaymentGateway interface from @rytass/payments, providing a unified API across different providers:
| Package | Provider | Description |
|---|
@rytass/payments-adapter-ecpay | ECPay (綠界科技) | Most popular Taiwan payment gateway |
@rytass/payments-adapter-newebpay | NewebPay (藍新金流) | Also known as EZPay, supports multiple payment methods |
@rytass/payments-adapter-hwanan | HwaNan Bank (華南銀行) | Bank-integrated payment service |
@rytass/payments-adapter-ctbc-micro-fast-pay | CTBC (中國信託) | CTBC Micro Fast Pay integration |
@rytass/payments-adapter-icash-pay | iCash Pay (愛金卡) | Unified group payment service |
@rytass/payments-adapter-happy-card | Happy Card (統一禮物卡) | Unified group gift card payment |
Base Interface (@rytass/payments)
All adapters share these core concepts:
PaymentGateway - Main interface for payment operations
interface PaymentGateway<OCM extends OrderCommitMessage> {
emitter: EventEmitter;
prepare<N extends OCM>(input: InputFromOrderCommitMessage<N>): Promise<Order<N>>;
query<OO extends Order>(id: string, options?: unknown): Promise<OO>;
}
Order Lifecycle - All orders follow this state machine:
INITED → PRE_COMMIT → [ASYNC_INFO_RETRIEVED] → COMMITTED/FAILED → REFUNDED
Payment Channels - Supported payment methods:
CREDIT_CARD - Credit card payments (信用卡)
VIRTUAL_ACCOUNT - Virtual account transfer (虛擬帳號)
WEB_ATM - Web ATM transfer (網路 ATM)
CVS_KIOSK - Convenience store code payment (超商代碼)
CVS_BARCODE - Convenience store barcode payment (超商條碼)
APPLE_PAY - Apple Pay integration
LINE_PAY - LINE Pay integration
Installation
npm install @rytass/payments
npm install @rytass/payments-adapter-ecpay
npm install @rytass/payments-adapter-newebpay
npm install @rytass/payments-adapter-hwanan
npm install @rytass/payments-adapter-ctbc-micro-fast-pay
npm install @rytass/payments-adapter-icash-pay
npm install @rytass/payments-adapter-happy-card
npm install @rytass/payments-nestjs-module
Quick Start
ECPay (綠界)
import { ECPayPayment, Channel, PaymentEvents } from '@rytass/payments-adapter-ecpay';
const gateway = new ECPayPayment({
merchantId: process.env.ECPAY_MERCHANT_ID!,
hashKey: process.env.ECPAY_HASH_KEY!,
hashIv: process.env.ECPAY_HASH_IV!,
serverHost: 'https://your-domain.com',
withServer: true,
});
gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, (order) => {
console.log('Payment successful:', order.id, order.totalPrice);
});
gateway.emitter.on(PaymentEvents.ORDER_FAILED, (failure) => {
console.error('Payment failed:', failure.code, failure.message);
});
const order = await gateway.prepare({
channel: Channel.CREDIT_CARD,
items: [
{ name: 'Product A', unitPrice: 1000, quantity: 2 },
{ name: 'Product B', unitPrice: 500, quantity: 1 },
],
clientBackUrl: 'https://your-site.com/payment/return',
});
const checkoutUrl = order.checkoutURL;
const autoSubmitHtml = order.formHTML;
const formData = order.form;
NewebPay (藍新)
import { NewebPayPayment, NewebPaymentChannel } from '@rytass/payments-adapter-newebpay';
const gateway = new NewebPayPayment({
merchantId: process.env.NEWEBPAY_MERCHANT_ID!,
aesKey: process.env.NEWEBPAY_AES_KEY!,
aesIv: process.env.NEWEBPAY_AES_IV!,
serverHost: 'https://your-domain.com',
withServer: true,
});
const order = await gateway.prepare({
channel: NewebPaymentChannel.CREDIT,
items: [{ name: 'Service Fee', unitPrice: 3000, quantity: 1 }],
});
console.log('Checkout URL:', order.checkoutURL);
HwaNan Bank (華南銀行)
import { HwaNanPayment, HwaNanPaymentChannel } from '@rytass/payments-adapter-hwanan';
const gateway = new HwaNanPayment({
merchantId: process.env.HWANAN_MERCHANT_ID!,
terminalId: process.env.HWANAN_TERMINAL_ID!,
merID: process.env.HWANAN_MER_ID!,
identifier: process.env.HWANAN_IDENTIFIER!,
merchantName: 'My Store Name',
withServer: true,
});
const order = await gateway.prepare({
channel: HwaNanPaymentChannel.CREDIT_CARD,
items: [{ name: 'Purchase', unitPrice: 5000, quantity: 1 }],
});
CTBC (中信)
import { CTBCPayment, Channel } from '@rytass/payments-adapter-ctbc-micro-fast-pay';
const gateway = new CTBCPayment({
merchantId: process.env.CTBC_MERCHANT_ID!,
merId: process.env.CTBC_MER_ID!,
txnKey: process.env.CTBC_TXN_KEY!,
terminalId: process.env.CTBC_TERMINAL_ID!,
withServer: true,
});
const order = await gateway.prepare({
channel: Channel.CREDIT_CARD,
items: [{ name: 'Item', unitPrice: 2000, quantity: 1 }],
});
iCash Pay (愛金卡)
import { ICashPayPayment, ICashPayBaseUrls } from '@rytass/payments-adapter-icash-pay';
const gateway = new ICashPayPayment({
baseUrl: ICashPayBaseUrls.DEVELOPMENT,
merchantId: process.env.ICASH_PAY_MERCHANT_ID!,
clientPrivateKey: process.env.ICASH_PAY_CLIENT_PRIVATE_KEY!,
serverPublicKey: process.env.ICASH_PAY_SERVER_PUBLIC_KEY!,
aesKey: {
id: process.env.ICASH_PAY_AES_KEY_ID!,
key: process.env.ICASH_PAY_AES_KEY!,
iv: process.env.ICASH_PAY_AES_IV!,
},
});
const order = await gateway.prepare({
id: 'ORDER-001',
storeName: 'My Coffee Shop',
barcode: '280012345678901234',
amount: 150,
items: [{ name: 'Americano', unitPrice: 120, quantity: 1 }],
collectedAmount: 150,
});
const result = await order.commit();
Happy Card (統一禮物卡)
import { HappyCardPayment, HappyCardRecordType } from '@rytass/payments-adapter-happy-card';
const gateway = new HappyCardPayment({
cSource: process.env.HAPPY_CARD_C_SOURCE!,
key: process.env.HAPPY_CARD_KEY!,
});
const [records, productType] = await gateway.getCardBalance('HC1234567890123456', true);
console.log('Product type:', productType);
records.forEach(record => {
console.log(`Record ${record.id}: Type=${record.type}, Amount=${record.amount}`);
});
const order = await gateway.prepare({
id: 'HAPPY-ORDER-001',
cardSerial: 'HC1234567890123456',
items: [{ name: 'Coffee', unitPrice: 150, quantity: 2 }],
useRecords: [
{ id: 12345, type: HappyCardRecordType.BONUS, amount: 300 },
],
});
const result = await order.commit();
Common Patterns
Event-Driven Architecture
All payment adapters use EventEmitter for asynchronous notifications:
import { PaymentEvents } from '@rytass/payments';
gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, (order) => {
console.log('Order ID:', order.id);
console.log('Total Price:', order.totalPrice);
console.log('Committed At:', order.committedAt);
console.log('Additional Info:', order.additionalInfo);
updateOrderStatus(order.id, 'PAID');
sendReceiptEmail(order);
});
gateway.emitter.on(PaymentEvents.ORDER_FAILED, (failure) => {
console.error('Failed Order ID:', failure.id);
console.error('Error Code:', failure.code);
console.error('Error Message:', failure.message);
updateOrderStatus(failure.id, 'FAILED');
notifyCustomer(failure);
});
gateway.emitter.on(PaymentEvents.ORDER_INFO_RETRIEVED, (order) => {
if (order.asyncInfo?.channel === Channel.VIRTUAL_ACCOUNT) {
console.log('Bank Code:', order.asyncInfo.bankCode);
console.log('Account Number:', order.asyncInfo.account);
console.log('Expires At:', order.asyncInfo.expiredAt);
sendVirtualAccountEmail({
bankCode: order.asyncInfo.bankCode,
account: order.asyncInfo.account,
amount: order.totalPrice,
expiredAt: order.asyncInfo.expiredAt,
});
}
if (order.asyncInfo?.channel === Channel.CVS_KIOSK) {
console.log('Payment Code:', order.asyncInfo.paymentCode);
console.log('Expires At:', order.asyncInfo.expiredAt);
sendCVSCodeSMS(order.asyncInfo.paymentCode);
}
});
gateway.emitter.on(PaymentEvents.CARD_BOUND, (bindRequest) => {
console.log('Member ID:', bindRequest.memberId);
console.log('Card ID:', bindRequest.cardId);
console.log('Card Suffix:', bindRequest.cardNumberSuffix);
saveCardToDatabase({
memberId: bindRequest.memberId,
cardId: bindRequest.cardId,
lastFourDigits: bindRequest.cardNumberSuffix,
});
});
gateway.emitter.on(PaymentEvents.CARD_BINDING_FAILED, (bindRequest) => {
console.error('Binding failed for member:', bindRequest.memberId);
console.error('Error:', bindRequest.failedMessage);
});
Order Lifecycle
Understanding the order state machine:
import { OrderState } from '@rytass/payments';
const order = await gateway.prepare({ ... });
console.log(order.state);
console.log(order.state);
if (order.state === OrderState.FAILED) {
console.error('Reason:', order.failedMessage?.message);
}
if (order.state === OrderState.COMMITTED) {
await order.refund();
await order.refund(500);
}
Credit Card Payment Flow
Complete flow from preparation to completion:
const order = await gateway.prepare({
channel: Channel.CREDIT_CARD,
items: [{ name: 'Product', unitPrice: 1000, quantity: 1 }],
clientBackUrl: 'https://your-site.com/payment/return',
});
res.redirect(order.checkoutURL);
res.send(order.formHTML);
res.render('checkout', { formData: order.form });
gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, (order) => {
console.log('Payment completed:', order.id);
console.log('Card info:', {
lastFour: order.additionalInfo.card4Number,
authCode: order.additionalInfo.authCode,
eci: order.additionalInfo.eci,
});
});
const updatedOrder = await gateway.query(order.id);
console.log('Final state:', updatedOrder.state);
console.log('Is paid:', updatedOrder.state === OrderState.COMMITTED);
Virtual Account Payment Flow
Async payment with bank transfer:
const order = await gateway.prepare({
channel: Channel.VIRTUAL_ACCOUNT,
items: [{ name: 'Bulk Purchase', unitPrice: 50000, quantity: 1 }],
virtualAccountExpireDays: 7,
});
gateway.emitter.on(PaymentEvents.ORDER_INFO_RETRIEVED, (order) => {
if (order.asyncInfo?.channel === Channel.VIRTUAL_ACCOUNT) {
const { bankCode, account, expiredAt } = order.asyncInfo;
console.log('=== Virtual Account Info ===');
console.log('Bank Code:', bankCode);
console.log('Account:', account);
console.log('Amount:', order.totalPrice);
console.log('Expires:', expiredAt);
sendVirtualAccountNotification({
orderId: order.id,
bankCode,
account,
amount: order.totalPrice,
expiredAt,
});
}
});
gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, (order) => {
console.log('Virtual account payment received!');
console.log('Paid from bank:', order.additionalInfo.buyerBankCode);
console.log('Account:', order.additionalInfo.buyerAccountNumber);
processOrder(order.id);
});
CVS Payment Flow
Convenience store code/barcode payment:
const cvsOrder = await gateway.prepare({
channel: Channel.CVS_KIOSK,
items: [{ name: 'Online Course', unitPrice: 1200, quantity: 1 }],
});
gateway.emitter.on(PaymentEvents.ORDER_INFO_RETRIEVED, (order) => {
if (order.asyncInfo?.channel === Channel.CVS_KIOSK) {
console.log('Payment Code:', order.asyncInfo.paymentCode);
console.log('Expires:', order.asyncInfo.expiredAt);
sendCVSCodeSMS(order.asyncInfo.paymentCode);
}
});
const barcodeOrder = await gateway.prepare({
channel: Channel.CVS_BARCODE,
items: [{ name: 'Magazine', unitPrice: 280, quantity: 1 }],
});
gateway.emitter.on(PaymentEvents.ORDER_INFO_RETRIEVED, (order) => {
if (order.asyncInfo?.channel === Channel.CVS_BARCODE) {
const [barcode1, barcode2, barcode3] = order.asyncInfo.barcodes;
console.log('Barcode 1:', barcode1);
console.log('Barcode 2:', barcode2);
console.log('Barcode 3:', barcode3);
sendBarcodesEmail([barcode1, barcode2, barcode3]);
}
});
Query Order Status
Check payment status at any time:
const order = await gateway.query('ORDER-2024-001');
console.log('Order State:', order.state);
console.log('Created At:', order.createdAt);
console.log('Committed At:', order.committedAt);
console.log('Total Price:', order.totalPrice);
if (order.state === OrderState.COMMITTED) {
console.log('Payment completed!');
console.log('Additional Info:', order.additionalInfo);
}
if (order.state === OrderState.FAILED) {
console.error('Payment failed!');
console.error('Error Code:', order.failedMessage?.code);
console.error('Error Message:', order.failedMessage?.message);
}
if (order.state === OrderState.PRE_COMMIT) {
console.log('Waiting for payment...');
}
if (order.state === OrderState.ASYNC_INFO_RETRIEVED) {
console.log('Virtual account/CVS code generated, waiting for customer payment');
console.log('Payment Info:', order.asyncInfo);
}
Advanced Features
Card Binding (卡片綁定)
Bind credit cards for future one-click payments:
import { PaymentEvents } from '@rytass/payments';
const bindRequest = await gateway.prepareBindCard('MEMBER_123');
console.log('Binding URL:', bindRequest.checkoutURL);
res.redirect(bindRequest.checkoutURL);
gateway.emitter.on(PaymentEvents.CARD_BOUND, (bindRequest) => {
console.log('Card bound successfully!');
console.log('Member ID:', bindRequest.memberId);
console.log('Card ID:', bindRequest.cardId);
console.log('Last 4 digits:', bindRequest.cardNumberSuffix);
await db.cards.create({
memberId: bindRequest.memberId,
cardId: bindRequest.cardId,
lastFourDigits: bindRequest.cardNumberSuffix,
createdAt: new Date(),
});
});
const boundOrder = await gateway.checkoutWithBoundCard({
memberId: 'MEMBER_123',
cardId: 'CARD_ID_FROM_BINDING',
items: [{ name: 'Subscription', unitPrice: 999, quantity: 1 }],
orderId: 'SUBSCRIPTION-001',
});
console.log('Payment result:', boundOrder);
gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, async (order) => {
const bindRequest = await gateway.bindCardWithTransaction(
'MEMBER_123',
order.platformTradeNumber,
order.id
);
console.log('Card bound with transaction:', bindRequest.cardId);
});
Memory Card vs Card Binding
Understanding the difference:
const memoryPayment = new ECPayPayment({
merchantId: 'YOUR_MERCHANT_ID',
hashKey: 'YOUR_KEY',
hashIv: 'YOUR_IV',
memory: true,
});
const bindingPayment = new ECPayPayment({
merchantId: 'YOUR_MERCHANT_ID',
hashKey: 'YOUR_KEY',
hashIv: 'YOUR_IV',
memory: false,
});
Installment Payments (分期付款)
Credit card installment options:
const installmentPayment = new ECPayPayment({
merchantId: 'YOUR_MERCHANT_ID',
hashKey: 'YOUR_KEY',
hashIv: 'YOUR_IV',
installments: '3,6,12,18,24',
});
const order = await installmentPayment.prepare({
channel: Channel.CREDIT_CARD,
items: [{ name: 'Laptop', unitPrice: 30000, quantity: 1 }],
});
const ctbcPayment = new CTBCPayment({
merchantId: 'YOUR_MERCHANT_ID',
merId: 'YOUR_MER_ID',
txnKey: 'YOUR_KEY',
terminalId: 'YOUR_TERMINAL',
installmentCount: 12,
});
const hwananPayment = new HwaNanPayment({
merchantId: 'YOUR_MERCHANT_ID',
terminalId: 'YOUR_TERMINAL',
merID: 'YOUR_MER_ID',
identifier: 'YOUR_IDENTIFIER',
installmentAmount: 5000,
});
Recurring Payments (訂閱付款)
Set up periodic payments:
import { PaymentPeriodType } from '@rytass/payments';
const recurringPayment = new ECPayPayment({
merchantId: 'YOUR_MERCHANT_ID',
hashKey: 'YOUR_KEY',
hashIv: 'YOUR_IV',
period: {
amountPerPeriod: 999,
type: PaymentPeriodType.MONTH,
frequency: 1,
times: 12,
},
});
const order = await recurringPayment.prepare({
channel: Channel.CREDIT_CARD,
items: [{ name: 'Monthly Subscription', unitPrice: 999, quantity: 1 }],
});
period: {
amountPerPeriod: 50,
type: PaymentPeriodType.DAY,
frequency: 1,
times: 30,
}
period: {
amountPerPeriod: 200,
type: PaymentPeriodType.DAY,
frequency: 7,
times: 8,
}
period: {
amountPerPeriod: 3000,
type: PaymentPeriodType.MONTH,
frequency: 3,
times: 4,
}
Built-in Server & Ngrok
Handle callbacks with built-in server:
const gateway = new ECPayPayment({
merchantId: 'YOUR_MERCHANT_ID',
hashKey: 'YOUR_KEY',
hashIv: 'YOUR_IV',
serverHost: 'https://your-domain.com',
withServer: true,
});
const gateway = new ECPayPayment({
merchantId: 'YOUR_MERCHANT_ID',
hashKey: 'YOUR_KEY',
hashIv: 'YOUR_IV',
withServer: 'ngrok',
});
gateway.emitter.on(PaymentEvents.SERVER_LISTENED, (info) => {
console.log('Server listening on:', info.url);
});
const gateway = new ECPayPayment({
merchantId: 'YOUR_MERCHANT_ID',
hashKey: 'YOUR_KEY',
hashIv: 'YOUR_IV',
withServer: false,
});
app.post('/payment/callback', (req, res) => {
gateway.defaultServerListener(req, res);
});
NestJS Integration
Complete integration with NestJS dependency injection:
Basic Setup
import { Module } from '@nestjs/common';
import { PaymentsModule } from '@rytass/payments-nestjs-module';
import { ECPayPayment } from '@rytass/payments-adapter-ecpay';
@Module({
imports: [
PaymentsModule.forRoot({
paymentGateway: new ECPayPayment({
merchantId: process.env.ECPAY_MERCHANT_ID!,
hashKey: process.env.ECPAY_HASH_KEY!,
hashIv: process.env.ECPAY_HASH_IV!,
serverHost: process.env.SERVER_HOST!,
withServer: true,
}),
}),
],
})
export class AppModule {}
Async Configuration
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PaymentsModule } from '@rytass/payments-nestjs-module';
import { ECPayPayment } from '@rytass/payments-adapter-ecpay';
@Module({
imports: [
ConfigModule.forRoot(),
PaymentsModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
paymentGateway: new ECPayPayment({
merchantId: config.get('ECPAY_MERCHANT_ID')!,
hashKey: config.get('ECPAY_HASH_KEY')!,
hashIv: config.get('ECPAY_HASH_IV')!,
serverHost: config.get('SERVER_HOST')!,
withServer: true,
}),
}),
}),
],
})
export class AppModule {}
Using in Services
import { Injectable, Inject, Logger } from '@nestjs/common';
import { PaymentGateway, Channel, PaymentEvents, OrderState } from '@rytass/payments';
import { PAYMENTS_GATEWAY } from '@rytass/payments-nestjs-module';
@Injectable()
export class PaymentService {
private readonly logger = new Logger(PaymentService.name);
constructor(
@Inject(PAYMENTS_GATEWAY)
private readonly paymentGateway: PaymentGateway,
) {
this.setupEventListeners();
}
private setupEventListeners() {
this.paymentGateway.emitter?.on(PaymentEvents.ORDER_COMMITTED, (message) => {
this.logger.log(`Payment committed: ${message.id}`);
this.handlePaymentSuccess(message);
});
this.paymentGateway.emitter?.on(PaymentEvents.ORDER_FAILED, (failure) => {
this.logger.error(`Payment failed: ${failure.code} - ${failure.message}`);
this.handlePaymentFailure(failure);
});
}
async createPayment(data: {
itemName: string;
amount: number;
quantity?: number;
}) {
const order = await this.paymentGateway.prepare({
channel: Channel.CREDIT_CARD,
items: [{
name: data.itemName,
unitPrice: data.amount,
quantity: data.quantity || 1,
}],
});
return {
orderId: order.id,
checkoutUrl: order.checkoutURL,
totalAmount: order.totalPrice,
};
}
async queryPayment(orderId: string) {
const order = await this.paymentGateway.query(orderId);
return {
orderId: order.id,
state: order.state,
isPaid: order.state === OrderState.COMMITTED,
totalAmount: order.totalPrice,
committedAt: order.committedAt,
};
}
private async handlePaymentSuccess(message: any) {
}
private async handlePaymentFailure(failure: any) {
}
}
Built-in Endpoints
PaymentsModule automatically provides these endpoints:
| Method | Endpoint | Description |
|---|
GET | /payments/checkout/:orderNo | Payment checkout page |
POST | /payments/callbacks | Payment gateway callbacks |
These endpoints are automatically marked as public (no authentication required).
Multiple Gateways
Use multiple payment gateways in one application:
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ECPayPayment } from '@rytass/payments-adapter-ecpay';
import { NewebPayPayment } from '@rytass/payments-adapter-newebpay';
@Module({
providers: [
{
provide: 'ECPAY_GATEWAY',
useFactory: (config: ConfigService) => new ECPayPayment({
merchantId: config.get('ECPAY_MERCHANT_ID'),
hashKey: config.get('ECPAY_HASH_KEY'),
hashIv: config.get('ECPAY_HASH_IV'),
withServer: true,
}),
inject: [ConfigService],
},
{
provide: 'NEWEBPAY_GATEWAY',
useFactory: (config: ConfigService) => new NewebPayPayment({
merchantId: config.get('NEWEBPAY_MERCHANT_ID'),
aesKey: config.get('NEWEBPAY_AES_KEY'),
aesIv: config.get('NEWEBPAY_AES_IV'),
withServer: true,
}),
inject: [ConfigService],
},
],
exports: ['ECPAY_GATEWAY', 'NEWEBPAY_GATEWAY'],
})
export class PaymentsConfigModule {}
@Injectable()
export class MultiGatewayService {
constructor(
@Inject('ECPAY_GATEWAY') private ecpay: any,
@Inject('NEWEBPAY_GATEWAY') private newebpay: any,
) {}
async createPayment(provider: 'ecpay' | 'newebpay', data: any) {
const gateway = provider === 'ecpay' ? this.ecpay : this.newebpay;
return await gateway.prepare(data);
}
}
Feature Comparison
| Feature | ECPay | NewebPay | HwaNan | CTBC | iCash Pay | Happy Card |
|---|
| Credit Card | Yes | Yes | Yes | Yes | Yes | No |
| Installments | Yes | Yes | Yes | Yes | No | No |
| Virtual Account | Yes | Yes | No | Yes | No | No |
| Web ATM | Yes | Yes | No | No | No | No |
| CVS Code | Yes | Yes | No | Yes | No | No |
| CVS Barcode | Yes | No | No | Yes | No | No |
| Apple Pay | Yes | No | No | Yes | No | No |
| Card Binding | Yes | Yes | No | Yes | No | No |
| Memory Card | Yes | Yes | No | No | No | No |
| Recurring Payment | Yes | No | No | No | No | No |
| Built-in Server | Yes | Yes | Yes | Yes | No | No |
| Ngrok Support | Yes | Yes | Yes | Yes | No | No |
| Barcode Scan | No | No | No | No | Yes | No |
| Gift Card Balance | No | No | No | No | No | Yes |
| Bonus Points | No | No | No | No | No | Yes |
| Multi-language | No | Yes | Yes | No | No | No |
| Refund | Partial | Partial | No | No | Yes | Yes |
Signature Methods
| Provider | Method | Algorithm |
|---|
| ECPay | SHA256 Hash | SHA256 with URL encoding |
| NewebPay | AES Encryption | AES-256-CBC |
| HwaNan | MAC Signature | MD5/SHA256 |
| CTBC | MAC/TXN | Proprietary algorithm |
| iCash Pay | RSA + AES | RSA-SHA256 + AES-256-CBC |
| Happy Card | MD5 Hash | MD5 signature |
Environment Configuration
Development vs Production
All adapters support environment switching:
import { ECPayPayment } from '@rytass/payments-adapter-ecpay';
const devGateway = new ECPayPayment();
const prodGateway = new ECPayPayment({
baseUrl: 'https://payment.ecpay.com.tw',
merchantId: 'YOUR_PROD_MERCHANT_ID',
hashKey: 'YOUR_PROD_HASH_KEY',
hashIv: 'YOUR_PROD_HASH_IV',
});
import { NewebPayPayment } from '@rytass/payments-adapter-newebpay';
const prodGateway = new NewebPayPayment({
baseUrl: 'https://core.newebpay.com',
merchantId: 'YOUR_PROD_MERCHANT_ID',
aesKey: 'YOUR_PROD_AES_KEY',
aesIv: 'YOUR_PROD_AES_IV',
});
import { HappyCardPayment, HappyCardBaseUrls } from '@rytass/payments-adapter-happy-card';
const prodGateway = new HappyCardPayment({
baseUrl: HappyCardBaseUrls.PRODUCTION,
cSource: 'YOUR_PROD_C_SOURCE',
key: 'YOUR_PROD_KEY',
});
Environment Variables
NODE_ENV=production
ECPAY_MERCHANT_ID=your_merchant_id
ECPAY_HASH_KEY=your_hash_key
ECPAY_HASH_IV=your_hash_iv
NEWEBPAY_MERCHANT_ID=your_merchant_id
NEWEBPAY_AES_KEY=your_aes_key
NEWEBPAY_AES_IV=your_aes_iv
HWANAN_MERCHANT_ID=your_merchant_id
HWANAN_TERMINAL_ID=your_terminal_id
HWANAN_MER_ID=your_mer_id
HWANAN_IDENTIFIER=your_identifier
CTBC_MERCHANT_ID=your_merchant_id
CTBC_MER_ID=your_mer_id
CTBC_TXN_KEY=your_txn_key
CTBC_TERMINAL_ID=your_terminal_id
ICASH_PAY_MERCHANT_ID=your_merchant_id
ICASH_PAY_CLIENT_PRIVATE_KEY=your_private_key
ICASH_PAY_SERVER_PUBLIC_KEY=server_public_key
ICASH_PAY_AES_KEY_ID=your_aes_key_id
ICASH_PAY_AES_KEY=your_32_char_aes_key
ICASH_PAY_AES_IV=your_16_char_aes_iv
HAPPY_CARD_C_SOURCE=your_c_source
HAPPY_CARD_KEY=your_key
SERVER_HOST=https://your-domain.com
Troubleshooting
Common Issues
1. Signature Verification Failed
Symptoms:
- Payment fails immediately
- Error message contains "signature", "checksum", or "verification"
- Callback returns error
Solutions:
console.log('Merchant ID:', process.env.ECPAY_MERCHANT_ID);
console.log('Hash Key length:', process.env.ECPAY_HASH_KEY?.length);
console.log('Hash IV length:', process.env.ECPAY_HASH_IV?.length);
ECPAY_HASH_KEY=your_key_here # ❌ Trailing space
ECPAY_HASH_KEY=your_key_here # ✅ No trailing space
const gateway = new ECPayPayment({
baseUrl: 'https://payment-stage.ecpay.com.tw',
});
2. Callback Not Received
Symptoms:
- Payment completes but ORDER_COMMITTED never fires
- Order stuck in PRE_COMMIT state
Solutions:
const gateway = new ECPayPayment({
withServer: true,
serverHost: 'https://your-domain.com',
});
gateway.emitter.on(PaymentEvents.SERVER_LISTENED, (info) => {
console.log('Server listening:', info.url);
});
curl -X POST https:
-H "Content-Type: application/json" \
-d '{"test": "data"}'
3. Ngrok Tunnel Issues
Symptoms:
- Ngrok tunnel fails to start
- Ngrok URL changes frequently
Solutions:
process.env.NGROK_AUTHTOKEN = 'your_ngrok_auth_token';
const gateway = new ECPayPayment({
withServer: 'ngrok',
});
const gateway = new ECPayPayment({
withServer: 'ngrok',
serverHost: 'your-static-subdomain.ngrok.io',
});
4. Order ID Conflicts
Symptoms:
- Error: "Order ID already exists"
- Cannot create new orders
Solutions:
const order = await gateway.prepare({
id: `ORDER-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
items: [...],
});
const order = await gateway.prepare({
items: [...],
});
5. Amount Mismatch
Symptoms:
- Payment amount different from expected
- Calculation errors
Solutions:
const items = [
{ name: 'A', unitPrice: 100, quantity: 2 },
{ name: 'B', unitPrice: 50, quantity: 3 },
];
const order = await gateway.prepare({ items });
console.log('Total Price:', order.totalPrice);
gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, (order) => {
console.log('Charged amount:', order.additionalInfo.amount);
console.log('Expected amount:', order.totalPrice);
if (order.additionalInfo.amount !== order.totalPrice) {
console.error('Amount mismatch!');
}
});
6. Event Listeners Not Firing
Symptoms:
- PaymentEvents.ORDER_COMMITTED not triggered
- No event handlers executing
Solutions:
gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, handler);
gateway.emitter.on(PaymentEvents.ORDER_FAILED, handler);
const order = await gateway.prepare({...});
if (!gateway.emitter) {
console.error('Gateway has no emitter!');
}
gateway.emitter.once(PaymentEvents.ORDER_COMMITTED, handler);
gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, (order) => {
try {
} catch (error) {
console.error('Handler error:', error);
}
});
7. Card Binding Failures
Symptoms:
- CARD_BINDING_FAILED event fires
- Cannot bind card
Solutions:
const gateway = new ECPayPayment({
memory: false,
});
gateway.emitter.on(PaymentEvents.CARD_BINDING_FAILED, (req) => {
if (req.failedMessage?.code === '10100112') {
console.log('Card already bound - this is OK');
}
});
gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, async (order) => {
const bindRequest = await gateway.bindCardWithTransaction(
memberId,
order.platformTradeNumber,
order.id
);
});
CTBC Advanced Features (CTBC 進階功能)
POS API Utility Functions
CTBC adapter exports utility functions for direct POS API operations:
import {
posApiQuery,
posApiRefund,
posApiCancelRefund,
posApiReversal,
posApiCapRev,
posApiSmartCancelOrRefund,
getPosNextActionFromInquiry,
CTBCPosApiConfig,
} from '@rytass/payments-adapter-ctbc-micro-fast-pay';
const posConfig: CTBCPosApiConfig = {
URL: 'https://ccapi.ctbcbank.com',
MacKey: 'YOUR_MAC_KEY',
};
const queryResult = await posApiQuery(posConfig, {
MERID: 'YOUR_MERID',
'LID-M': 'ORDER-001',
Tx_ATTRIBUTE: 'TX_AUTH',
});
const { action, response, inquiry } = await posApiSmartCancelOrRefund(posConfig, {
MERID: 'YOUR_MERID',
'LID-M': 'ORDER-001',
XID: 'TRANSACTION_XID',
AuthCode: 'AUTH_CODE',
OrgAmt: '1000',
PurchAmt: '1000',
currency: 'TWD',
exponent: '0',
});
console.log('Action taken:', action);
AMEX SOAP API Utility Functions
For American Express card processing:
import {
amexInquiry,
amexRefund,
amexCancelRefund,
amexAuthRev,
amexCapRev,
amexSmartCancelOrRefund,
getAmexNextActionFromInquiry,
CTBCAmexConfig,
} from '@rytass/payments-adapter-ctbc-micro-fast-pay';
const amexConfig: CTBCAmexConfig = {
wsdlUrl: 'https://amex.ctbcbank.com/wsdl',
timeout: 30000,
sslOptions: {
rejectUnauthorized: true,
},
};
const amexResult = await amexInquiry(amexConfig, {
merId: 'YOUR_MERID',
lidm: 'ORDER-001',
xid: 'TRANSACTION_XID',
IN_MAC_KEY: 'YOUR_MAC_KEY',
});
const { action, response } = await amexSmartCancelOrRefund(amexConfig, {
merId: 'YOUR_MERID',
xid: 'TRANSACTION_XID',
lidm: 'ORDER-001',
purchAmt: 1000,
orgAmt: 1000,
IN_MAC_KEY: 'YOUR_MAC_KEY',
});
CTBC Configuration Options
Complete configuration for CTBC gateway:
interface CTBCPaymentOptions {
merchantId: string;
merchantName?: string;
merId: string;
txnKey: string;
terminalId: string;
baseUrl?: string;
isAmex?: boolean;
withServer?: boolean | 'ngrok';
serverHost?: string;
callbackPath?: string;
checkoutPath?: string;
bindCardPath?: string;
boundCardPath?: string;
boundCardCheckoutResultPath?: string;
orderCache?: OrderCache;
orderCacheTTL?: number;
bindCardRequestsCache?: BindCardRequestCache;
bindCardRequestsCacheTTL?: number;
serverListener?: (req, res) => void;
onServerListen?: () => void;
onCommit?: (order) => void;
}
Happy Card Advanced Features (統一禮物卡進階功能)
Get Card Balance
Query card balance before making payment:
import { HappyCardPayment, HappyCardRecordType } from '@rytass/payments-adapter-happy-card';
const gateway = new HappyCardPayment({
cSource: process.env.HAPPY_CARD_C_SOURCE!,
key: process.env.HAPPY_CARD_KEY!,
});
const [balance, productType] = await gateway.getCardBalance('HC1234567890123456', false);
console.log('Total balance:', balance);
console.log('Product type:', productType);
const [records, productType2] = await gateway.getCardBalance('HC1234567890123456', true);
records.forEach(record => {
console.log(`Record ${record.id}: ${record.type === HappyCardRecordType.AMOUNT ? 'Amount' : 'Bonus'} = ${record.amount}`);
});
Happy Card Enums
enum HappyCardRecordType {
AMOUNT = 1,
BONUS = 2,
}
enum HappyCardProductType {
INVOICE_FIRST_HAPPY_CARD_GF = '1',
INVOICE_LATER_HAPPY_CARD_GS = '2',
INVOICE_FIRST_DIGITAL_GIFT_GF = '3',
INVOICE_LATER_DIGITAL_GIFT_GS = '4',
INVOICE_FIRST_PHYSICAL_GIFT_GF = '5',
INVOICE_LATER_PHYSICAL_GIFT_GS = '6',
}
enum HappyCardResultCode {
FAILED = '0',
SUCCESS = '1',
}
enum HappyCardBaseUrls {
PRODUCTION = 'https://prd-jp-posapi.azurewebsites.net/api/Pos',
DEVELOPMENT = 'https://uat-pos-api.azurewebsites.net/api/Pos',
}
Complete Happy Card Payment Flow
import {
HappyCardPayment,
HappyCardRecordType,
HappyCardBaseUrls,
} from '@rytass/payments-adapter-happy-card';
const gateway = new HappyCardPayment({
baseUrl: HappyCardBaseUrls.PRODUCTION,
cSource: process.env.HAPPY_CARD_C_SOURCE!,
key: process.env.HAPPY_CARD_KEY!,
});
const [records, productType] = await gateway.getCardBalance('HC1234567890123456', true);
console.log('Available records:', records);
const order = await gateway.prepare({
id: 'ORDER-001',
cardSerial: 'HC1234567890123456',
items: [
{ name: 'Coffee', unitPrice: 120, quantity: 1 },
{ name: 'Cake', unitPrice: 180, quantity: 1 },
],
useRecords: [
{ id: records[0].id, type: HappyCardRecordType.AMOUNT, amount: 200 },
{ id: records[1].id, type: HappyCardRecordType.BONUS, amount: 100 },
],
posTradeNo: 'POS001',
uniMemberGID: 'MEMBER_GID',
isIsland: false,
});
await order.commit();
console.log('Payment successful');
HwaNan Bank Advanced Features (華南銀行進階功能)
Installment Payments
HwaNan supports installment payments (分期付款):
import {
HwaNanPayment,
HwaNanTransactionType,
HwaNanAutoCapMode,
HwaNanCustomizePageType,
} from '@rytass/payments-adapter-hwanan';
const gateway = new HwaNanPayment({
merchantId: process.env.HWANAN_MERCHANT_ID!,
terminalId: process.env.HWANAN_TERMINAL_ID!,
merID: process.env.HWANAN_MER_ID!,
identifier: process.env.HWANAN_IDENTIFIER!,
merchantName: 'My Store',
withServer: true,
});
HwaNan Enums
enum HwaNanTransactionType {
ONE_TIME = 0,
INSTALLMENTS = 1,
}
enum HwaNanAutoCapMode {
MANUALLY = 0,
AUTO = 1,
}
enum HwaNanCustomizePageType {
ZH_TW = 1,
ZH_CN = 2,
EN_US = 3,
JA_JP = 4,
OTHER = 5,
}
enum HwaNanPaymentChannel {
CREDIT = 1,
}
HwaNan Configuration Options
interface HwaNanPaymentInitOptions {
merchantId: string;
terminalId: string;
merchantName: string;
merID: string;
identifier: string;
baseUrl?: string;
customizePageType?: HwaNanCustomizePageType;
customizePageVersion?: string;
serverHost?: string;
callbackPath?: string;
checkoutPath?: string;
withServer?: boolean | 'ngrok';
ttl?: number;
serverListener?: (req, res) => void;
onCommit?: (order) => void;
onServerListen?: () => void;
ordersCache?: OrdersCache;
}
Detailed Documentation
For complete API reference and advanced usage: