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

@birtalanrobert/billing

v7.1.1

Published

Our own revenue: plans, subscriptions, seat and usage metering, invoicing and the plan-feature gate

Readme

@birtalanrobert/billing

What a customer pays us: a business for its plan, or a player for a pack.

Not to be confused with @birtalanrobert/commerce, which is the other direction: a business taking money from its own customers through Stripe Connect, with the funds never touching us. They share a vendor and almost nothing else — different account, different liability, different answer when one fails. Conflating them is the mistake that makes both hard to reason about.

Pricing a plan, without a database

import { priceFor, withinLimit, isEntitled } from '@birtalanrobert/billing';

// Five staff included, then graduated bands.
priceFor(plan, 7); // 14_900 + 2 × 2_000

withinLimit(plan, 'locations', 1); // false — one is the ceiling
isEntitled('past_due'); // true, and deliberately so

past_due entitles, because a card expires on a Sunday and nobody reads the email until Tuesday. A salon whose diary stopped in between has lost bookings it cannot recover, over an amount it fully intended to pay. How long that is tolerated is dunningStage, and suspend withholds the service without deleting anything.

Putting a business on a plan without a payment page

await billing.assign(tenantId, { planCode: 'chain', quantity: 9 });

A chain is sold to rather than checked out, a pilot runs for three months on somebody's word, and a business migrating from a competitor is put on the plan it agreed to before a card is ever entered. It is also the only way a deployment with no provider configured can have a subscription at all — which is every deployment before the Stripe account exists.

It refuses to touch a subscription the provider owns: once a card is being charged on a schedule, a plan code written beside it makes our screen and their invoice disagree. Changing how many is setQuantity, which tells the provider. Who assigned it and why is the caller's to record — this package knows nothing about operators.

Selling a pack, rather than a plan

A premium currency in packs, a ticket, a wedding: one payment, through the same hosted checkout, priced by its amount rather than by a price kept in the provider's dashboard.

await provider.checkout({
  mode: 'payment',
  customer: 'cus_…',
  price: {
    amount: 999, // €9.99, the tax inside it
    currency: 'EUR',
    name: 'A purse: 260 Silver',
    taxBehavior: 'inclusive',
    taxCode: 'txcd_…',
  },
  subject: 'account:…',
  metadata: { purchase: '…' },
  locale: 'ro',
  reference: '…', // the provider's idempotency key
  successUrl: '…',
  cancelUrl: '…',
});

await provider.refund({ payment: 'pi_…', reference: 'refund-…' });

await provider.deleteCustomer('cus_…'); // for an erasure: the payments stay
  • The amount is the one the product shows. A pack whose price an operator edits in the product's settings is charged as written; a price id in the dashboard would be a second copy of the figure, and the two drift the first time somebody edits one of them.
  • Metadata rides on the payment as well as the session, because a refund or a dispute names the payment and never the session.
  • A completed checkout is not always a paid one. A bank debit completes the session days before the money arrives; paid says which, and its own event says when it did.
  • Every refund is heard, ours or not. charge.refunded arrives for one made in the provider's dashboard too, with how much has gone back altogether, and a dispute is read by the money it moves — withdrawn, or reinstated when it is won.
  • eventId is the same on every delivery, which is what a product keys on to act on a webhook delivered twice once.
  • An erasure deletes the customer, and with it the address and name the provider was given; the payments stay, as accounting law requires. One already deleted is forgotten all the same, so an erasure retried after a timeout does not fail on its first attempt. NoBilling refuses, as it refuses everything: a customer made by a provider the deployment no longer has cannot be said to be forgotten.

Taking the payment in the product's own page

When buyers should not leave the product to pay, the card form is Stripe's Payment Element, mounted in the product's page with no intent yet. On submit the page makes a ConfirmationToken of what the buyer gave and hands it to the server, which decides the amount and confirms:

const paid = await provider.pay({
  customer: 'cus_…',
  price: {
    amount: 999, // €9.99, the tax inside it
    currency: 'EUR',
    name: 'A purse: 260 Silver',
    taxBehavior: 'inclusive',
    taxCode: 'txcd_…',
  },
  confirmationToken: 'ctoken_…', // from stripe.createConfirmationToken in the page
  reference: '…', // the payment's idempotency key
  metadata: { purchase: '…' },
});
// paid.status: 'succeeded' | 'processing' | 'requires_action' | 'failed' | 'canceled'

