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

@ambushsoftworks/nestjs-payments-graphql

v0.1.2

Published

NestJS payments module with GraphQL support — invoicing, payment processing, recurring billing, and email notifications

Downloads

259

Readme

@ambushsoftworks/nestjs-payments-graphql

Production-grade payments module for NestJS with GraphQL support. Invoicing, Stripe payment processing, refunds, payment plans, recurring invoicing with auto-charge, e-transfer, and composable email notifications — with zero database coupling.

Table of Contents


Design Principles

  • Interface-driven persistence — All storage is behind interfaces (IInvoiceRepository, IPaymentRepository, IRefundRepository, etc.). Bring your own ORM; Prisma is the expected default, but any ORM works.
  • Instance-based DI — Repositories and adapters are injected as instances via useFactory, not as classes.
  • Framework-agnostic core — Services are pure business logic. Resolvers, schedulers, and Prisma adapters live in your application.
  • Consumer owns GraphQL decoration — The package ships @ObjectType/@InputType classes. You build resolvers that call services and return these types.
  • Composable email — A sender (transport) plus a renderer (HTML) plus a branding resolver plus a client resolver. All but the first two are optional with sane defaults.
  • Stripe is bundled, not required — The Stripe gateway and webhook controller ship with the package, but are only activated when stripe config is provided.

Installation

npm install @ambushsoftworks/nestjs-payments-graphql
# or
pnpm add @ambushsoftworks/nestjs-payments-graphql

Peer Dependencies

npm install @nestjs/common @nestjs/core @nestjs/graphql \
  class-transformer class-validator graphql graphql-type-json \
  reflect-metadata rxjs

stripe is bundled as a hard dependency of this package — you do not need to install it separately.


Quick Start

A minimal setup: invoices, manual payments, and a Stripe gateway.

1. Implement the required repositories

Each repository is a pure TypeScript interface backed by your ORM. Here is the sketch for Prisma. A ready-to-copy reference schema lives at prisma/payment-models.prisma and ships inside the npm tarball — it contains every model and enum you need, with consumer-owned relations marked // CONSUMER:.

// prisma-invoice.repository.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import {
  IInvoiceRepository,
  UniqueConstraintViolationException,
} from '@ambushsoftworks/nestjs-payments-graphql';

@Injectable()
export class PrismaInvoiceRepository implements IInvoiceRepository {
  constructor(private readonly prisma: PrismaService) {}

  async create(data) {
    try {
      return await this.prisma.invoice.create({ data });
    } catch (e: any) {
      if (e.code === 'P2002') throw new UniqueConstraintViolationException();
      throw e;
    }
  }

  async findById(id) { return this.prisma.invoice.findUnique({ where: { id } }); }
  // ... implement the rest of IInvoiceRepository
  async withTransaction(fn) {
    return this.prisma.$transaction(async (tx) => {
      const scoped = new PrismaInvoiceRepository({ ...this.prisma, invoice: tx.invoice } as any);
      return fn(scoped);
    });
  }
}

Repositories you must implement:

| Interface | Purpose | |-----------|---------| | IInvoiceRepository | Invoice + line item persistence | | IPaymentRepository | Payment records | | IRefundRepository | Refund records | | IPaymentConfigRepository | Per-division payment config + providers | | IWebhookIdempotencyRepository | Dedupe Stripe webhook deliveries | | ITransactionManager | Multi-repo transaction coordinator |

Opt-in interfaces:

| Interface | Required when | |-----------|---------------| | IPaymentPlanRepository | features.paymentPlans = true | | IRecurringInvoiceRepository | features.recurringInvoices = true | | IPaymentCustomerRepository | features.recurringInvoices = true |

2. Register the module

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PaymentsModule } from '@ambushsoftworks/nestjs-payments-graphql';

@Module({
  imports: [
    ConfigModule.forRoot(),
    PaymentsModule.forRootAsync({
      imports: [ConfigModule, MyRepositoriesModule, MyAdaptersModule],
      inject: [
        PrismaInvoiceRepository,
        PrismaPaymentRepository,
        PrismaRefundRepository,
        PrismaPaymentConfigRepository,
        PrismaWebhookIdempotencyRepository,
        PrismaTransactionManager,
        ConfigService,
      ],
      useFactory: (invoices, payments, refunds, config, webhooks, tx, cfg) => ({
        invoiceRepositoryInstance: invoices,
        paymentRepositoryInstance: payments,
        refundRepositoryInstance: refunds,
        paymentConfigRepositoryInstance: config,
        webhookIdempotencyRepositoryInstance: webhooks,
        transactionManagerInstance: tx,

        stripe: {
          secretKey: cfg.get('STRIPE_SECRET_KEY'),
          webhookSecret: cfg.get('STRIPE_WEBHOOK_SECRET'),
        },

        defaultCurrency: 'USD',
      }),
    }),
  ],
})
export class AppModule {}

