@rytass/payments-adapter-ctbc-micro-fast-pay
v0.1.20
Published
Rytass Payment Gateway
Readme
Rytass Utils - Payments Adapter CTBC Micro Fast Pay
A comprehensive TypeScript payment adapter for CTBC (Chinatrust Bank) Micro Fast Pay system. This adapter provides seamless integration with Taiwan's CTBC Bank payment gateway, supporting credit card payments, card binding, virtual accounts, and comprehensive payment lifecycle management with built-in server support.
Features
- [x] CTBC Micro Fast Pay credit card payment integration
- [x] Card binding and tokenization with transaction-based binding
- [x] Bound card checkout functionality
- [x] Built-in HTTP server with automatic callback handling
- [x] Multiple payment channels (Credit Card, Virtual Account, CVS, Barcode, Apple Pay)
- [x] Credit card installment payments
- [x] Transaction query and status tracking
- [x] Order lifecycle management with event-driven architecture
- [x] Secure MAC/TXN signature verification
- [x] Ngrok integration for local development
- [x] LRU cache support for orders and bind card requests
- [x] Custom server integration support
- [x] TypeScript type safety throughout
- [x] Comprehensive error handling and logging
- [x] Production and development environment support
Installation
npm install @rytass/payments-adapter-ctbc-micro-fast-pay
# or
yarn add @rytass/payments-adapter-ctbc-micro-fast-payPeer Dependencies:
npm install @rytass/payments
# Optional: For local development with ngrok
npm install @ngrok/ngrokConfiguration
Basic Configuration
import { CTBCPayment, CTBCOrderState } from '@rytass/payments-adapter-ctbc-micro-fast-pay';
import { PaymentEvents } from '@rytass/payments';
// Production configuration
const productionGateway = new CTBCPayment({
merchantId: 'YOUR_CTBC_MERCHANT_ID', // CTBC provided merchant ID
merId: 'YOUR_MER_ID', // Merchant identifier
txnKey: 'YOUR_TXN_KEY', // MAC/TXN signature key
terminalId: 'YOUR_TERMINAL_ID', // Terminal identifier
baseUrl: 'https://ccapi.ctbcbank.com', // Production API URL
withServer: true, // Enable built-in server
serverHost: 'https://your-domain.com', // Your callback server
orderCacheTTL: 1800000, // Order cache TTL (30 minutes)
bindCardRequestsCacheTTL: 3600000, // Bind card cache TTL (1 hour)
});
// Development configuration
const developmentGateway = new CTBCPayment({
merchantId: 'TEST_MERCHANT_ID',
merId: 'TEST_MER_ID',
txnKey: 'TEST_TXN_KEY',
terminalId: 'TEST_TERMINAL_ID',
baseUrl: 'https://test-ccapi.ctbcbank.com', // Test API URL
withServer: 'ngrok', // Use ngrok for local development
orderCacheTTL: 600000, // Shorter TTL for testing (10 minutes)
});Environment-Based Configuration
// Secure environment-based setup
const paymentGateway = 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!,
baseUrl: process.env.NODE_ENV === 'production' ? 'https://ccapi.ctbcbank.com' : 'https://test-ccapi.ctbcbank.com',
withServer: true,
serverHost: process.env.SERVER_HOST || 'http://localhost:3000',
checkoutPath: '/payments/ctbc/checkout',
callbackPath: '/payments/ctbc/callback',
bindCardPath: '/payments/ctbc/bind-card',
boundCardPath: '/payments/ctbc/bound-card',
orderCacheTTL: parseInt(process.env.ORDER_CACHE_TTL || '1800000'),
onServerListen: () => {
console.log('CTBC payment server ready!');
},
});Basic Usage
Simple Credit Card Payment
Note: Please use NAT tunnel service (like ngrok) to proxy built-in server if you are behind a LAN network.
import { CTBCPayment, CTBCOrderState } from '@rytass/payments-adapter-ctbc-micro-fast-pay';
import { PaymentEvents, Channel } from '@rytass/payments';
const paymentGateway = new CTBCPayment({
merchantId: 'YOUR_CTBC_MERCHANT_ID',
merId: 'YOUR_MER_ID',
txnKey: 'YOUR_TXN_KEY',
terminalId: 'YOUR_TERMINAL_ID',
withServer: true,
serverHost: 'http://localhost:3000',
onServerListen: () => {
console.log('CTBC payment server is ready!');
},
});
// Setup payment event listeners
paymentGateway.emitter.on(PaymentEvents.ORDER_COMMITTED, message => {
console.log('CTBC payment successful:', message.id);
console.log('Transaction amount:', message.totalPrice);
console.log('Platform trade number:', message.platformTradeNumber);
});
// Create order
const order = await paymentGateway.prepare({
id: 'ORDER-2024-001', // Optional: auto-generated if not provided
items: [
{
name: 'Premium Product',
unitPrice: 2500,
quantity: 1,
},
{
name: 'Shipping Fee',
unitPrice: 100,
quantity: 1,
},
],
clientBackUrl: 'https://yoursite.com/payment/return', // Return URL after payment
});
console.log('Order ID:', order.id);
console.log('Total Price:', order.totalPrice);
console.log('Order State:', order.state);
// Three ways to handle checkout:
// 1. Get form data to prepare POST form by yourself
const formData = order.form;
console.log('Form action URL:', 'https://ccapi.ctbcbank.com/PayJSON');
console.log('Form data:', formData);
// 2. Get HTML including form data and automatic submit script
const autoSubmitHTML = order.formHTML;
// 3. Get built-in server URL for auto submit (only works if withServer is set)
const checkoutURL = order.checkoutURL;
console.log('Checkout URL:', checkoutURL);Credit Card Installment Payment
// Create installment payment order
const installmentOrder = await paymentGateway.prepare({
id: 'INSTALLMENT-ORDER-001',
items: [
{
name: 'High-Value Product',
unitPrice: 12000,
quantity: 1,
},
],
clientBackUrl: 'https://yoursite.com/payment/return',
installmentCount: 12, // 12 installment periods
cardType: 'VMJ', // Visa, MasterCard, JCB
});
console.log('Installment order created:', installmentOrder.id);
console.log('Installment periods:', installmentOrder.installmentCount);Advanced Usage
Card Binding with Transaction
CTBC Micro Fast Pay allows you to bind cards using successful transactions:
import { PaymentEvents } from '@rytass/payments';
const paymentGateway = new CTBCPayment({
merchantId: 'YOUR_CTBC_MERCHANT_ID',
merId: 'YOUR_MER_ID',
txnKey: 'YOUR_TXN_KEY',
terminalId: 'YOUR_TERMINAL_ID',
withServer: true,
requireCacheHit: false, // Allow card binding from transactions
onCommit: handleOrderCommit,
});
// Handle successful payment and bind card
async function handleOrderCommit(order: CTBCOrder) {
if (order.state === CTBCOrderState.COMMITTED) {
const { id, platformTradeNumber } = order;
const memberId = 'MEMBER_123456';
try {
// Prepare card binding request for user
const bindRequest = await paymentGateway.prepareBindCard(memberId, {
finishRedirectURL: 'https://your-domain.com/card-bound-success',
});
// Save card ID to your database
const cardId = bindRequest.cardId;
console.log('Card bound successfully:', cardId);
console.log('Card prefix:', bindRequest.cardNumberPrefix);
console.log('Card suffix:', bindRequest.cardNumberSuffix);
// Store the card information
await saveCustomerCard({
memberId,
cardId,
cardPrefix: bindRequest.cardNumberPrefix,
cardSuffix: bindRequest.cardNumberSuffix,
boundAt: new Date(),
});
} catch (error) {
console.error('Card binding failed:', error.message);
}
}
}
// Handle card binding events
paymentGateway.emitter.on(PaymentEvents.CARD_BOUND, bindRequest => {
console.log(`Card ${bindRequest.cardId} bound for member ${bindRequest.memberId}`);
});
paymentGateway.emitter.on(PaymentEvents.CARD_BINDING_FAILED, bindRequest => {
console.error('Card binding failed:', bindRequest.failedMessage);
// Handle card already bound scenario
if (bindRequest.failedMessage?.code === '10100112') {
console.log('Card already bound:');
console.log('Member ID:', bindRequest.memberId);
console.log('Card ID:', bindRequest.cardId);
console.log('Card Number Prefix:', bindRequest.cardNumberPrefix);
console.log('Card Number Suffix:', bindRequest.cardNumberSuffix);
}
});Bound Card Checkout
Use previously bound cards for quick checkout:
// Checkout with bound card
const boundCardResult = await paymentGateway.checkoutWithBoundCard({
memberId: 'MEMBER_123456',
cardId: 'CARD_789012',
orderId: 'BOUND-ORDER-001',
items: [
{
name: 'Subscription Service',
unitPrice: 999,
quantity: 1,
},
],
});
if (boundCardResult.success) {
console.log('Bound card payment successful:', boundCardResult.orderId);
console.log('Transaction ID:', boundCardResult.transactionId);
} else {
console.error('Bound card payment failed:', boundCardResult.error);
}Transaction Query
// Query order status
const orderStatus = await paymentGateway.query('ORDER-2024-001');
console.log('Order ID:', orderStatus.id);
console.log('Order State:', orderStatus.state);
console.log('Total Price:', orderStatus.totalPrice);
console.log('Created At:', orderStatus.createdAt);
console.log('Committed At:', orderStatus.committedAt);
// Check if order is committed
if (orderStatus.state === CTBCOrderState.COMMITTED) {
console.log('Payment completed successfully');
console.log('Platform Trade Number:', orderStatus.platformTradeNumber);
} else if (orderStatus.state === CTBCOrderState.FAILED) {
console.log('Payment failed:', orderStatus.failedMessage);
}Virtual Account Payment
// Create virtual account order
const vatOrder = await paymentGateway.prepare({
id: 'VAT-ORDER-001',
channel: Channel.VIRTUAL_ACCOUNT,
items: [
{
name: 'Bulk Purchase',
unitPrice: 50000,
quantity: 1,
},
],
clientBackUrl: 'https://yoursite.com/payment/return',
});
// Listen for async info (virtual account details)
paymentGateway.emitter.on(PaymentEvents.ORDER_INFO_RETRIEVED, order => {
if (order.asyncInfo?.channel === Channel.VIRTUAL_ACCOUNT) {
console.log('Virtual Account Details:');
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
notifyCustomerVirtualAccount({
orderId: order.id,
bankCode: order.asyncInfo.bankCode,
accountNumber: order.asyncInfo.account,
amount: order.totalPrice,
expiresAt: order.asyncInfo.expiredAt,
});
}
});CVS Payment
// Create CVS payment order
const cvsOrder = await paymentGateway.prepare({
id: 'CVS-ORDER-001',
channel: Channel.CVS_KIOSK,
items: [
{
name: 'Online Purchase',
unitPrice: 1500,
quantity: 1,
},
],
clientBackUrl: 'https://yoursite.com/payment/return',
});
// Listen for CVS payment code
paymentGateway.emitter.on(PaymentEvents.ORDER_INFO_RETRIEVED, order => {
if (order.asyncInfo?.channel === Channel.CVS_KIOSK) {
console.log('CVS Payment Details:');
console.log('Payment Code:', order.asyncInfo.paymentCode);
console.log('Expires At:', order.asyncInfo.expiredAt);
// Send CVS payment code to customer
notifyCustomerCVSCode({
orderId: order.id,
paymentCode: order.asyncInfo.paymentCode,
amount: order.totalPrice,
expiresAt: order.asyncInfo.expiredAt,
});
}
});Integration Examples
Express.js API Integration
import express from 'express';
import { CTBCPayment, CTBCOrderState } from '@rytass/payments-adapter-ctbc-micro-fast-pay';
import { PaymentEvents, Channel } from '@rytass/payments';
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const paymentGateway = 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: false, // Use custom Express server
baseUrl: process.env.NODE_ENV === 'production' ? 'https://ccapi.ctbcbank.com' : 'https://test-ccapi.ctbcbank.com',
});
// Use the default server listener for payment handling
app.use(paymentGateway.defaultServerListener);
// Setup payment event handlers
paymentGateway.emitter.on(PaymentEvents.ORDER_COMMITTED, async message => {
try {
// Update database
await updateOrderStatus(message.id, 'paid');
// Send confirmation email
await sendPaymentConfirmation(message.id);
console.log(`CTBC payment committed: ${message.id}`);
} catch (error) {
console.error('Error handling payment success:', error);
}
});
// Create payment endpoint
app.post('/api/payments/ctbc/create', async (req, res) => {
try {
const { items, orderId, returnUrl, channel, memberId } = req.body;
const order = await paymentGateway.prepare({
id: orderId,
items: items.map(item => ({
name: item.name,
unitPrice: item.price,
quantity: item.quantity,
})),
clientBackUrl: returnUrl,
checkoutMemberId: memberId, // For potential card binding
});
res.json({
success: true,
payment: {
orderId: order.id,
totalAmount: order.totalPrice,
checkoutUrl: order.checkoutURL,
formData: order.form,
formHTML: order.formHTML,
state: order.state,
},
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message,
});
}
});
// Query payment status endpoint
app.get('/api/payments/ctbc/:orderId/status', async (req, res) => {
try {
const { orderId } = req.params;
const order = await paymentGateway.query(orderId);
res.json({
success: true,
payment: {
orderId: order.id,
state: order.state,
totalAmount: order.totalPrice,
createdAt: order.createdAt,
committedAt: order.committedAt,
platformTradeNumber: order.platformTradeNumber,
isCommitted: order.state === CTBCOrderState.COMMITTED,
isFailed: order.state === CTBCOrderState.FAILED,
},
});
} catch (error) {
res.status(404).json({
success: false,
error: 'Payment not found',
});
}
});
// Bind card endpoint
app.post('/api/payments/ctbc/bind-card', async (req, res) => {
try {
const { memberId } = req.body;
const bindRequest = await paymentGateway.prepareBindCard(memberId, {
finishRedirectURL: 'https://your-domain.com/card-bound-success',
});
res.json({
success: true,
cardBinding: {
cardId: bindRequest.cardId,
memberId: bindRequest.memberId,
cardPrefix: bindRequest.cardNumberPrefix,
cardSuffix: bindRequest.cardNumberSuffix,
state: bindRequest.state,
},
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message,
});
}
});
// Bound card checkout endpoint
app.post('/api/payments/ctbc/bound-card-checkout', async (req, res) => {
try {
const { memberId, cardId, items, orderId } = req.body;
const result = await paymentGateway.checkoutWithBoundCard({
memberId,
cardId,
orderId,
items: items.map(item => ({
name: item.name,
unitPrice: item.price,
quantity: item.quantity,
})),
});
if (result.success) {
res.json({
success: true,
payment: {
orderId: result.orderId,
transactionId: result.transactionId,
amount: result.amount,
},
});
} else {
res.status(400).json({
success: false,
error: result.error,
});
}
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
});
app.listen(3000, () => {
console.log('CTBC payment server running on port 3000');
});NestJS Service Integration
import { Injectable, BadRequestException, NotFoundException, Logger } from '@nestjs/common';
import { CTBCPayment, CTBCOrderState, CTBCBindCardRequestState } from '@rytass/payments-adapter-ctbc-micro-fast-pay';
import { PaymentEvents, Channel } from '@rytass/payments';
@Injectable()
export class CTBCPaymentService {
private readonly logger = new Logger(CTBCPaymentService.name);
private readonly paymentGateway: CTBCPayment;
constructor() {
this.paymentGateway = 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!,
baseUrl: process.env.NODE_ENV === 'production' ? 'https://ccapi.ctbcbank.com' : 'https://test-ccapi.ctbcbank.com',
withServer: true,
serverHost: process.env.SERVER_HOST,
});
this.setupEventHandlers();
}
private setupEventHandlers() {
this.paymentGateway.emitter.on(PaymentEvents.ORDER_COMMITTED, message => {
this.logger.log(`CTBC payment committed: ${message.id}`);
this.handlePaymentSuccess(message);
});
this.paymentGateway.emitter.on(PaymentEvents.ORDER_FAILED, failure => {
this.logger.error(`CTBC payment failed: ${failure.code} - ${failure.message}`);
this.handlePaymentFailure(failure);
});
this.paymentGateway.emitter.on(PaymentEvents.CARD_BOUND, bindRequest => {
this.logger.log(`Card bound: ${bindRequest.cardId} for member ${bindRequest.memberId}`);
this.handleCardBound(bindRequest);
});
}
async createPayment(paymentData: {
orderId?: string;
items: Array<{
name: string;
price: number;
quantity: number;
}>;
returnUrl?: string;
memberId?: string;
channel?: string;
installmentCount?: number;
}) {
const order = await this.paymentGateway.prepare({
id: paymentData.orderId,
items: paymentData.items.map(item => ({
name: item.name,
unitPrice: item.price,
quantity: item.quantity,
})),
clientBackUrl: paymentData.returnUrl,
checkoutMemberId: paymentData.memberId,
installmentCount: paymentData.installmentCount,
});
return {
orderId: order.id,
totalAmount: order.totalPrice,
checkoutUrl: order.checkoutURL,
formData: order.form,
state: order.state,
createdAt: order.createdAt,
};
}
async getPaymentStatus(orderId: string) {
try {
const order = await this.paymentGateway.query(orderId);
return {
orderId: order.id,
state: order.state,
totalAmount: order.totalPrice,
createdAt: order.createdAt,
committedAt: order.committedAt,
platformTradeNumber: order.platformTradeNumber,
isCommitted: order.state === CTBCOrderState.COMMITTED,
isFailed: order.state === CTBCOrderState.FAILED,
failedMessage: order.failedMessage,
};
} catch (error) {
throw new NotFoundException(`Payment ${orderId} not found`);
}
}
async bindCard(memberId: string) {
try {
const bindRequest = await this.paymentGateway.prepareBindCard(memberId, {
finishRedirectURL: 'https://your-domain.com/card-bound-success',
});
return {
cardId: bindRequest.cardId,
memberId: bindRequest.memberId,
cardPrefix: bindRequest.cardNumberPrefix,
cardSuffix: bindRequest.cardNumberSuffix,
state: bindRequest.state,
isSuccessful: bindRequest.state === CTBCBindCardRequestState.BOUND,
};
} catch (error) {
throw new BadRequestException(`Card binding failed: ${error.message}`);
}
}
async checkoutWithBoundCard(checkoutData: {
memberId: string;
cardId: string;
orderId?: string;
items: Array<{
name: string;
price: number;
quantity: number;
}>;
}) {
const result = await this.paymentGateway.checkoutWithBoundCard({
memberId: checkoutData.memberId,
cardId: checkoutData.cardId,
orderId: checkoutData.orderId,
items: checkoutData.items.map(item => ({
name: item.name,
unitPrice: item.price,
quantity: item.quantity,
})),
});
if (!result.success) {
throw new BadRequestException(`Bound card checkout failed: ${result.error}`);
}
return {
orderId: result.orderId,
transactionId: result.transactionId,
amount: result.amount,
success: result.success,
};
}
private async handlePaymentSuccess(message: any) {
// Handle successful payment - update database, send notifications, etc.
this.logger.log(`Processing payment success for order: ${message.id}`);
}
private async handlePaymentFailure(failure: any) {
// Handle payment failure - log, notify customer, etc.
this.logger.error(`Processing payment failure: ${failure.code} - ${failure.message}`);
}
private async handleCardBound(bindRequest: any) {
// Handle successful card binding - save to database, etc.
this.logger.log(`Processing card binding for member: ${bindRequest.memberId}`);
}
}React Payment Component
import React, { useState, useEffect } from 'react';
interface CTBCPaymentProps {
items: Array<{
name: string;
price: number;
quantity: number;
}>;
memberId?: string;
onSuccess: (result: any) => void;
onError: (error: string) => void;
}
const CTBCPayment: React.FC<CTBCPaymentProps> = ({
items,
memberId,
onSuccess,
onError
}) => {
const [paymentData, setPaymentData] = useState<any>(null);
const [boundCards, setBoundCards] = useState<any[]>([]);
const [selectedCardId, setSelectedCardId] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const [paymentMethod, setPaymentMethod] = useState<'new' | 'bound'>('new');
const totalAmount = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
useEffect(() => {
if (memberId) {
fetchBoundCards();
}
}, [memberId]);
const fetchBoundCards = async () => {
try {
const response = await fetch(`/api/members/${memberId}/bound-cards`);
const data = await response.json();
if (data.success) {
setBoundCards(data.cards);
}
} catch (error) {
console.error('Failed to fetch bound cards:', error);
}
};
const createNewPayment = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/payments/ctbc/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items,
memberId,
returnUrl: `${window.location.origin}/payment/return`
})
});
const data = await response.json();
if (data.success) {
setPaymentData(data.payment);
} else {
onError(data.error);
}
} catch (error) {
onError('Payment creation failed');
} finally {
setIsLoading(false);
}
};
const checkoutWithBoundCard = async () => {
if (!selectedCardId) {
onError('Please select a bound card');
return;
}
setIsLoading(true);
try {
const response = await fetch('/api/payments/ctbc/bound-card-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
memberId,
cardId: selectedCardId,
items
})
});
const data = await response.json();
if (data.success) {
onSuccess(data.payment);
} else {
onError(data.error);
}
} catch (error) {
onError('Bound card checkout failed');
} finally {
setIsLoading(false);
}
};
const proceedToPayment = () => {
if (paymentData) {
// Option 1: Redirect to CTBC hosted checkout
if (paymentData.checkoutUrl) {
window.location.href = paymentData.checkoutUrl;
} else {
// Option 2: Submit form programmatically
const form = document.createElement('form');
form.method = 'POST';
form.action = 'https://ccapi.ctbcbank.com/PayJSON';
Object.entries(paymentData.formData).forEach(([key, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = value as string;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
}
};
return (
<div className="ctbc-payment">
<h3>CTBC Bank Payment</h3>
<div className="order-summary">
<h4>Order Summary</h4>
{items.map((item, index) => (
<div key={index} className="item">
<span>{item.name}</span>
<span>{item.quantity} x NT${item.price}</span>
</div>
))}
<div className="total">
<strong>Total: NT${totalAmount}</strong>
</div>
</div>
{memberId && boundCards.length > 0 && (
<div className="payment-method-selection">
<h4>Payment Method</h4>
<div className="method-options">
<label>
<input
type="radio"
value="new"
checked={paymentMethod === 'new'}
onChange={(e) => setPaymentMethod(e.target.value as 'new' | 'bound')}
/>
New Credit Card
</label>
<label>
<input
type="radio"
value="bound"
checked={paymentMethod === 'bound'}
onChange={(e) => setPaymentMethod(e.target.value as 'new' | 'bound')}
/>
Use Bound Card
</label>
</div>
</div>
)}
{paymentMethod === 'bound' && boundCards.length > 0 && (
<div className="bound-cards">
<h4>Select Bound Card</h4>
{boundCards.map((card) => (
<div key={card.cardId} className="bound-card">
<label>
<input
type="radio"
value={card.cardId}
checked={selectedCardId === card.cardId}
onChange={(e) => setSelectedCardId(e.target.value)}
/>
**** **** **** {card.cardSuffix} ({card.cardType})
</label>
</div>
))}
</div>
)}
<div className="payment-actions">
{paymentMethod === 'new' && (
<>
{!paymentData && (
<button
onClick={createNewPayment}
disabled={isLoading}
className="create-payment-btn"
>
{isLoading ? 'Creating Payment...' : 'Proceed to Payment'}
</button>
)}
{paymentData && (
<div className="payment-ready">
<p>Payment ready for NT${paymentData.totalAmount}</p>
<button onClick={proceedToPayment} className="pay-btn">
Pay with CTBC Bank
</button>
</div>
)}
</>
)}
{paymentMethod === 'bound' && (
<button
onClick={checkoutWithBoundCard}
disabled={!selectedCardId || isLoading}
className="bound-pay-btn"
>
{isLoading ? 'Processing...' : 'Pay with Bound Card'}
</button>
)}
</div>
</div>
);
};
export default CTBCPayment;Error Handling
Common Error Scenarios
try {
const order = await paymentGateway.prepare({
items: [
{
name: 'Product',
unitPrice: 1000,
quantity: 1,
},
],
});
} catch (error) {
// Handle different types of errors
if (error.message.includes('merchantId is required')) {
console.error('Missing merchant configuration');
} else if (error.message.includes('Invalid TXN key')) {
console.error('TXN signature key is invalid');
} else if (error.message.includes('Server not started')) {
console.error('Payment server not initialized');
} else if (error.message.includes('Order cache miss')) {
console.error('Order not found in cache - may have expired');
} else {
console.error('Order preparation failed:', error.message);
}
}
// Card binding errors
try {
const bindRequest = await paymentGateway.prepareBindCard('MEMBER_123', {
finishRedirectURL: 'https://your-domain.com/card-bound-success',
});
} catch (error) {
if (error.message.includes('Card already bound')) {
console.error('This card is already bound to the member');
} else if (error.message.includes('Invalid transaction')) {
console.error('Transaction not found or not successful');
} else if (error.message.includes('Binding timeout')) {
console.error('Card binding request timed out');
} else {
console.error('Card binding failed:', error.message);
}
}
// Payment event error handling
paymentGateway.emitter.on(PaymentEvents.ORDER_FAILED, failure => {
switch (failure.code) {
case 'INVALID_CARD':
console.error('Credit card information is invalid');
break;
case 'INSUFFICIENT_FUNDS':
console.error('Insufficient funds on card');
break;
case 'EXPIRED_CARD':
console.error('Credit card has expired');
break;
case 'DECLINED':
console.error('Transaction declined by bank');
break;
case 'NETWORK_ERROR':
console.error('Network connection error');
break;
default:
console.error(`Payment failed: ${failure.code} - ${failure.message}`);
}
});Validation and Security
// Validate MAC/TXN signatures
const validateTransactionSecurity = (transactionData: any) => {
// CTBC uses MAC/TXN for transaction verification
// The adapter handles this internally, but you can add additional validation
if (!transactionData.platformTradeNumber) {
throw new Error('Missing platform trade number');
}
if (!transactionData.amount || transactionData.amount <= 0) {
throw new Error('Invalid transaction amount');
}
return true;
};
// Safe order processing with validation
const processOrderSafely = async (orderData: any) => {
try {
// Validate inputs
if (!orderData.items || orderData.items.length === 0) {
throw new Error('Order must have at least one item');
}
const totalAmount = orderData.items.reduce((sum, item) => {
return sum + item.unitPrice * item.quantity;
}, 0);
if (totalAmount <= 0) {
throw new Error('Order total must be greater than 0');
}
// Create order
const order = await paymentGateway.prepare(orderData);
// Log for audit
console.log(`Order created: ${order.id}, Amount: ${order.totalPrice}`);
return { success: true, order };
} catch (error) {
console.error('Order processing failed:', error);
return { success: false, error: error.message };
}
};Best Practices
Configuration Management
- Store sensitive credentials (merchantId, txnKey) in environment variables
- Use different merchant IDs for development and production
- Implement proper HTTPS for all payment-related endpoints
- Regularly rotate TXN keys and credentials
Order Management
- Always validate transaction authenticity using MAC/TXN verification
- Implement appropriate cache TTL for orders and bind card requests
- Use unique order IDs to prevent duplicate transactions
- Log all payment events for debugging and auditing
Card Binding
- Only bind cards from successful transactions
- Implement proper member authentication before card binding
- Handle "card already bound" scenarios gracefully
- Store card information securely with encryption
Security
- Validate all payment callbacks before processing
- Implement proper CSRF protection for payment forms
- Use HTTPS for all payment-related communications
- Never log sensitive payment data (card numbers, TXN keys)
Performance
- Use appropriate cache TTL values based on your use case
- Implement proper connection pooling for database operations
- Monitor payment gateway response times
- Set appropriate timeout values for payment operations
Error Handling
- Implement comprehensive error logging
- Provide clear error messages to customers
- Handle network failures gracefully with retry mechanisms
- Monitor for unusual payment failure patterns
Testing
import { CTBCPayment, CTBCOrderState, CTBCBindCardRequestState } from '@rytass/payments-adapter-ctbc-micro-fast-pay';
import { Channel } from '@rytass/payments';
describe('CTBC Payment Integration', () => {
let paymentGateway: CTBCPayment;
beforeEach(() => {
paymentGateway = new CTBCPayment({
merchantId: 'TEST_MERCHANT',
merId: 'TEST_MER_ID',
txnKey: 'TEST_TXN_KEY',
terminalId: 'TEST_TERMINAL',
baseUrl: 'https://test-ccapi.ctbcbank.com',
});
});
it('should create order successfully', async () => {
const order = await paymentGateway.prepare({
items: [
{
name: 'Test Product',
unitPrice: 1000,
quantity: 1,
},
],
});
expect(order.id).toBeDefined();
expect(order.totalPrice).toBe(1000);
expect(order.state).toBe(CTBCOrderState.INITED);
});
it('should generate correct form data', async () => {
const order = await paymentGateway.prepare({
items: [
{
name: 'Test Product',
unitPrice: 2000,
quantity: 2,
},
],
});
const form = order.form;
expect(form.merID).toBe('TEST_MER_ID');
expect(form.URLEnc).toBeDefined();
expect(form.URLEnc).toContain('TEST_MERCHANT');
});
it('should query order status', async () => {
const order = await paymentGateway.query('TEST-ORDER-001');
expect(order.id).toBe('TEST-ORDER-001');
expect(order.state).toBeDefined();
});
it('should handle card binding', async () => {
const bindRequest = await paymentGateway.prepareBindCard('TEST_MEMBER', {
finishRedirectURL: 'https://test-domain.com/card-bound-success',
});
expect(bindRequest.memberId).toBe('TEST_MEMBER');
expect(bindRequest.cardId).toBeDefined();
expect(bindRequest.state).toBe(CTBCBindCardRequestState.REQUESTED);
});
it('should checkout with bound card', async () => {
const result = await paymentGateway.checkoutWithBoundCard({
memberId: 'TEST_MEMBER',
cardId: 'TEST_CARD_ID',
orderId: 'BOUND_ORDER_001',
items: [
{
name: 'Test Product',
unitPrice: 500,
quantity: 1,
},
],
});
expect(result.success).toBe(true);
expect(result.orderId).toBe('BOUND_ORDER_001');
expect(result.amount).toBe(500);
});
});API Reference
CTBCPayment Constructor Options
interface CTBCPaymentOptions {
merchantId: string; // Required: CTBC merchant ID
merchantName?: string; // Optional: Merchant name
merId: string; // Required: Merchant identifier
txnKey: string; // Required: MAC/TXN signature key
terminalId: string; // Required: Terminal identifier
baseUrl?: string; // Optional: CTBC API URL
requireCacheHit?: boolean; // Optional: Require cache hit for operations
withServer?: boolean | 'ngrok'; // Optional: Enable built-in server
serverHost?: string; // Optional: Server host URL
checkoutPath?: string; // Optional: Checkout endpoint path
callbackPath?: string; // Optional: Callback endpoint path
bindCardPath?: string; // Optional: Card binding path
orderCacheTTL?: number; // Optional: Order cache TTL in ms
bindCardRequestsCacheTTL?: number; // Optional: Bind card cache TTL in ms
onServerListen?: () => void; // Optional: Server ready callback
onCommit?: (order: CTBCOrder) => void; // Optional: Payment success callback
}CTBCPayment Methods
prepare(options: PrepareOrderInput): Promise<CTBCOrder>
Creates a new CTBC payment order.
query(orderId: string): Promise<CTBCOrder>
Queries payment status by order ID.
prepareBindCard(memberId: string, options?: CTBCRequestPrepareBindCardOptions): Promise<CTBCBindCardRequest>
Prepares a card binding request for a member.
checkoutWithBoundCard(options: CheckoutWithBoundCardOptions): Promise<BoundCardCheckoutResult>
Processes payment using a previously bound card.
Constants and Enums
// Order states
enum CTBCOrderState {
INITED = 'INITED', // Order created
PRE_COMMIT = 'PRE_COMMIT', // Order submitted to CTBC
COMMITTED = 'COMMITTED', // Payment successful
FAILED = 'FAILED', // Payment failed
}
// Card binding states
enum CTBCBindCardRequestState {
REQUESTED = 'REQUESTED', // Binding requested
BOUND = 'BOUND', // Card successfully bound
FAILED = 'FAILED', // Binding failed
}License
MIT
