npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rytass/payments

v0.3.0

Published

Rytass Payment Gateway

Readme

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:

Installation

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

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

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

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

// 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:

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

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

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

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

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

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

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

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

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

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

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

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

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