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

@companyio/accounting

v0.1.11

Published

Reusable accounting domain package for multi-tenant business applications

Readme

@companyio/accounting

Installable double-entry accounting for multi-tenant SaaS apps.

Use it from this repo or any other app. After you publish the package (npm, GitHub Packages, a git URL, or a private registry), install it in the other application and run accounting init there. Accounting tables live in that app’s existing PostgreSQL database. This package does not create a second database or its own Prisma Client.

What the host must provide

| Host owns | Package owns | | --------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | PostgreSQL + DATABASE_URL | Domain logic (posting, ledgers, reports, periods) | | Prisma schema and migrations | Schema source (prisma/accounting.prisma) and accounting init | | Authentication / sessions | Fastify routes via attachAccounting | | Mapping session → { main_business_id, branch_id } (host switcher is the branch) | One platform General Ledger for all products |

Requirements: Node.js, PostgreSQL, Prisma on the host, zod (installed with this package). Fastify ≥ 4 is required only if you call attachAccounting / registerAccountingRoutes.

Package boundaries

| Owns | Does not own | | ------------------------------------------------ | ------------------------------------------------------------- | | Chart of accounts, journals, one GL, periods | Invoices, POS tickets, fuel sales, school fees | | AccountingParty + external refs | Contacts identity (link via externalType / externalRefId) | | Reversals, idempotency, trial balance / P&L / BS | HR obligations, advances, loans as domain tables | | Generic sourceModule + referenceId | Knowledge of what those source ids mean | | | Product-specific ledgers (FuelLedger, school books, …) |

Domain packages own the business event. Accounting owns the single financial General Ledger for Fuel, POS, School, Sales, Purchasing, Expenses, HR, Payments, and future products. Fuel tank/meter/stock history stays in @companyio/fuel and is not part of this ledger.

Source references

Every journal carries:

  • sourceModule — opaque domain name (hr, fuel, pos, school, …)
  • referenceId — opaque domain document id
  • Optional idempotencyKey — retry-safe unique key within the branch (e.g. hr:salary_payment:pay_123)

Accounting does not interpret those values.

Money

Amounts are decimal major units with 2 dp, rounded through integer cents. Prisma stores Decimal(14,2). Use toCents / fromCents / roundMoney / addMoney from this package.

Tenant context

type AccountingContext = {
  main_business_id: string;
  branch_id: string;
  user_id?: string; // used as createdBy/postedBy when posting journals
};

Domain adapters

Product posting helpers (postFuelJournal, postSchoolJournal, /integrations/fuel, /integrations/school) were removed. They posted into the same GL but used product-prefixed entry types and account mapping fields.

Post from every product the same way:

await accounting.operations.post(context, userId, {
  sourceModule: 'fuel', // or pos | school | sales | …
  kind: 'credit_sale',
  amount: 8000,
  transactionDate: '2026-09-21',
  referenceId: 'sale-001',
  party: { type: 'CUSTOMER', name: 'Fleet', externalType: 'contacts_party', externalRefId: '…' },
});

// or explicit lines:
await accounting.journals.post(context, {
  entryType: 'CREDIT_SALE',
  sourceModule: 'sales',
  referenceId: invoiceId,
  transactionDate: '2026-09-21',
  description: invoiceNumber,
  lines: [/* balanced AR + revenue */],
  idempotencyKey: `sales:invoice:${invoiceId}`,
});

Install in another app

pnpm add @companyio/accounting
# or: npm install @companyio/accounting

Until it is on a registry, install from git or a packed tarball:

pnpm add github:<org>/<repo>#<tag>
# or
pnpm pack   # in this package, then pnpm add /path/to/companyio-accounting-0.1.0.tgz

Then, from the host app directory that contains Prisma:

pnpm exec accounting init
# optional: pnpm exec accounting init --schema path/to/schema.prisma
pnpm --filter @companyio/api exec prisma migrate deploy --config prisma7.config.ts
pnpm prisma generate

accounting init:

  • Finds prisma/schema.prisma (or apps/api/prisma/schema.prisma, or --schema)
  • Requires a PostgreSQL datasource
  • Appends accounting models between // @companyio/accounting:begin and // @companyio/accounting:end
  • Copies a host-owned migration into the host prisma/migrations folder

Then run the host’s usual migrate + generate against the same DATABASE_URL. If models are already present, the CLI prints Accounting already initialized. and does not duplicate them.

pnpm exec accounting status

Wire Fastify (typical)

Map main_business_id and branch_id from the authenticated session, never from the client body.

import Fastify from 'fastify';
import { attachAccounting } from '@companyio/accounting';

const app = Fastify();

const accounting = attachAccounting(app, {
  prisma, // host PrismaClient after generate (must include Accounting* models)
  prefix: '/api/v1/accounting', // optional; this is the default
  authenticate: async (request) => getUser(request.headers.authorization),
  getContext: (_request, user) => ({
    main_business_id: user.main_business_id,
    branch_id: user.branch_id,
  }),
});

await app.listen({ port: 3000 });

attachAccounting returns the same services object you can call from your own routes (accounting.journals.post, and so on).

Without Fastify, use services only:

import { createAccounting } from '@companyio/accounting';

