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

@wtfalch/invoicing

v0.1.1

Published

Norwegian invoices: gapless per-org numbering, credit notes, MVA (VAT) computed and rounded per rate, line items as first-class input, HTML documents, a period CSV export and a typed EHF seam.

Readme

@wtfalch/invoicing

Norwegian invoices: gapless per-org numbering, credit notes, MVA (Norwegian VAT) computed and rounded correctly per rate, line items as first-class input, an HTML document, a period CSV export with the VAT termin helper, and a typed EHF seam. It is not an accounting system and it never files anything with the government -- see the root README for scope.

Install

pnpm add @wtfalch/invoicing
pnpm exec invoicing-migrations   # copies migrations/*.sql into drizzle/ as the next numbers

Apply the copied file with the host's own migrate script, or call migrate() against any Queryable for a quick local setup:

import { migrate } from '@wtfalch/invoicing';

await migrate(db); // idempotent

@wtfalch/authz is an optional peer, scaffolded per the estate's package template: the invoicing catalogue offers invoicing:issue (write) and invoicing:read (read). A host that does not use @wtfalch/authz can ignore catalogue/checkInvoicingRead/checkInvoicingIssue entirely and authorize calls its own way -- this package never calls them itself.

Issuing an invoice

Seller and buyer identity is plain data the caller supplies on every call -- this package has no counterparty store. orgNumber is optional: a private-person buyer or a foreign counterparty may not have one.

import { issueInvoice } from '@wtfalch/invoicing';

const invoice = await issueInvoice(db, {
  organisationId,
  currency: 'NOK',
  seller: {
    legalName: 'Factory AS',
    orgNumber: '123456789',
    vatRegistered: true,
    address: { street: 'Fabrikkveien 1', postalCode: '0001', city: 'Oslo', country: 'NO' },
  },
  buyer: {
    legalName: 'Customer AS',
    orgNumber: '987654321',
    address: { street: 'Kundeveien 2', postalCode: '0002', city: 'Bergen', country: 'NO' },
  },
  lines: [
    { description: 'Consulting, September', quantity: '10', unitPriceMicros: '1000000000', vatRate: 25 },
  ],
  dueDate: '2026-10-31',       // mandatory: "vederlag og betalingsforfall"
  deliveryDate: '2026-09-23',  // mandatory: "tidspunkt ... for levering"
  deliveryPlace: 'Oslo',       // mandatory: "... og sted for levering"
  reverseCharge: false,        // true forces every line to vatRate 0
});

invoice.number;       // "1", "2", ... -- gapless per (organisationId, 'invoice')
invoice.netMicros;    // sum of line nets, in @wtfalch/ledger's micros
invoice.vatMicros;    // VAT rounded once per rate group -- see "VAT rounding" below
invoice.grossMicros;
invoice.vatBreakdown; // [{ rate, netMicros, vatMicros }], one entry per rate used

unitPriceMicros and every other money amount is @wtfalch/ledger's Money.micros -- convert with that package's toMicros/fromMicros, e.g. toMicros('100.00'). quantity is a plain decimal string ("2.5" hours is fine); it is not money and is not validated against microsSchema.

Line items are first-class input, independent of @wtfalch/billing: a host that computes its own line items (e.g. manage, invoicing a customer's own customer) calls issueInvoice directly with lines it built itself.

VAT rounding

The MVA split by rate (vatBreakdown) rounds once per rate group, from that group's unrounded net sum -- never by summing each line's individually-rounded VAT. Three lines of 3.33 net at 25% sum to 9.99 net; the correct VAT is round(9.99 × 0.25) = 2.50, not round(3.33 × 0.25) × 3 = 0.83 × 3 = 2.49. netMicros/vatMicros/grossMicros on the returned document are the sums of the already-rounded vatBreakdown entries, so they always agree with what a reader gets by adding up the printed table.

Credit notes

import { issueCreditNote } from '@wtfalch/invoicing';

const creditNote = await issueCreditNote(db, {
  organisationId,
  invoiceId: invoice.id,
  reason: 'Wrong quantity billed',
  // lines: [...] -- omit for a full reversal (mirrors the invoice exactly)
});

creditNote.number;               // its own gapless series: (organisationId, 'credit_note')
creditNote.referencesDocumentId; // invoice.id
creditNote.dueDate;              // always null -- a credit note creates no new payment obligation

A credit note mirrors the original invoice's seller, buyer, currency, delivery date/place and reverse-charge flag. Omit lines for a full reversal of the invoice's own lines; pass lines for a partial credit. It can only be issued against an invoice, never against another credit note.

Documents and export

import { renderInvoiceHtml, exportPeriod, mvaTermin } from '@wtfalch/invoicing';

renderInvoiceHtml(invoice); // a self-contained HTML document

// evori (the planned accounting system) has no API yet: a CSV summary plus
// each document's HTML, for manual import.
const period = await exportPeriod(db, organisationId, { start: '2026-09-01', end: '2026-10-01' });
period.csv;       // header + one row per invoice/credit note in the range
period.documents; // [{ id, kind, number, html }]

mvaTermin('2026-09-23'); // { number: 5, start: '2026-09-01', end: '2026-11-01', label: '2026 termin 5 (sep-okt)' }

exportPeriod groups by each document's issueDate (mva-loven §15-1's tidfesting rule), not by when the row was created. mvaTermin identifies Norway's six bimonthly VAT reporting periods; it does not compute a filing deadline (term 3's summer extension is unverified against a primary source here -- an accountant applies their own deadline from the termin number).

The EHF seam

v1 does not produce EHF (Norway's e-invoice format, mandatory for B2B from 2027-01-01). toEhfInvoiceData is a pure mapping from an issued document to a typed shape an external EHF/UBL generator can build against; no XML, no PEPPOL access-point call.

import { toEhfInvoiceData } from '@wtfalch/invoicing';
import type { EhfEmitter } from '@wtfalch/invoicing';

const data = toEhfInvoiceData(invoice);
// an EhfEmitter (not shipped here) turns `data` into EHF XML / a PEPPOL submission

Tests

pnpm test                                  # PGlite, in memory
TEST_DATABASE_URL=postgres://... pnpm test # a real Postgres; a scratch schema per run

Gapless numbering under concurrency is only meaningfully exercised against a real Postgres (PGlite is a single connection, so "concurrent" calls in that tier serialize rather than racing) -- see docs/adr/0007-testing.md.