3. Enable rawBody for Stripe signature verification

Stripe's webhook signature check requires the unparsed request body.

// main.ts
const app = await NestFactory.create(AppModule, { rawBody: true });

4. Exempt the webhook path from auth

The controller is registered at POST /webhooks/stripe and marked with @PaymentsWebhook(). Your auth/tenant guards must respect this metadata. Either:

  • Have guards check Reflector.get(PAYMENTS_WEBHOOK, handler) and bail out early, or
  • Have guards check the route path against the exported PAYMENTS_WEBHOOK string constant.

See Critical Integration Requirements.

5. Build a resolver

import { Resolver, Mutation, Args } from '@nestjs/graphql';
import {
  InvoiceService,
  InvoiceModel,
  CreateInvoiceInput,
} from '@ambushsoftworks/nestjs-payments-graphql';

@Resolver()
export class InvoiceResolver {
  constructor(private readonly invoices: InvoiceService) {}

  @Mutation(() => InvoiceModel)
  async createInvoice(
    @Args('input') input: CreateInvoiceInput,
    // resolve divisionId + userId from your auth/tenant context
  ) {
    return this.invoices.createInvoice(divisionId, input, userId);
  }
}

Architecture

┌──────────────────────────────────────────────────────────────┐
│  Consumer App (e.g. Ariadne API)                             │
│  ──────────────────────────────                              │
│  • Prisma repositories (implements IInvoiceRepository, ...)  │
│  • Resolvers (GraphQL queries/mutations)                     │
│  • Schedulers (@nestjs/schedule)                             │
│  • Auth guards (respect @PaymentsWebhook metadata)           │
│  • Event listener (implements IPaymentEventListener)         │
│  • Email adapters (IPaymentEmailSender, IPaymentClient...)   │
│                                                              │
│  Wires together via PaymentsModule.forRootAsync({ ... })     │
└──────────────────────────────────────────────────────────────┘
                              ↓ imports
┌──────────────────────────────────────────────────────────────┐
│  @ambushsoftworks/nestjs-payments-graphql                    │
│  ─────────────────────────────────────────                   │
│  • Services (pure business logic)                            │
│  • Interfaces (what consumer must implement)                 │
│  • DI tokens (Symbols)                                       │
│  • GraphQL models + input types                              │
│  • StripeGateway + StripeWebhookController (opt-in)          │
│  • Exceptions                                                │
└──────────────────────────────────────────────────────────────┘

The dynamic module is registered as global: true — any module in your app can inject services and tokens without re-importing.


Features

Invoicing

  • Create DRAFT invoices with optional line items.
  • Line item edits allowed only in DRAFT; totals auto-recalculate on mutation.
  • InvoiceNumberService generates globally-unique numbers against a per-division prefix, retrying on unique-constraint collisions up to 5 times.
  • Tax rate resolution: use the value passed on createInvoice if provided, otherwise PaymentConfig.defaultTaxRate for the division, otherwise 0.
  • Status transitions: DRAFT → SENT → {PARTIALLY_PAID, PAID, OVERDUE, VOID, REFUNDED}.
  • Invoice metadata is a Stripe-style Record<string, string> for consumer-specific context.

Stripe Gateway & Webhooks

When stripe config is provided, the module registers StripeGateway and mounts POST /webhooks/stripe. Webhook security relies entirely on Stripe signature verification. Behaviour:

  • Idempotency — every event ID is recorded via IWebhookIdempotencyRepository before the handler runs; replays no-op.
  • Retriespayment_intent.succeeded events retry up to 3 times with a 2-second delay if the corresponding Payment row has not yet been created.
  • Error reporting — configure webhook.errorReporter to pipe exceptions into Sentry/GlitchTip/etc.

GatewayRegistryService keeps a runtime lookup of registered gateways so the package can work without @Optional() @Inject().

