payment-adapters

Taiwan Payment Adapters

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "payment-adapters" with this command: npx skills add rytass/utils/rytass-utils-payment-adapters

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

Install base package

npm install @rytass/payments

Choose the adapter for your provider

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

For NestJS integration

npm install @rytass/payments-nestjs-module

Quick Start

ECPay (綠界)

import { ECPayPayment, Channel, PaymentEvents } from '@rytass/payments-adapter-ecpay';

// Initialize gateway 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', // Your server URL for callbacks withServer: true, // Enable built-in callback server });

// Listen to payment events 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); });

// Prepare payment order 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', // User return URL });

// Get checkout URL (3 ways) const checkoutUrl = order.checkoutURL; // Built-in server URL (recommended) const autoSubmitHtml = order.formHTML; // HTML with auto-submit form const formData = order.form; // Raw form data for custom implementation

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, // 注意:使用 NewebPaymentChannel 而非 Channel 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, // 注意:使用 HwaNanPaymentChannel 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!, }, });

// iCash Pay uses barcode scanning const order = await gateway.prepare({ id: 'ORDER-001', storeName: 'My Coffee Shop', barcode: '280012345678901234', // 18-digit barcode from customer 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!, });

// Query card balance first (get detailed records) 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}); });

// Make payment using card 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(); // Immediate deduction

Common Patterns

Event-Driven Architecture

All payment adapters use EventEmitter for asynchronous notifications:

import { PaymentEvents } from '@rytass/payments';

// Payment success 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); // Card info, bank code, etc.

// Update database, send notifications, etc. updateOrderStatus(order.id, 'PAID'); sendReceiptEmail(order); });

// Payment failure 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);

// Handle failure updateOrderStatus(failure.id, 'FAILED'); notifyCustomer(failure); });

// Async payment info retrieved (Virtual Account, CVS codes) 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);

// Send virtual account info to customer
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);

// Send CVS code to customer
sendCVSCodeSMS(order.asyncInfo.paymentCode);

} });

// Card binding success 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);

// Save card info to database saveCardToDatabase({ memberId: bindRequest.memberId, cardId: bindRequest.cardId, lastFourDigits: bindRequest.cardNumberSuffix, }); });

// Card binding failure 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';

// 1. INITED - Order object created but not sent to gateway const order = await gateway.prepare({ ... }); console.log(order.state); // OrderState.INITED

// Order becomes PRE_COMMIT after prepare() returns console.log(order.state); // OrderState.PRE_COMMIT

// 2. For sync payments (Credit Card): // User completes payment → Gateway sends callback → commit() called → COMMITTED

// 3. For async payments (Virtual Account, CVS): // prepare() → ASYNC_INFO_RETRIEVED → User pays → COMMITTED

// 4. Payment can fail at any time → FAILED // Check for failure: if (order.state === OrderState.FAILED) { console.error('Reason:', order.failedMessage?.message); }

// 5. After committed, can refund → REFUNDED (if supported) if (order.state === OrderState.COMMITTED) { await order.refund(); // Full refund await order.refund(500); // Partial refund (if supported) }

Credit Card Payment Flow

Complete flow from preparation to completion:

// 1. Prepare order const order = await gateway.prepare({ channel: Channel.CREDIT_CARD, items: [{ name: 'Product', unitPrice: 1000, quantity: 1 }], clientBackUrl: 'https://your-site.com/payment/return', });

// 2. Three ways to get checkout: // Option A: Built-in server URL (recommended if withServer: true) res.redirect(order.checkoutURL);

// Option B: Auto-submit HTML res.send(order.formHTML);

// Option C: Custom form implementation res.render('checkout', { formData: order.form });

// 3. User completes payment on payment gateway page

// 4. Gateway sends callback to your server // If withServer: true, this is handled automatically // The ORDER_COMMITTED event will fire:

gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, (order) => { // Order is now paid! console.log('Payment completed:', order.id); console.log('Card info:', { lastFour: order.additionalInfo.card4Number, authCode: order.additionalInfo.authCode, eci: order.additionalInfo.eci, }); });

// 5. User is redirected to clientBackUrl // You can query the order status: 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:

// 1. Prepare virtual account order const order = await gateway.prepare({ channel: Channel.VIRTUAL_ACCOUNT, items: [{ name: 'Bulk Purchase', unitPrice: 50000, quantity: 1 }], virtualAccountExpireDays: 7, // Account expires in 7 days });

// 2. Listen for virtual account info 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); // e.g., '004' (Taiwan Bank)
console.log('Account:', account); // e.g., '88801234567890'
console.log('Amount:', order.totalPrice);
console.log('Expires:', expiredAt);

// Send to customer via email/SMS
sendVirtualAccountNotification({
  orderId: order.id,
  bankCode,
  account,
  amount: order.totalPrice,
  expiredAt,
});

} });

