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

@codecanva/payment-platform

v0.2.0

Published

Reusable NestJS payment platform: gateways, payments, webhooks, subscriptions, invoicing.

Readme

@codecanva/payment-platform

A reusable NestJS payment ecosystem — gateway adapters, payment lifecycle, refunds, webhooks, subscriptions, invoicing/tax, and a typed event bus.

Deliberately decoupled from any specific ORM or database: the package owns no persistence. You implement narrow storage ports against whatever you already use (TypeORM, Prisma, Mongoose, Drizzle, raw SQL …), and the platform stays usable even with no storage bound.

📦 Published on npm: @codecanva/payment-platform.


Features

  • Gateway abstraction — one PaymentGateway interface; adapters for Razorpay, Stripe, Cashfree, PayU. Write your code once, swap providers freely.
  • Payments — create order, verify, capture, refund, status.
  • Webhooks as a first-class module — signature verification, normalization, and routing to @WebhookEvent handlers, with a ready-made endpoint.
  • Subscriptions — plans, trials, billing cycles, renewals, cancellation.
  • Invoicing + tax — line items with GST (CGST/SGST/IGST) calculation, credit notes.
  • Event-driven — typed domain events over @nestjs/event-emitter; extend the platform without modifying it.
  • ORM-agnostic storage ports — bind your own persistence, or run stateless.

Installation

Fastest: one-command setup

Inside your NestJS project, run:

npx @codecanva/payment-platform init

This installs the package + peer deps, scaffolds a payment/ module (wiring + an example controller + webhook handlers), seeds .env.example, and wires PaymentModule into your AppModule plus { rawBody: true } and the exception filter into main.ts (writing *.bak backups). Pick a gateway with --gateway stripe|razorpay|cashfree|payu; see npx @codecanva/payment-platform init --help for all flags.

Manual install

npm install @codecanva/payment-platform
# peer deps (if not already present)
npm install @nestjs/common @nestjs/core @nestjs/event-emitter class-validator class-transformer reflect-metadata rxjs

After scaffolding, run your formatter (Prettier/ESLint) to tidy the generated wiring.


Quick start

1. Register the module

import { Module } from '@nestjs/common';
import {
  PaymentPlatformModule,
  RazorpayAdapter,
  GatewayProvider,
} from '@codecanva/payment-platform';

@Module({
  imports: [
    PaymentPlatformModule.forRoot({
      gateways: [
        new RazorpayAdapter({
          apiKey: process.env.RAZORPAY_KEY_ID,
          apiSecret: process.env.RAZORPAY_KEY_SECRET,
          webhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
        }),
      ],
      defaultGateway: GatewayProvider.RAZORPAY,

      // Optional — bind only the ports you use:
      storage: { payment: new MyPaymentStore() },

      // Optional — invoice/tax config:
      invoice: { sellerGstin: '29ABCDE1234F1Z5', defaultTaxRate: 18 },
    }),
  ],
})
export class AppModule {}

2. Enable raw body + the exception filter

Webhook signature verification needs the raw request bytes, so create the app with { rawBody: true }:

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { PaymentExceptionFilter } from '@codecanva/payment-platform';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { rawBody: true });
  app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
  app.useGlobalFilters(new PaymentExceptionFilter()); // maps platform errors -> HTTP status
  await app.listen(3000);
}
bootstrap();

3. Use the services

import { Controller, Post, Body } from '@nestjs/common';
import { OrderService, RefundService } from '@codecanva/payment-platform';

@Controller('checkout')
export class CheckoutController {
  constructor(
    private readonly orders: OrderService,
    private readonly refunds: RefundService,
  ) {}

  @Post('order')
  create(@Body() body) {
    // amounts are in minor units (paise/cents)
    return this.orders.createOrder({ amount: 50000, currency: 'INR' as any });
  }
}

Architecture

@codecanva/payment-platform
├── payment        Create order, verify, capture, refund, status
├── gateway        PaymentGateway interface + adapters + registry   ← the core abstraction
├── webhook        Signature verify → normalize → route (@WebhookEvent)
├── subscription   Plans, trials, cycles, renewals, cancellation
├── invoice        Invoice generation + GST/tax + credit notes
├── events         Typed domain events + PlatformEventBus
└── common         Enums, record interfaces, exceptions, storage ports, DI tokens, utils

The dependency direction that matters: subscription → payment (a renewal creates a payment), not the other way around. The payment core is tax-agnostic — all GST logic is confined to the invoice module.


Gateways

Every provider is wrapped in an adapter implementing PaymentGateway:

export interface PaymentGateway {
  readonly provider: GatewayProvider;
  createOrder(input): Promise<GatewayOrder>;
  fetchPayment(id): Promise<GatewayPayment>;
  capturePayment(input): Promise<GatewayPayment>;
  refund(input): Promise<GatewayRefund>;
  verifyPaymentSignature(input): boolean;   // client handshake
  verifyWebhookSignature(input): boolean;   // incoming webhook
  parseWebhook(rawBody): NormalizedWebhookEvent;
}

Resolve adapters via the GatewayRegistry (services do this for you); it falls back to defaultGateway when a provider isn't specified.