Refunds

Full and partial refunds through Stripe (when the originating payment went through Stripe) or manual refund records. All amounts in cents.

Payment Plans

Split an invoice into installments with per-installment due dates.

Enable:

features: { paymentPlans: true },
paymentPlanRepositoryInstance: paymentPlanRepo,

Each installment can optionally reference a generated invoice; the service tracks PENDING → INVOICED → PAID → OVERDUE → WAIVED → CANCELLED.

Recurring Invoices

Auto-generates invoices on a recurring schedule. Supports DAYS and MONTHS interval units anchored to an original date (handles month-end correctly via computeNextInvoiceDate).

Enable:

features: { recurringInvoices: true },
recurringInvoiceRepositoryInstance: recurringRepo,
paymentCustomerRepositoryInstance: paymentCustomerRepo,

Phase 2 auto-charge: when autoCharge: true, the scheduler pulls the stored PaymentCustomer for the client, issues a charge via the gateway, and fires either onRecurringInvoiceGenerated or onRecurringPaymentFailed. Consecutive failures can auto-pause the schedule (onRecurringInvoicePaused).

Utility helpers exported:

import {
  computeNextInvoiceDate,
  computeCycleCount,
} from '@ambushsoftworks/nestjs-payments-graphql';

The consumer provides the scheduler (typically @nestjs/schedule) that calls RecurringInvoiceService.generateDue() and friends on a cron.

E-Transfer

Canadian Interac e-transfer flow: ETransferService generates human-readable codes (<prefix>-<invoice>-<random>) and security answers, then exposes GraphQL instructions for the client.

Enable:

features: { eTransfer: true },
eTransferCodePrefix: 'ARD-', // whatever short prefix makes sense for your brand

Email Notifications

Composable: transport + rendering + branding + client lookup.

import {
  PaymentsModule,
  StaticBrandingResolver,
  DefaultPaymentEmailTemplateRenderer,
} from '@ambushsoftworks/nestjs-payments-graphql';

PaymentsModule.forRootAsync({
  useFactory: (..., emailSender, clientResolver) => ({
    // ... required deps
    features: { emailNotifications: true },
    email: {
      senderInstance: emailSender,
      clientResolverInstance: clientResolver,
      brandingResolverInstance: new StaticBrandingResolver({
        appName: 'My App',
        primaryColor: '#1976D2',
        fromEmail: '[email protected]',
        fromName: 'My App Billing',
        companyName: 'My App Inc.',
        supportEmail: '[email protected]',
      }),
      templateRendererInstance: new DefaultPaymentEmailTemplateRenderer(),
    },
  }),
});

You implement:

  • IPaymentEmailSender — a send() method that posts to your transport (Resend, SendGrid, SES, etc.).
  • IPaymentClientResolver — returns { email, name } | null for a clientDetailsId. Returning null skips that email silently.

You can override:

  • IPaymentEmailBrandingResolver — per-division branding. Use StaticBrandingResolver for single-tenant apps.
  • IPaymentEmailTemplateRenderer — full HTML control (e.g. React Email, MJML, Handlebars). DefaultPaymentEmailTemplateRenderer ships inline-styled responsive templates.

Covered emails: invoice sent, e-transfer instructions, payment confirmation, overdue reminder, refund confirmation.

Event Listeners

Register a single IPaymentEventListener to react to domain events without touching services. All handlers except onInvoicePaid are optional.

paymentEventListenerInstance: {
  async onInvoicePaid(event) { /* fulfill the order, confirm the booking, ... */ },
  async onInvoiceRefunded(event) { /* undo fulfillment */ },
  async onRecurringInvoiceGenerated(event) { /* notify, audit */ },
  async onRecurringPaymentFailed(event) { /* alert, retry */ },
  async onRecurringInvoicePaused(event) { /* notify billing team */ },
}

Event payloads include metadata from the invoice plus the full lineItems snapshot so consumers don't need to re-query.

Transactions

ITransactionManager.runInTransaction(fn) gives the callback a TransactionRepositories bag with all repositories bound to the same ORM transaction. The package uses this internally when recording payments and mutating totals.

await transactionManager.runInTransaction(async (repos) => {
  const invoice = await repos.invoices.findByIdForUpdate(id);
  await repos.payments.create({ ... });
  await repos.invoices.update(id, { amountPaid: invoice.amountPaid + amount });
});