// 3. User makes bank transfer

// 4. When payment is received, ORDER_COMMITTED event fires 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);

// Fulfill order processOrder(order.id); });

CVS Payment Flow

Convenience store code/barcode payment:

// CVS Kiosk Code 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); // e.g., '1234567890' console.log('Expires:', order.asyncInfo.expiredAt);

// Customer can pay at 7-Eleven, FamilyMart, OK Mart, Hi-Life
sendCVSCodeSMS(order.asyncInfo.paymentCode);

} });

// CVS Barcode Payment (超商條碼) 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);

// Send barcodes to customer (display on page or send via email)
sendBarcodesEmail([barcode1, barcode2, barcode3]);

} });

Query Order Status

Check payment status at any time:

// Query by order ID 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);

// Check state 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';

// Step 1: Prepare card binding request const bindRequest = await gateway.prepareBindCard('MEMBER_123');

console.log('Binding URL:', bindRequest.checkoutURL);

// Redirect user to binding URL res.redirect(bindRequest.checkoutURL);

// Step 2: Listen for binding result gateway.emitter.on(PaymentEvents.CARD_BOUND, (bindRequest) => { console.log('Card bound successfully!'); console.log('Member ID:', bindRequest.memberId); console.log('Card ID:', bindRequest.cardId); // Save this for future use console.log('Last 4 digits:', bindRequest.cardNumberSuffix);

// Save to database await db.cards.create({ memberId: bindRequest.memberId, cardId: bindRequest.cardId, lastFourDigits: bindRequest.cardNumberSuffix, createdAt: new Date(), }); });

// Step 3: Use bound card for payment const boundOrder = await gateway.checkoutWithBoundCard({ memberId: 'MEMBER_123', cardId: 'CARD_ID_FROM_BINDING', items: [{ name: 'Subscription', unitPrice: 999, quantity: 1 }], orderId: 'SUBSCRIPTION-001', // Optional });

console.log('Payment result:', boundOrder);

// Alternative: Bind card with transaction (ECPay only) gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, async (order) => { // After successful payment, bind the card used 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:

// Memory Card (記憶卡) - Gateway remembers card for single user session const memoryPayment = new ECPayPayment({ merchantId: 'YOUR_MERCHANT_ID', hashKey: 'YOUR_KEY', hashIv: 'YOUR_IV', memory: true, // Enable memory card });

// User only needs to enter card once per session // Subsequent payments in same session can reuse card // Card is NOT saved permanently // Cannot use bindCardWithTransaction when memory: true

// Card Binding (卡片綁定) - Permanently save card for future use const bindingPayment = new ECPayPayment({ merchantId: 'YOUR_MERCHANT_ID', hashKey: 'YOUR_KEY', hashIv: 'YOUR_IV', memory: false, // Must be false for binding });

// Card is saved permanently // Can use across sessions and devices // Requires prepareBindCard() or bindCardWithTransaction() // User must consent to saving card

Installment Payments (分期付款)

Credit card installment options:

// ECPay installments const installmentPayment = new ECPayPayment({ merchantId: 'YOUR_MERCHANT_ID', hashKey: 'YOUR_KEY', hashIv: 'YOUR_IV', installments: '3,6,12,18,24', // Available installment periods });