Implementation status: the signature-verification methods are fully implemented. The API calls (createOrder, capturePayment, refund, fetchPayment) throw a self-describing GatewayException until you wire each provider's SDK/HTTP client into the adapter.

Add your own gateway by extending BaseGatewayAdapter and passing the instance to gateways: [...].


Webhooks

A ready-made endpoint is registered at POST /webhooks/:provider (disable with webhookController: false and call WebhookService.handle() from your own route).

React to events two ways:

// Option A — platform decorator, discovered automatically
import { WebhookEvent, WebhookEventType } from '@codecanva/payment-platform';

@Injectable()
export class PaymentWebhooks {
  @WebhookEvent(WebhookEventType.PAYMENT_CAPTURED)
  onCaptured(event) { /* ... */ }
}

// Option B — standard NestJS event listener (same events are emitted on the bus)
import { OnEvent } from '@nestjs/event-emitter';

@OnEvent(WebhookEventType.PAYMENT_CAPTURED)
handle(event) { /* ... */ }

The order of operations is security-critical and enforced by the platform: verify signature → parse → dispatch. Nothing downstream sees an unverified payload.


Events

Emitted via the typed PlatformEventBus and consumable with @OnEvent:

| Event | Name | |---|---| | PaymentCreatedEvent | payment.created | | PaymentSucceededEvent | payment.succeeded | | PaymentFailedEvent | payment.failed | | RefundCreatedEvent | refund.created | | RefundProcessedEvent | refund.processed | | SubscriptionRenewedEvent | subscription.renewed | | SubscriptionCancelledEvent | subscription.cancelled |

@OnEvent('payment.succeeded')
handleSuccess(event: PaymentSucceededEvent) {
  sendReceiptEmail(event.payment);
}

Storage (bring your own DB)

The platform defines persistence ports; you provide the implementation. All are optional and injected with @Optional() — unbound ports simply mean "not persisted".

| Port | Token | Used by | |---|---|---| | PaymentStorage | PAYMENT_STORAGE | payment, refund | | SubscriptionStorage | SUBSCRIPTION_STORAGE | subscription | | InvoiceStorage | INVOICE_STORAGE | invoice |

import { PaymentStorage, CreatePaymentRecord, PaymentRecord } from '@codecanva/payment-platform';

export class TypeOrmPaymentStore implements PaymentStorage {
  async createPayment(data: CreatePaymentRecord): Promise<PaymentRecord> { /* ... */ }
  async updatePayment(id, patch) { /* ... */ }
  async findPaymentById(id) { /* ... */ }
  async findPaymentByGatewayId(gatewayPaymentId) { /* ... */ }
  async createRefund(data) { /* ... */ }
  async updateRefund(id, patch) { /* ... */ }
  async findRefundById(id) { /* ... */ }
}

// then: PaymentPlatformModule.forRoot({ ..., storage: { payment: new TypeOrmPaymentStore() } })

Subscriptions & invoicing

const plan = await planService.createPlan({
  name: 'Pro', amount: 99900, currency: Currency.INR,
  interval: BillingInterval.MONTH, intervalCount: 1, trialDays: 14,
});

const sub = await subscriptionService.create({ planId: plan.id, customerId: 'cus_1' });
await subscriptionService.renew(sub.id);
await subscriptionService.cancel(sub.id, { atPeriodEnd: true });

const invoice = await invoiceService.generate({
  customerId: 'cus_1', currency: Currency.INR,
  lineItems: [{ description: 'Pro plan', quantity: 1, unitAmount: 99900, taxRate: 18 }],
  intraState: true, // CGST + SGST; false → IGST
});

Subscription/invoice stateful operations require their respective storage ports. InvoiceService.renderPdf() is a stub — plug in your preferred renderer.


Configuration reference

PaymentPlatformModule.forRoot(options):

| Option | Type | Description | |---|---|---| | gateways | PaymentGateway[] | Required. Configured adapter instances. | | defaultGateway | GatewayProvider | Fallback provider. Defaults to gateways[0]. | | storage | { payment?, subscription?, invoice? } | Persistence implementations (all optional). | | invoice | { sellerGstin?, defaultTaxRate?, intraStateByDefault? } | Tax/invoice config. | | webhookController | boolean | Register POST /webhooks/:provider. Default true. |

Money is always expressed in the currency's minor unit (paise/cents) as integers, to avoid floating-point drift.


Development

npm install
npm run build         # nest build (tsc) → dist/
npm run start:dev     # run the local dev harness (src/app.module.ts)
npm run test          # jest
npm run lint

The repo includes a small dev harness (app.module.ts, main.ts) showing the library wired up; it's excluded from the published package (files: ["dist"]).


What's implemented vs. left to you

| Implemented | Left for you (clearly stubbed) | |---|---| | Gateway interface, registry, adapter scaffolds | Adapter API calls (real provider SDK/HTTP) | | Webhook signature verify + normalize + route | Concrete storage port implementations | | Payment/refund/subscription/invoice service logic | InvoiceService.renderPdf() | | GST/tax calculation, money & HMAC utils | Provider credentials & DB | | Typed events + event bus | |

Stubs throw a descriptive error rather than silently no-op'ing, so partially-wired adapters fail loudly.


License

UNLICENSED (update before publishing).