await provider.payment('pi_…'); // asked again when the page says it is done
  • The tax is still Stripe's. pay works out the VAT from the billing address in the token — a country is all VAT asks of a buyer outside the United States and Canada — and attaches the calculation to the PaymentIntent, so Stripe records the tax transaction when the payment succeeds and reverses it on a refund, made here or in its dashboard. An address it cannot place is thrown as PaymentLocationError, before anything is charged. The tax worked out rides on the payment, so the payment read back and its events say it too — for a webhook that arrives before pay has answered.
  • A card refused is an answer, not an error. It comes back failed with Stripe's code, the issuer's reason and Stripe's sentence; the buyer tries another card, as a payment of its own under a reference of its own. Anything else is thrown.
  • One reference is one payment. Asked again under it — a retry after a timeout, or after Stripe failed — pay is answered with the payment already made, never a second; asked with another card while the first may have been made, Stripe refuses it rather than charging twice. A failure thrown is not a payment refused: what was asked may have been done all the same, which the payment's own event then says — so the caller retries under the same reference, never a new one. An address corrected after PaymentLocationError keeps it, nothing having been asked under it yet: the tax calculation is keyed by what the buyer gave as well.
  • requires_action carries a client secret, for the page to finish a 3-D Secure check with stripe.handleNextAction — and is the only status that does.
  • A dispute's tax is the product's to reverse. Stripe reverses a refund's tax by itself, never a dispute's: reverseTax reverses the tax of the money a dispute withdrew — a partial reversal, by that amount — and restoreTax undoes it when the dispute is won, by reversing that reversal in full, as Stripe undoes a reversal. A payment that recorded no tax has nothing to reverse.
  • A payment's own events — payment_intent.succeeded, processing, payment_failed, canceled — read as kind: 'payment', paid only on success and failed on a refusal or a cancellation, with the metadata the payment carried.

Using it in a NestJS application

import { NoBilling } from '@birtalanrobert/billing';
import { StripeBilling } from '@birtalanrobert/billing/stripe';
import {
  BILLING_PROVIDER,
  BillingService,
  billingEntities,
  billingMigrations,
} from '@birtalanrobert/billing/nestjs';

@Module({
  providers: [
    {
      provide: BILLING_PROVIDER,
      inject: [ConfigModule.token()],
      // A deployment with no key still has to start. Constructing the vendor's
      // client with an empty string throws inside its own constructor.
      useFactory: (config: AppConfig) =>
        config.BILLING_SECRET_KEY
          ? new StripeBilling({ secretKey: config.BILLING_SECRET_KEY })
          : new NoBilling(),
    },
    BillingService,
  ],
})
export class SubscriptionModule {}

Register billingEntities and billingMigrations with the data source, as with every other package here.

Three entry points, and the split is deliberate. The root is pure — what a plan costs, what it permits, what to do about an unpaid invoice — because a console decides all three while somebody is still choosing. /nestjs holds the service, the entities and the migrations. /stripe holds the vendor's client on its own, because importing it pulls in the whole SDK.

The parts worth knowing before using it

  • A plan is a row, and a price change is a new row. A business that bought "Salon, 149 lei, five staff included" in March keeps those terms in April. A subscription stores the plan's code, never a key into something editable.
  • Tiers are graduated, not volume. "Five included, then twenty each" charges twenty for the sixth, not twenty for all six. Past the last band, the last band's rate keeps applying — the alternative is an invoice that has been wrong in the customer's favour for a year.
  • Absence is the gate. No deny list: a feature nobody added to a plan is one nobody can use, which is noticed at once.
  • Usage is counted here and reported in a batch. A provider call on the path of every text message sent is a provider outage that stops the product.
  • Two ways to count, and the product picks by what it is counting. record adds, for an event as it happens — a message sent, a pack bought. recount sets, for a table read on a schedule — tickets issued yesterday — where adding would double the day on a second run and could never take a refunded ticket off again. usageBetween reads a period back, because the table has a policy on it and a product summing it for itself would read nothing and report success.
  • The subscription row is written when the provider says money moved, not on the way to the payment page — that is a subscription for somebody who closed the tab.
  • A webhook about a subscription we have no record of is ignored, never inserted: it belongs to another environment sharing the provider account.