const order = await installmentPayment.prepare({ channel: Channel.CREDIT_CARD, items: [{ name: 'Laptop', unitPrice: 30000, quantity: 1 }], // User can choose 3, 6, 12, 18, or 24 installments on payment page });

// CTBC installments const ctbcPayment = new CTBCPayment({ merchantId: 'YOUR_MERCHANT_ID', merId: 'YOUR_MER_ID', txnKey: 'YOUR_KEY', terminalId: 'YOUR_TERMINAL', installmentCount: 12, // Fixed 12 installments });

// HwaNan installments const hwananPayment = new HwaNanPayment({ merchantId: 'YOUR_MERCHANT_ID', terminalId: 'YOUR_TERMINAL', merID: 'YOUR_MER_ID', identifier: 'YOUR_IDENTIFIER', installmentAmount: 5000, // Minimum amount for installments });

Recurring Payments (訂閱付款)

Set up periodic payments:

import { PaymentPeriodType } from '@rytass/payments';

// ECPay recurring payment const recurringPayment = new ECPayPayment({ merchantId: 'YOUR_MERCHANT_ID', hashKey: 'YOUR_KEY', hashIv: 'YOUR_IV', period: { amountPerPeriod: 999, // Amount per charge type: PaymentPeriodType.MONTH, // DAY, MONTH, or YEAR frequency: 1, // Charge every 1 month times: 12, // Total 12 charges }, });

const order = await recurringPayment.prepare({ channel: Channel.CREDIT_CARD, items: [{ name: 'Monthly Subscription', unitPrice: 999, quantity: 1 }], });

// Gateway will automatically charge 999 every month for 12 months

// Examples: // Daily payment for 30 days period: { amountPerPeriod: 50, type: PaymentPeriodType.DAY, frequency: 1, times: 30, }

// Weekly payment (every 7 days) for 8 weeks period: { amountPerPeriod: 200, type: PaymentPeriodType.DAY, frequency: 7, times: 8, }

// Quarterly payment for 1 year (4 times) period: { amountPerPeriod: 3000, type: PaymentPeriodType.MONTH, frequency: 3, times: 4, }

Built-in Server & Ngrok

Handle callbacks with built-in server:

// Option 1: Built-in server with public domain const gateway = new ECPayPayment({ merchantId: 'YOUR_MERCHANT_ID', hashKey: 'YOUR_KEY', hashIv: 'YOUR_IV', serverHost: 'https://your-domain.com', withServer: true, // Enable built-in server });

// Server automatically handles callbacks at: // https://your-domain.com/payments/callbacks

// Option 2: Built-in server with Ngrok (for development) const gateway = new ECPayPayment({ merchantId: 'YOUR_MERCHANT_ID', hashKey: 'YOUR_KEY', hashIv: 'YOUR_IV', withServer: 'ngrok', // Auto-start ngrok tunnel });

// Listen for server ready gateway.emitter.on(PaymentEvents.SERVER_LISTENED, (info) => { console.log('Server listening on:', info.url); // e.g., "https://abc123.ngrok.io" });

// Option 3: Custom server (no built-in server) const gateway = new ECPayPayment({ merchantId: 'YOUR_MERCHANT_ID', hashKey: 'YOUR_KEY', hashIv: 'YOUR_IV', withServer: false, });

// Implement your own callback endpoint app.post('/payment/callback', (req, res) => { gateway.defaultServerListener(req, res); });

NestJS Integration

Complete integration with NestJS dependency injection:

Basic Setup

// app.module.ts 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

// app.module.ts 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

// payment.service.ts 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) { // Update database, send notifications, etc. }

private async handlePaymentFailure(failure: any) { // Handle failure logic } }

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:

// payments.config.ts 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 {}

// Use in service @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:

// ECPay import { ECPayPayment } from '@rytass/payments-adapter-ecpay';

// Development (default) const devGateway = new ECPayPayment(); // Uses test credentials

// Production 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', });

// NewebPay import { NewebPayPayment } from '@rytass/payments-adapter-newebpay';

// 預設為測試環境: https://ccore.newebpay.com const prodGateway = new NewebPayPayment({ baseUrl: 'https://core.newebpay.com', // 正式環境 merchantId: 'YOUR_PROD_MERCHANT_ID', aesKey: 'YOUR_PROD_AES_KEY', aesIv: 'YOUR_PROD_AES_IV', });

// Happy Card 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

.env

NODE_ENV=production

ECPay

ECPAY_MERCHANT_ID=your_merchant_id ECPAY_HASH_KEY=your_hash_key ECPAY_HASH_IV=your_hash_iv

NewebPay

NEWEBPAY_MERCHANT_ID=your_merchant_id NEWEBPAY_AES_KEY=your_aes_key NEWEBPAY_AES_IV=your_aes_iv

HwaNan

HWANAN_MERCHANT_ID=your_merchant_id HWANAN_TERMINAL_ID=your_terminal_id HWANAN_MER_ID=your_mer_id HWANAN_IDENTIFIER=your_identifier

CTBC

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

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

HAPPY_CARD_C_SOURCE=your_c_source HAPPY_CARD_KEY=your_key

Server

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:

// Check credentials 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);

// Ensure no extra spaces in .env file ECPAY_HASH_KEY=your_key_here # ❌ Trailing space ECPAY_HASH_KEY=your_key_here # ✅ No trailing space

// Check baseUrl matches environment const gateway = new ECPayPayment({ baseUrl: 'https://payment-stage.ecpay.com.tw', // Test environment // Use production URL for production });

  1. Callback Not Received

Symptoms:

  • Payment completes but ORDER_COMMITTED never fires

  • Order stuck in PRE_COMMIT state

Solutions:

// 1. Check withServer setting const gateway = new ECPayPayment({ withServer: true, // Must be true for built-in server serverHost: 'https://your-domain.com', // Must be accessible from internet });

// 2. Verify server is listening gateway.emitter.on(PaymentEvents.SERVER_LISTENED, (info) => { console.log('Server listening:', info.url); // Ensure this URL is accessible from payment gateway });

// 3. Test callback endpoint manually curl -X POST https://your-domain.com/payments/callbacks
-H "Content-Type: application/json"
-d '{"test": "data"}'

// 4. Check firewall/nginx/load balancer // Ensure POST requests to /payments/callbacks are allowed

  1. Ngrok Tunnel Issues

Symptoms:

  • Ngrok tunnel fails to start

  • Ngrok URL changes frequently

Solutions:

// 1. Use environment variable for ngrok auth process.env.NGROK_AUTHTOKEN = 'your_ngrok_auth_token';

const gateway = new ECPayPayment({ withServer: 'ngrok', });

// 2. Use static ngrok domain (paid feature) const gateway = new ECPayPayment({ withServer: 'ngrok', serverHost: 'your-static-subdomain.ngrok.io', });

// 3. Check ngrok installation // npm install @ngrok/ngrok

  1. Order ID Conflicts

Symptoms:

  • Error: "Order ID already exists"

  • Cannot create new orders

Solutions:

// 1. Use unique order IDs const order = await gateway.prepare({ id: ORDER-${Date.now()}-${Math.random().toString(36).substr(2, 9)}, items: [...], });

// 2. Or let gateway auto-generate const order = await gateway.prepare({ // No id field - gateway generates unique ID items: [...], });

// 3. Check for duplicate submissions // Implement idempotency in your application

  1. Amount Mismatch

Symptoms:

  • Payment amount different from expected

  • Calculation errors

Solutions:

// Check item calculation const items = [ { name: 'A', unitPrice: 100, quantity: 2 }, // 200 { name: 'B', unitPrice: 50, quantity: 3 }, // 150 ]; // Total: 350

const order = await gateway.prepare({ items }); console.log('Total Price:', order.totalPrice); // Should be 350

// Verify additionalInfo after payment 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!'); } });

  1. Event Listeners Not Firing

Symptoms:

  • PaymentEvents.ORDER_COMMITTED not triggered

  • No event handlers executing

Solutions:

// 1. Register listeners BEFORE calling prepare() gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, handler); gateway.emitter.on(PaymentEvents.ORDER_FAILED, handler);

// Then create order const order = await gateway.prepare({...});

// 2. Check emitter exists if (!gateway.emitter) { console.error('Gateway has no emitter!'); }

// 3. Use once() for one-time events gateway.emitter.once(PaymentEvents.ORDER_COMMITTED, handler);

// 4. Check for errors in handler gateway.emitter.on(PaymentEvents.ORDER_COMMITTED, (order) => { try { // Your logic here } catch (error) { console.error('Handler error:', error); } });

  1. Card Binding Failures

Symptoms:

  • CARD_BINDING_FAILED event fires

  • Cannot bind card

Solutions:

// 1. Check memory setting const gateway = new ECPayPayment({ memory: false, // Must be false for binding // ... });

// 2. Handle already-bound cards gateway.emitter.on(PaymentEvents.CARD_BINDING_FAILED, (req) => { if (req.failedMessage?.code === '10100112') { console.log('Card already bound - this is OK'); // Retrieve existing binding } });

// 3. Use bindCardWithTransaction instead 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';

// Configure POS API const posConfig: CTBCPosApiConfig = { URL: 'https://ccapi.ctbcbank.com', MacKey: 'YOUR_MAC_KEY', // 8 or 24 characters };

// Query transaction status const queryResult = await posApiQuery(posConfig, { MERID: 'YOUR_MERID', 'LID-M': 'ORDER-001', Tx_ATTRIBUTE: 'TX_AUTH', // TX_AUTH | TX_SETTLE | TX_VOID | TX_REFUND });

// Smart cancel or refund (auto-determine action based on transaction state) 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); // 'Reversal' | 'CapRev' | 'Refund' | 'None' | 'Pending' | 'Failed'

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';

// Configure AMEX API const amexConfig: CTBCAmexConfig = { wsdlUrl: 'https://amex.ctbcbank.com/wsdl', timeout: 30000, sslOptions: { rejectUnauthorized: true, }, };

// Query AMEX transaction const amexResult = await amexInquiry(amexConfig, { merId: 'YOUR_MERID', lidm: 'ORDER-001', xid: 'TRANSACTION_XID', IN_MAC_KEY: 'YOUR_MAC_KEY', });

// Smart cancel or refund for AMEX 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; // CTBC merchant ID merchantName?: string; // Merchant display name merId: string; // MER ID from CTBC txnKey: string; // MAC/TXN key for encryption terminalId: string; // Terminal ID baseUrl?: string; // API base URL (default: https://ccapi.ctbcbank.com) isAmex?: boolean; // Enable AMEX card support (uses SOAP API)

// Server options withServer?: boolean | 'ngrok'; serverHost?: string; callbackPath?: string; checkoutPath?: string; bindCardPath?: string; boundCardPath?: string; boundCardCheckoutResultPath?: string;

// Cache options orderCache?: OrderCache; orderCacheTTL?: number; bindCardRequestsCache?: BindCardRequestCache; bindCardRequestsCacheTTL?: number;

// Callbacks 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!, });

// Get total balance (sum of all records) const [balance, productType] = await gateway.getCardBalance('HC1234567890123456', false); console.log('Total balance:', balance); console.log('Product type:', productType);

// Get detailed records 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

// Record types enum HappyCardRecordType { AMOUNT = 1, // Cash value (現金價值) BONUS = 2, // Bonus points (紅利點數) }

// Product types enum HappyCardProductType { INVOICE_FIRST_HAPPY_CARD_GF = '1', // 發票先開禮物卡 GF INVOICE_LATER_HAPPY_CARD_GS = '2', // 發票後開禮物卡 GS INVOICE_FIRST_DIGITAL_GIFT_GF = '3', // 發票先開數位禮物 GF INVOICE_LATER_DIGITAL_GIFT_GS = '4', // 發票後開數位禮物 GS INVOICE_FIRST_PHYSICAL_GIFT_GF = '5', // 發票先開實體禮物 GF INVOICE_LATER_PHYSICAL_GIFT_GS = '6', // 發票後開實體禮物 GS }

// Result codes enum HappyCardResultCode { FAILED = '0', SUCCESS = '1', }

// Base URLs 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!, });

// Step 1: Check card balance and get records const [records, productType] = await gateway.getCardBalance('HC1234567890123456', true); console.log('Available records:', records);

// Step 2: Prepare payment with specific 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', // Optional, max 6 chars uniMemberGID: 'MEMBER_GID', // Optional unified member ID isIsland: false, // true for offshore islands (離島) });

// Step 3: Commit the payment 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, });

// The order payload includes installment configuration // txType: HwaNanTransactionType.INSTALLMENTS (1) // NumberOfPay: Number of installments (minimum 3)

HwaNan Enums

// Transaction types enum HwaNanTransactionType { ONE_TIME = 0, // 一次付清 INSTALLMENTS = 1, // 分期付款 }

// Auto capture mode enum HwaNanAutoCapMode { MANUALLY = 0, // 手動請款 AUTO = 1, // 自動請款 }

// Customize page language enum HwaNanCustomizePageType { ZH_TW = 1, // 繁體中文 ZH_CN = 2, // 簡體中文 EN_US = 3, // English JA_JP = 4, // 日本語 OTHER = 5, // 其他 }

// Payment channel (currently only credit card) enum HwaNanPaymentChannel { CREDIT = 1, }

HwaNan Configuration Options

interface HwaNanPaymentInitOptions { merchantId: string; // 商店代碼 terminalId: string; // 端末機代碼 merchantName: string; // 商店名稱 merID: string; // MER ID identifier: string; // 識別碼 (用於產生 checkValue) baseUrl?: string; // API base URL customizePageType?: HwaNanCustomizePageType; // 頁面語言 customizePageVersion?: string; // 頁面版本

// Server options serverHost?: string; callbackPath?: string; checkoutPath?: string; withServer?: boolean | 'ngrok'; ttl?: number; // Order TTL in ms

// Callbacks serverListener?: (req, res) => void; onCommit?: (order) => void; onServerListen?: () => void;

// Cache ordersCache?: OrdersCache; }

Detailed Documentation

For complete API reference and advanced usage:

  • ECPay Adapter Reference

  • NewebPay Adapter Reference

  • HwaNan Bank Adapter Reference

  • CTBC Adapter Reference

  • iCash Pay Adapter Reference

  • Happy Card Adapter Reference

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

wms-module

No summary provided by upstream source.

Repository SourceNeeds Review
General

invoice-adapters

No summary provided by upstream source.

Repository SourceNeeds Review
General

wms-react-components

No summary provided by upstream source.

Repository SourceNeeds Review