Configuration Reference

Required Options

| Option | Type | Description | |--------|------|-------------| | invoiceRepositoryInstance | IInvoiceRepository | Invoice + line item persistence | | paymentRepositoryInstance | IPaymentRepository | Payment records | | refundRepositoryInstance | IRefundRepository | Refund records | | paymentConfigRepositoryInstance | IPaymentConfigRepository | Per-division payment config | | webhookIdempotencyRepositoryInstance | IWebhookIdempotencyRepository | Stripe webhook dedupe | | transactionManagerInstance | ITransactionManager | Multi-repo transactions |

Optional Repositories

| Option | Required when | |--------|---------------| | paymentPlanRepositoryInstance | features.paymentPlans is enabled | | recurringInvoiceRepositoryInstance | features.recurringInvoices is enabled | | paymentCustomerRepositoryInstance | features.recurringInvoices is enabled |

Stripe Options

stripe: {
  secretKey: string;
  webhookSecret: string;
}

Omit the entire stripe key for non-Stripe deployments. A warning logs at startup if no gateway is configured.

Email Options

email: {
  senderInstance: IPaymentEmailSender;        // required
  clientResolverInstance: IPaymentClientResolver; // required
  brandingResolverInstance?: IPaymentEmailBrandingResolver;  // optional
  templateRendererInstance?: IPaymentEmailTemplateRenderer;  // optional
}

Required when features.emailNotifications is enabled.

Defaults & Feature Flags

| Option | Type | Default | Description | |--------|------|---------|-------------| | defaultCurrency | string | 'USD' | ISO 4217 currency used when an invoice omits one | | eTransferCodePrefix | string | '' | Short prefix prepended to generated e-transfer codes | | webhook.errorReporter | (err, ctx) => void | — | Forward webhook errors to Sentry/GlitchTip/etc. |

| Flag | Default | Description | |------|---------|-------------| | features.paymentPlans | false | Enable payment-plan services + GraphQL types | | features.recurringInvoices | false | Enable recurring invoice services + auto-charge | | features.eTransfer | false | Enable e-transfer code generation and instruction emails | | features.emailNotifications | false | Enable outbound email; requires email config |

Validation runs when the module boots. Enabling a feature without providing the backing repository or config throws at startup.


DI Tokens

All DI tokens are exported from the package as Symbols. Use them with ModuleRef.get() (see Critical Integration Requirements).

import {
  INVOICE_REPOSITORY,
  PAYMENT_REPOSITORY,
  REFUND_REPOSITORY,
  PAYMENT_PLAN_REPOSITORY,
  PAYMENT_CONFIG_REPOSITORY,
  WEBHOOK_IDEMPOTENCY_REPOSITORY,
  RECURRING_INVOICE_REPOSITORY,
  PAYMENT_CUSTOMER_REPOSITORY,
  TRANSACTION_MANAGER,
  PAYMENT_EVENT_LISTENER,
  PAYMENT_EMAIL_SENDER,
  PAYMENT_CLIENT_RESOLVER,
  PAYMENT_EMAIL_BRANDING_RESOLVER,
  PAYMENT_EMAIL_TEMPLATE_RENDERER,
  WEBHOOK_ERROR_REPORTER,
  DEFAULT_CURRENCY,
  E_TRANSFER_CODE_PREFIX,
} from '@ambushsoftworks/nestjs-payments-graphql';

Reserved GraphQL Type Names

The package registers these names on module import. Consumers must not redefine or re-register them, or GraphQL will throw "type already registered".

Object types: Invoice, InvoiceLineItem, InvoiceClient, Payment, Refund, PaymentPlan, PaymentPlanInstallment, PaymentConfig, PaymentProvider, PaymentMethodSetup, ETransferInstructions, OnlinePaymentSession, RecurringInvoice, RecurringInvoiceLineItem, PaginatedInvoices, PaginatedPaymentPlans, PaginatedRecurringInvoices

Enums: InvoiceStatus, PaymentMethod, PaymentStatus, RefundStatus, PaymentPlanStatus, InstallmentStatus, RecurringIntervalUnit, RecurringInvoiceStatus