const accounting = createAccounting({
  prisma,
  getContext: () => ({ main_business_id: '...', branch_id: '...' }),
});

Tests can pass store: createMemoryAccountingStore() instead of Prisma.

Tenancy

Every row is scoped by both:

  • main_business_id — SaaS tenant
  • branch_id — organization unit inside that tenant. In Fuel this is the branch you switch to; books are not shared across branches of the same business.

Books are not shared across branch_id values under the same business. Idempotency is unique on main_business_id + branch_id + idempotencyKey.

Money and journals

Amounts are major currency units with 2 decimal places (for example 500.5 → 500.50). Journals must balance (Σ debit = Σ credit). Posted entries are immutable; corrections use reversals in the same organization.

const cash = await accounting.accounts.create(context, {
  code: '1100',
  name: 'Cash',
  type: 'ASSET',
});

const entry = await accounting.journals.post(context, {
  entryType: 'SALE',
  sourceModule: 'pos',
  referenceId: 'sale-001',
  transactionDate: '2026-09-21',
  description: 'Invoice posted',
  lines: [
    { accountId: cash.id, debit: 500, credit: 0 },
    { accountId: revenue.id, debit: 0, credit: 500 },
  ],
  idempotencyKey: 'pos:sale:sale-001',
  userId: user.id,
});

await accounting.journals.reverse(context, entry.id, user.id);
await accounting.accounts.seedDefaults(context);
await accounting.ledger.getAccountLedger(context, { accountId: cash.id, from, to });
await accounting.parties.getStatement(context, { partyId });
await accounting.reports.trialBalance(context, { from, to });
await accounting.reports.profitAndLoss(context, { from, to });
await accounting.reports.balanceSheet(context, { asOf });

HTTP routes

Default prefix: /api/v1/accounting. All routes except GET .../health require authenticate to succeed.

Other apps (Fuel, school, POS) do not implement posting rules. They either:

  1. Call POST /api/v1/accounting/post after a business document is saved, or
  2. If they host this package (like apps/api), call accounting.operations.post in-process.

POST /post seeds the default chart if needed, upserts parties by externalRefId, and posts a balanced journal. Account ids are optional — the default codes in STANDARD_CHART_CODES are used (1110 cash, 1200 AR, 2100 AP, 4100 sales, 6400 expenses, 1500 assets, …).

{
  "sourceModule": "fuel",
  "kind": "cash_sale",
  "amount": 15000,
  "transactionDate": "2026-09-21",
  "referenceId": "sale-001",
  "instrument": "CASH"
}

Kinds: cash_sale, credit_sale, payment, expense, other_income, asset_purchase, purchase, supplier_payment, cogs, salary.

Credit/receivable kinds need party or partyId. Example credit sale:

{
  "kind": "credit_sale",
  "amount": 8000,
  "transactionDate": "2026-09-21",
  "referenceId": "inv-88",
  "party": {
    "type": "CUSTOMER",
    "name": "City Logistics",
    "externalType": "organization",
    "externalRefId": "org-88"
  }
}

Fuel Management’s UI keeps calling /fuel/* for operations. The Fuel API posts the matching accounting event so the chart, journals, P&L, and receivables stay in this package.

| Method | Path | Purpose | | --------- | ---------------------------- | ------------------------------------------------------------------------------------------ | | GET | /health | Package health | | GET/POST | /accounts | List / create | | GET/PATCH | /accounts/:id | Read / update | | GET | /ledger/:accountId | Account ledger | | GET/POST | /journal | List / post | | GET | /journal/:id | Read entry | | POST | /journal/:id/reverse | Reverse | | GET/POST | /parties | List / create | | GET | /parties/:id/statement | Party statement | | GET | /reports/trial-balance | Trial balance | | GET | /reports/pnl | Profit and loss | | GET | /reports/balance-sheet | Balance sheet | | GET | /reports/receivables | AR by party | | GET | /reports/payables | AP by party | | GET | /reports/reconciliation | Control-account check | | GET/POST | /periods | List / create | | POST | /periods/:id/close /open | Close or reopen | | POST | /post | Operational journals (sale, expense, AR, AP, asset, income) — use this from other apps | | POST | /seed-defaults | Default chart of accounts | | POST | /opening-balance | Opening journal |

School and fuel

Do not post through product-specific Accounting helpers. Fuel keeps pumps/meters/stock in @companyio/fuel; financial recognition goes through Sales / Payments / Purchasing / Expenses adapters into this GL (sourceModule + referenceId). School fees and POS tickets use the same journals.post / operations.post contract with their own sourceModule.

Fuel kind values on the operational API: cash_sale, credit_sale, payment, expense, purchase, supplier_payment, salary, plus cogs / asset_purchase / other_income.

Publish checklist

This package ships dist/, prisma/, and the CLI bin accounting. Before publish, build:

pnpm --filter @companyio/accounting build
# or from packages/accounting: pnpm build

prepublishOnly runs tsc. Consumers need the built dist and the prisma/ folder (CLI reads prisma/accounting.prisma and prisma/migrations/0001_accounting_baseline).

pnpm add @companyio/accounting
pnpm exec accounting init --schema path/to/schema.prisma
pnpm --filter @companyio/api exec prisma migrate deploy --config prisma7.config.ts
pnpm prisma generate