UNPKG

@rytass/payments

Version:
771 lines (633 loc) 20.9 kB
# Rytass Utils - Payments A comprehensive TypeScript payment framework designed for Taiwan's payment ecosystem. Provides a unified interface for payment processing across multiple providers with support for various payment methods, card binding, and subscription payments. ## Features - [x] Unified payment interface across multiple providers - [x] Multiple payment channel support (Credit Card, WebATM, Virtual Account, CVS) - [x] Card binding and tokenization support - [x] Subscription and recurring payment management - [x] Real-time payment status tracking - [x] Event-driven architecture with EventEmitter - [x] TypeScript type safety throughout - [x] Order lifecycle management - [x] Refund and reversal operations - [x] Payment method detection and routing - [x] Extensible adapter pattern architecture ## Available Adapters This package provides the core interfaces and types. Use with specific adapter implementations: - **[@rytass/payments-adapter-ecpay](https://www.npmjs.com/package/@rytass/payments-adapter-ecpay)** - ECPay payment integration - **[@rytass/payments-adapter-newebpay](https://www.npmjs.com/package/@rytass/payments-adapter-newebpay)** - NewebPay payment integration - **[@rytass/payments-adapter-hwanan](https://www.npmjs.com/package/@rytass/payments-adapter-hwanan)** - HwaNan Bank payment integration - **[@rytass/payments-adapter-happy-card](https://www.npmjs.com/package/@rytass/payments-adapter-happy-card)** - Happy Card payment integration - **[@rytass/payments-adapter-icash-pay](https://www.npmjs.com/package/@rytass/payments-adapter-icash-pay)** - iCash Pay integration - **[@rytass/payments-adapter-ctbc-micro-fast-pay](https://www.npmjs.com/package/@rytass/payments-adapter-ctbc-micro-fast-pay)** - CTBC Micro Fast Pay integration - **[@rytass/payments-nestjs-module](https://www.npmjs.com/package/@rytass/payments-nestjs-module)** - NestJS integration module ## Installation ```bash npm install @rytass/payments # Install a specific adapter npm install @rytass/payments-adapter-ecpay # or yarn add @rytass/payments @rytass/payments-adapter-ecpay ``` ## Basic Usage ### Payment Gateway Implementation ```typescript import { PaymentGateway, PaymentItem, Channel } from '@rytass/payments'; import { ECPayPaymentGateway } from '@rytass/payments-adapter-ecpay'; // Initialize payment gateway const paymentGateway = new ECPayPaymentGateway({ merchantId: 'YOUR_MERCHANT_ID', hashKey: 'YOUR_HASH_KEY', hashIV: 'YOUR_HASH_IV', isProduction: false, }); // Prepare an order const order = await paymentGateway.prepare({ items: [ { name: 'Product A', unitPrice: 1000, quantity: 2, }, { name: 'Product B', unitPrice: 500, quantity: 1, }, ], id: 'ORDER-2024-001', clientBackUrl: 'https://yoursite.com/payment/return', }); console.log('Order ID:', order.id); console.log('Order State:', order.state); console.log( 'Total Price:', order.items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0), ); ``` ### Credit Card Payment ```typescript // Listen for payment events paymentGateway.emitter.on('ORDER_COMMITTED', message => { console.log('Payment successful:', message); console.log('Card Type:', message.cardType); console.log('Committed At:', message.committedAt); }); // Prepare credit card order const creditCardOrder = await paymentGateway.prepare({ items: [ { name: 'Premium Service', unitPrice: 2000, quantity: 1, }, ], id: 'CC-ORDER-001', }); // The order will be committed automatically when payment is completed // through the payment provider's interface ``` ### Virtual Account Payment ```typescript // Prepare virtual account order const vatOrder = await paymentGateway.prepare({ items: [ { name: 'Bulk Purchase', unitPrice: 10000, quantity: 1, }, ], id: 'VAT-ORDER-001', }); // Listen for async info retrieval paymentGateway.emitter.on('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); } }); ``` ### CVS Payment ```typescript // Prepare CVS payment order const cvsOrder = await paymentGateway.prepare({ items: [ { name: 'Online Purchase', unitPrice: 800, quantity: 1, }, ], id: 'CVS-ORDER-001', }); // Listen for CVS payment code paymentGateway.emitter.on('ORDER_INFO_RETRIEVED', order => { if (order.asyncInfo?.channel === Channel.CVS_KIOSK) { console.log('Payment Code:', order.asyncInfo.paymentCode); console.log('Expires At:', order.asyncInfo.expiredAt); // Display payment code to customer } }); ``` ## Core Concepts ### Payment Channels The framework supports multiple payment channels: ```typescript import { Channel } from '@rytass/payments'; // Available payment channels Channel.CREDIT_CARD; // Credit/Debit card payments Channel.WEB_ATM; // Online ATM transfers Channel.VIRTUAL_ACCOUNT; // Virtual account transfers Channel.CVS_KIOSK; // CVS kiosk payments Channel.CVS_BARCODE; // CVS barcode payments Channel.APPLE_PAY; // Apple Pay payments Channel.LINE_PAY; // LINE Pay payments ``` ### Order Lifecycle ```typescript import { OrderState } from '@rytass/payments'; // Order states throughout the payment process OrderState.INITED; // Order initialized OrderState.PRE_COMMIT; // Order created at gateway OrderState.ASYNC_INFO_RETRIEVED; // Payment info retrieved (for async payments) OrderState.COMMITTED; // Payment completed successfully OrderState.FAILED; // Payment failed OrderState.REFUNDED; // Payment refunded ``` ### Payment Items ```typescript import { PaymentItem } from '@rytass/payments'; const items: PaymentItem[] = [ { name: 'Product Name', unitPrice: 1000, // Price per unit in cents or smallest currency unit quantity: 2, // Number of items }, ]; ``` ## Advanced Usage ### Card Binding and Tokenization ```typescript import { BindCardPaymentGateway, BindCardRequest } from '@rytass/payments'; // For gateways that support card binding const bindingGateway = paymentGateway as BindCardPaymentGateway; // Prepare card binding const bindRequest = await bindingGateway.prepareBindCard('member-123'); // Listen for binding events paymentGateway.emitter.on('CARD_BOUND', bindingInfo => { console.log('Card bound successfully:', bindingInfo.cardId); }); // Use bound card for payment const boundCardOrder = await bindingGateway.checkoutWithBoundCard({ cardId: 'bound-card-id', memberId: 'member-123', orderId: 'BOUND-ORDER-001', items: [ { name: 'Subscription Service', unitPrice: 2999, quantity: 1, }, ], }); ``` ### Event Handling ```typescript import { PaymentEvents } from '@rytass/payments'; // Listen to all payment events paymentGateway.emitter.on(PaymentEvents.ORDER_PRE_COMMIT, order => { console.log('Order created:', order.id); }); paymentGateway.emitter.on(PaymentEvents.ORDER_INFO_RETRIEVED, order => { console.log('Payment info retrieved for:', order.id); // Handle async payment information (VAT, CVS, etc.) }); paymentGateway.emitter.on(PaymentEvents.ORDER_COMMITTED, message => { console.log('Payment committed:', message.id); console.log('Amount:', message.totalPrice); console.log('Committed at:', message.committedAt); }); paymentGateway.emitter.on(PaymentEvents.ORDER_FAILED, failure => { console.error('Payment failed:', failure.code, failure.message); }); ``` ### Order Querying ```typescript // Query order status const existingOrder = await paymentGateway.query('ORDER-2024-001'); console.log('Order State:', existingOrder.state); console.log('Created At:', existingOrder.createdAt); console.log('Committed At:', existingOrder.committedAt); // Check additional payment info if (existingOrder.additionalInfo) { switch (existingOrder.additionalInfo.channel) { case Channel.CREDIT_CARD: console.log('Auth Code:', existingOrder.additionalInfo.authCode); console.log('Card Number:', existingOrder.additionalInfo.card4Number); break; case Channel.WEB_ATM: console.log('Bank Code:', existingOrder.additionalInfo.buyerBankCode); console.log('Account:', existingOrder.additionalInfo.buyerAccountNumber); break; } } ``` ### Refund Operations ```typescript // Full refund await order.refund(); // Partial refund await order.refund(500); // Refund 500 units // Refund with options (gateway-specific) await order.refund(1000, { reason: 'Customer requested', refundId: 'REFUND-001', }); ``` ## TypeScript Integration ### Custom Payment Items ```typescript import { PaymentItem, PrepareOrderInput } from '@rytass/payments'; // Extend base PaymentItem with custom properties interface CustomPaymentItem extends PaymentItem { productId: string; category: string; discount?: number; taxRate?: number; } // Use with payment gateway const customOrder = await paymentGateway.prepare({ items: [ { name: 'Premium Product', unitPrice: 2000, quantity: 1, productId: 'PROD-001', category: 'Electronics', discount: 200, taxRate: 0.05, }, ] as CustomPaymentItem[], id: 'CUSTOM-ORDER-001', }); ``` ### Generic Payment Gateway ```typescript import { PaymentGateway, Order, OrderCommitMessage } from '@rytass/payments'; // Type-safe payment service class PaymentService<CM extends OrderCommitMessage> { constructor(private gateway: PaymentGateway<CM>) {} async processPayment(items: PaymentItem[], orderId: string): Promise<Order<CM>> { return this.gateway.prepare({ items, id: orderId }); } async checkPaymentStatus(orderId: string): Promise<Order<CM>> { return this.gateway.query(orderId); } } ``` ## Integration Examples ### Express.js API Integration ```typescript import express from 'express'; import { ECPayPaymentGateway } from '@rytass/payments-adapter-ecpay'; import { PaymentEvents, Channel } from '@rytass/payments'; const app = express(); const paymentGateway = new ECPayPaymentGateway({ merchantId: process.env.ECPAY_MERCHANT_ID!, hashKey: process.env.ECPAY_HASH_KEY!, hashIV: process.env.ECPAY_HASH_IV!, isProduction: process.env.NODE_ENV === 'production', }); // Setup payment event handlers paymentGateway.emitter.on(PaymentEvents.ORDER_COMMITTED, async message => { // Handle successful payment await updateOrderStatus(message.id, 'paid'); await sendConfirmationEmail(message.id); }); // Create payment endpoint app.post('/api/payments/create', async (req, res) => { try { const { items, orderId, paymentMethod, returnUrl } = req.body; const order = await paymentGateway.prepare({ items, id: orderId, clientBackUrl: returnUrl, }); res.json({ success: true, orderId: order.id, paymentUrl: order.checkoutUrl, // Gateway-specific checkout URL state: order.state, }); } catch (error) { res.status(400).json({ success: false, error: error.message, }); } }); // Query payment status endpoint app.get('/api/payments/:orderId/status', async (req, res) => { try { const { orderId } = req.params; const order = await paymentGateway.query(orderId); res.json({ success: true, order: { id: order.id, state: order.state, createdAt: order.createdAt, committedAt: order.committedAt, totalAmount: order.items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0), }, }); } catch (error) { res.status(404).json({ success: false, error: 'Order not found', }); } }); // Payment callback endpoint (for async payments) app.post('/api/payments/callback', async (req, res) => { try { // Handle payment gateway callback // This is typically handled by the specific adapter res.status(200).send('OK'); } catch (error) { res.status(400).send('ERROR'); } }); ``` ### NestJS Service Integration ```typescript import { Injectable, Logger } from '@nestjs/common'; import { PaymentGateway, PaymentEvents, OrderState, Channel } from '@rytass/payments'; import { ECPayPaymentGateway } from '@rytass/payments-adapter-ecpay'; @Injectable() export class PaymentService { private readonly logger = new Logger(PaymentService.name); private readonly paymentGateway: PaymentGateway; constructor() { this.paymentGateway = new ECPayPaymentGateway({ merchantId: process.env.ECPAY_MERCHANT_ID!, hashKey: process.env.ECPAY_HASH_KEY!, hashIV: process.env.ECPAY_HASH_IV!, isProduction: process.env.NODE_ENV === 'production', }); this.setupEventHandlers(); } private setupEventHandlers() { 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); }); this.paymentGateway.emitter.on(PaymentEvents.ORDER_INFO_RETRIEVED, order => { this.logger.log(`Payment info retrieved: ${order.id}`); this.handleAsyncPaymentInfo(order); }); } async createPayment(paymentData: { items: Array<{ name: string; price: number; quantity: number; }>; orderId: string; returnUrl?: string; }) { const order = await this.paymentGateway.prepare({ items: paymentData.items.map(item => ({ name: item.name, unitPrice: item.price, quantity: item.quantity, })), id: paymentData.orderId, clientBackUrl: paymentData.returnUrl, }); return { orderId: order.id, state: order.state, paymentUrl: order.checkoutUrl, totalAmount: order.items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0), }; } async getPaymentStatus(orderId: string) { const order = await this.paymentGateway.query(orderId); return { orderId: order.id, state: order.state, createdAt: order.createdAt, committedAt: order.committedAt, failedMessage: order.failedMessage, additionalInfo: order.additionalInfo, }; } async refundPayment(orderId: string, amount?: number) { const order = await this.paymentGateway.query(orderId); if (order.state !== OrderState.COMMITTED) { throw new Error('Can only refund committed payments'); } await order.refund(amount); return { orderId: order.id, state: order.state, refundAmount: amount, }; } private async handlePaymentSuccess(message: any) { // Update database, send notifications, etc. } private async handlePaymentFailure(failure: any) { // Log failure, notify customer, etc. } private async handleAsyncPaymentInfo(order: any) { // Handle virtual account, CVS payment codes, etc. if (order.asyncInfo?.channel === Channel.VIRTUAL_ACCOUNT) { // Send virtual account info to customer } else if (order.asyncInfo?.channel === Channel.CVS_KIOSK) { // Send CVS payment code to customer } } } ``` ### React Payment Integration ```typescript import React, { useState, useEffect } from 'react'; import { PaymentItem, OrderState, Channel } from '@rytass/payments'; interface PaymentProps { items: PaymentItem[]; orderId: string; onSuccess: (orderId: string) => void; onError: (error: string) => void; } const PaymentComponent: React.FC<PaymentProps> = ({ items, orderId, onSuccess, onError }) => { const [paymentState, setPaymentState] = useState<OrderState | null>(null); const [paymentInfo, setPaymentInfo] = useState<any>(null); const createPayment = async () => { try { const response = await fetch('/api/payments/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items, orderId, returnUrl: `${window.location.origin}/payment/return` }) }); const data = await response.json(); if (data.success) { setPaymentState(data.state); // Redirect to payment gateway for credit card payments if (data.paymentUrl) { window.location.href = data.paymentUrl; } } else { onError(data.error); } } catch (error) { onError('Payment creation failed'); } }; const checkPaymentStatus = async () => { try { const response = await fetch(`/api/payments/${orderId}/status`); const data = await response.json(); if (data.success) { setPaymentState(data.order.state); if (data.order.state === OrderState.COMMITTED) { onSuccess(orderId); } else if (data.order.state === OrderState.FAILED) { onError('Payment failed'); } } } catch (error) { onError('Status check failed'); } }; useEffect(() => { // Poll payment status for async payments if (paymentState === OrderState.ASYNC_INFO_RETRIEVED) { const interval = setInterval(checkPaymentStatus, 5000); return () => clearInterval(interval); } }, [paymentState]); const totalAmount = items.reduce((sum, item) => sum + (item.unitPrice * item.quantity), 0); return ( <div className="payment-component"> <h3>Payment Summary</h3> <div className="items"> {items.map((item, index) => ( <div key={index} className="item"> <span>{item.name}</span> <span>{item.quantity} x ${item.unitPrice}</span> </div> ))} </div> <div className="total"> Total: ${totalAmount} </div> {!paymentState && ( <button onClick={createPayment}> Proceed to Payment </button> )} {paymentState === OrderState.ASYNC_INFO_RETRIEVED && ( <div className="async-payment-info"> <p>Please complete payment using the provided information</p> {/* Display virtual account, CVS code, etc. */} </div> )} {paymentState === OrderState.COMMITTED && ( <div className="success"> Payment completed successfully! </div> )} </div> ); }; export default PaymentComponent; ``` ## Best Practices ### Security - Store sensitive credentials in environment variables - Validate all payment callbacks and webhooks - Implement proper HTTPS for all payment-related endpoints - Use CSRF protection for payment forms ### Error Handling - Always listen for payment failure events - Implement proper timeout handling for async payments - Provide clear error messages to users - Log all payment events for debugging and auditing ### Performance - Cache payment gateway instances when possible - Implement proper connection pooling for high-volume scenarios - Use appropriate timeout settings for payment operations - Monitor payment gateway response times ### State Management - Track order states throughout the payment lifecycle - Implement proper cleanup for expired orders - Handle duplicate payment attempts gracefully - Maintain audit trails for all payment operations ## Testing ```typescript import { PaymentGateway, OrderState, Channel } from '@rytass/payments'; describe('Payment Integration', () => { let paymentGateway: PaymentGateway; beforeEach(() => { // Initialize test gateway paymentGateway = new TestPaymentGateway(); }); it('should create order successfully', async () => { const order = await paymentGateway.prepare({ items: [ { name: 'Test Product', unitPrice: 1000, quantity: 1, }, ], id: 'TEST-ORDER-001', }); expect(order.id).toBe('TEST-ORDER-001'); expect(order.state).toBe(OrderState.PRE_COMMIT); }); it('should handle payment success', async () => { const order = await paymentGateway.prepare({ items: [{ name: 'Test', unitPrice: 1000, quantity: 1 }], id: 'TEST-SUCCESS', }); // Simulate payment success order.commit({ id: 'TEST-SUCCESS', totalPrice: 1000, committedAt: new Date(), type: Channel.CREDIT_CARD, }); expect(order.state).toBe(OrderState.COMMITTED); expect(order.committedAt).toBeDefined(); }); it('should handle payment failure', async () => { const order = await paymentGateway.prepare({ items: [{ name: 'Test', unitPrice: 1000, quantity: 1 }], id: 'TEST-FAILURE', }); order.fail('CARD_DECLINED', 'Credit card was declined'); expect(order.state).toBe(OrderState.FAILED); expect(order.failedMessage?.code).toBe('CARD_DECLINED'); }); }); ``` ## License MIT