Input types: CreateInvoiceInput, CreateLineItemInput, CreateRefundInput, CreateInstallmentInput, CreatePaymentPlanInput, CreateRecurringInvoiceInput, CreateRecurringLineItemInput, RecordManualPaymentInput, UpdatePaymentConfigInput, UpdateInvoiceMetadataInput, UpdateRecurringTemplateInput, UpsertPaymentProviderInput, InvoiceFilterInput, PaymentPlanFilterInput, RecurringInvoiceFilterInput


Exceptions

All exceptions extend PaymentException (a plain Error subclass) with a stable .code string so consumers can map them to GraphQL/HTTP status codes.

| Exception | .code | |-----------|---------| | InvalidInvoiceStateException | INVALID_INVOICE_STATE | | PaymentAmountExceededException | PAYMENT_AMOUNT_EXCEEDED | | PaymentGatewayException | PAYMENT_GATEWAY_ERROR | | DuplicatePaymentException | DUPLICATE_PAYMENT | | InvoiceNotPayableException | INVOICE_NOT_PAYABLE | | InvoiceNumberExhaustedException | INVOICE_NUMBER_EXHAUSTED | | RefundNotAllowedException | REFUND_NOT_ALLOWED | | InvalidPaymentStateException | INVALID_PAYMENT_STATE | | InvalidRecurringInvoiceStateException | INVALID_RECURRING_INVOICE_STATE | | UniqueConstraintViolationException | UNIQUE_CONSTRAINT_VIOLATION |

Repository adapters are responsible for translating ORM-specific unique-constraint errors (e.g. Prisma P2002) into UniqueConstraintViolationException so core services stay ORM-agnostic.


Critical Integration Requirements

1. Use ModuleRef.get() for package tokens

There is a known NestJS bug where @Optional() @Inject(TOKEN) combined with forwardRef() module imports delivers null even when the provider exists. The package itself works around this internally. Consumer code that needs a package token must do the same:

// BAD — will fail with "Nest can't resolve dependencies"
constructor(@Inject(WEBHOOK_IDEMPOTENCY_REPOSITORY) private readonly repo: IWebhookIdempotencyRepository) {}

// GOOD — resolve lazily in onModuleInit
constructor(private readonly moduleRef: ModuleRef) {}
onModuleInit() {
  this.repo = this.moduleRef.get(WEBHOOK_IDEMPOTENCY_REPOSITORY, { strict: false });
}

2. Do not re-register GraphQL enums

PaymentsModule imports register-payment-enums.ts as a side effect, which calls registerEnumType() for every enum listed under Reserved GraphQL Type Names. Remove any duplicate registerEnumType() calls from your app.

3. Exempt /webhooks/stripe from auth

The webhook controller mounts at a fixed path and is decorated with @PaymentsWebhook(). Your auth/tenant guards must either:

  • Check for the PAYMENTS_WEBHOOK metadata key via Reflector, or
  • Skip the route by path (use the exported PAYMENTS_WEBHOOK constant if you match on string keys).

@PaymentsWebhook() also sets isPublic and skipTenant metadata, so guards that honour those keys work out of the box.

4. Enable rawBody in NestFactory.create()

Stripe signature verification requires the unparsed request body:

const app = await NestFactory.create(AppModule, { rawBody: true });

5. Translate ORM errors to UniqueConstraintViolationException

Repository adapters must catch ORM-specific unique constraint errors and rethrow as UniqueConstraintViolationException so core services (especially InvoiceNumberService) can apply their retry logic.

6. Use "moduleResolution": "nodenext" friendly imports

The package.json has an explicit exports field, so nodenext resolution is supported. If you use node16/nodenext, you do not need to opt into any special import syntax.


Local Development

Developing the package alongside a consumer

Terminal 1 — package watch mode:

cd nestjs-payments-graphql
pnpm link --global
pnpm run build:watch

Terminal 2 — consumer:

cd my-app
pnpm link --global @ambushsoftworks/nestjs-payments-graphql
pnpm start:dev

Before committing or deploying the consumer:

cd my-app
pnpm uninstall @ambushsoftworks/nestjs-payments-graphql
pnpm install

Do not commit package.json or pnpm-lock.yaml with a link: or workspace: reference to this package. BuildKit-based deploys (e.g. Railway) cannot resolve those references.

Publishing

npm version patch   # or minor/major
git push --follow-tags

GitLab CI publishes to npm via OIDC provenance on tag push. See .gitlab-ci.yml for the pipeline.


License

MIT — see LICENSE.