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

ohadakit

v2.0.0

Published

TypeScript SDK for the OHADA/SYSCOHADA chart of accounts — O(1) lookups, querying, custom accounts, i18n, and export

Readme

npm version npm downloads License: Apache-2.0 TypeScript Node.js


OhadaKit is a production-ready SDK for working with the OHADA/SYSCOHADA accounting plan — the standardised chart of accounts used across 17 West and Central African states. It ships the full 1 000+ account tree out of the box and exposes a composable, type-safe API for lookups, querying, custom extensions, multilingual names, and data export.

Live demo: https://ohadakit.claviscore.com

It manages the chart of accounts. Journal entries, balances, and financial statements belong in your application.


Table of Contents


Features

| | | |---|---| | Complete chart | All 1 000+ official OHADA accounts across 9 classes | | O(1) lookups | Map-based indexes; Trie for fast prefix search | | Fluent query builder | Filter by class, level, parent, name, regex, or custom predicate | | Fuzzy search | Typo-tolerant name search with configurable threshold | | Account tree | Parent, children, ancestors, siblings, path — lazily computed | | i18n | French, English, Portuguese, Spanish with graceful fallback | | Custom accounts | Non-destructive overlay for sub-accounts and label overrides | | Pluggable storage | Notes on any account; memory and localStorage adapters included | | Snapshot / restore | Serialisable chart state for database persistence | | Export | JSON (flat or hierarchical) and CSV | | Type-safe | Branded types, Result<T, E> error handling, full generics | | Zero dependencies | No runtime dependencies |


Installation

# npm
npm install ohadakit

# yarn
yarn add ohadakit

# pnpm
pnpm add ohadakit

Quick Start

import { LedgerEngine } from 'ohadakit';

const ledger = new LedgerEngine();

// Safe lookup — returns a Result<Account>
const result = ledger.get('4111');
if (result.ok) {
  console.log(result.data.name);        // "Clients"
  console.log(result.data.pathString);  // "41 > 411 > 4111"
}

// Convenience accessors
const account  = ledger.getOrNull('4111');   // Account | null
const account2 = ledger.getOrThrow('4111');  // Account  (throws on miss)

// Fluent query builder
const staffExpenses = ledger
  .query()
  .inClass('6')
  .atLevel(3)
  .nameContains('personnel')
  .sortBy('code', 'asc')
  .execute();  // Account[]

Core API

Account Access

// Safe — never throws
ledger.get('4111')        // Result<Account>
ledger.getOrNull('4111')  // Account | null

// Eager — throws AccountNotFoundError or InvalidAccountCodeFormatError
ledger.getOrThrow('4111') // Account

// Registry helpers
ledger.registry.has('4111')             // boolean
ledger.registry.getByClass('4')         // Account[]
ledger.registry.getByLevel(3)           // Account[]
ledger.registry.searchByPrefix('41')    // Account[]  (Trie-based)

Query Builder

ledger
  .query()
  .inClass(['4', '5'])        // one or more classes
  .atLevel([3, 4])            // one or more levels
  .withParent('41')           // direct children only
  .nameContains('client')     // case-insensitive substring
  .codeMatches(/^6[0-3]/)     // regex on code
  .where(a => a.isLeaf)       // arbitrary predicate
  .sortBy('code', 'asc')      // 'code' | 'name' | 'level'
  .offset(0).limit(25)        // pagination
  .execute();                 // → Account[]

// Aggregation shortcuts
ledger.query().inClass('4').count();   // number
ledger.query().inClass('4').first();   // Account | null
ledger.query().inClass('4').exists();  // boolean

// Fuzzy / typo-tolerant search
ledger
  .query()
  .search('cliens', { fuzzy: true, threshold: 0.6 })
  .execute();

Account Relationships

Relationship properties are lazily computed and cached on first access.

const account = ledger.getOrThrow('4111');

account.parent      // Account | null
account.children    // Account[]
account.ancestors   // Account[]  (nearest-first)
account.siblings    // Account[]
account.path        // Account[]  (root → self)
account.pathString  // "41 > 411 > 4111"
account.isLeaf      // boolean
account.isRoot      // boolean
account.depth       // number (1-based)

account.isDescendantOf('4')      // true
account.isAncestorOf('41111')    // true (if account exists)
account.getDescendants()         // Account[]  (all levels)
account.getDescendantsAtLevel(4) // Account[]  (specific level only)

Internationalization

Supports four OHADA locales. Names fall back to French when a translation is missing.

const ledger = new LedgerEngine({ locale: 'en' });

ledger.setLocale('pt');
ledger.getLocale();             // 'pt'
ledger.getAvailableLocales();   // ['fr', 'en', 'pt', 'es']

// Localised name with automatic fallback
ledger.getLocalizedName('4111');  // English name, or French if not translated

// Low-level TranslationService
ledger.i18n.getAccountName('10', 'Capital');
ledger.i18n.hasTranslation('10');

Custom Accounts

Extend the immutable OHADA tree without modifying it. Custom accounts and label overrides live in an isolated overlay.

Rules

  • 2-character main accounts (10, 11, …) cannot be created or renamed.
  • Custom accounts must be ≥ 3 characters and start with their parent's code.
  • Any 3+-character account (official or custom) can have its label overridden.
import { CustomAccountManager, MemoryStorage } from 'ohadakit';

const manager = new CustomAccountManager({ storage: new MemoryStorage() });
await manager.initialize();

// Create a custom sub-account
await manager.createAccount({
  code: '411-VIP',
  name: 'Clients VIP',
  parentCode: '411',
});

// Override an official account label
await manager.updateLabel('4111', 'Clients — Particuliers');

// Query the merged chart
manager.getByCode('411-VIP');    // Account
manager.getAll();                // official + custom
manager.getCustomAccounts();     // custom only

AccountBook — unified facade

AccountBook combines every OhadaKit feature behind a single async-initialised object. Start here for any real application.

import { AccountBook, MemoryStorage } from 'ohadakit';

const book = new AccountBook({ storage: new MemoryStorage() });
await book.initialize();

// Lookup (official + custom, overrides applied)
const account = book.getAccountOrNull('411');

// Mutate
await book.createAccount({ code: '411-VIP', name: 'Clients VIP', parentCode: '411' });
await book.updateLabel('4111', 'Clients — Particuliers');
await book.setNote('411-VIP', 'High-value clients segment');

// Export the merged chart
const json = book.exportToJSON({ pretty: true });
const csv  = book.exportToCSV();

// i18n
book.setLocale('en');
book.getLocalizedName('10');

// Aggregate stats
const stats = await book.getStats();
// → { total, byClass, byLevel, customAccountCount, labelOverrideCount, noteCount }

Snapshot & restore

Persist the complete chart state — custom accounts, overrides, and notes — as a plain JSON object that can be stored anywhere.

// Capture
const snapshot = await book.snapshot();
// { version, timestamp, locale, customAccounts, labelOverrides, notes }

await db.put('chart-state', JSON.stringify(snapshot));

// Restore into a fresh book
const result = await book.restore(JSON.parse(savedJson));
if (!result.ok) {
  console.error('Restore failed:', result.error.message);
}

AccountBook vs LedgerEngine

| Need | Use | |------|-----| | Quick lookups on the official chart | LedgerEngine | | Custom accounts, label overrides, notes | AccountBook | | Snapshot / restore of chart state | AccountBook | | Minimal setup with no persistence wiring | LedgerEngine |

Notes & Storage

Attach free-text notes to any account code. The storage backend is swappable.

import { LedgerEngine, LocalStorageAdapter } from 'ohadakit';

// Default: in-memory (no persistence)
const ledger = new LedgerEngine();

// Browser: localStorage with a namespaced prefix
const ledger = new LedgerEngine({
  storage: new LocalStorageAdapter('myapp:'),
});

await ledger.setNote('5121', 'Mobile Money Orange');
await ledger.getNote('5121');    // 'Mobile Money Orange'
await ledger.hasNote('5121');    // true
await ledger.deleteNote('5121');
await ledger.getAllNotes();      // Map<string, string>

Custom adapter — implement the StorageAdapter interface to integrate any backend (Redis, IndexedDB, a REST API, etc.).

Export

// JSON
ledger.exportToJSON({ structure: 'flat', pretty: true });
ledger.exportToJSON({ structure: 'hierarchical' });

// CSV
ledger.exportToCSV({ columns: ['code', 'name', 'level'] });
ledger.exportToCSV({ delimiter: ';', includeHeader: true });

// Scoped to a single class
ledger.exportClass('4', 'json', { structure: 'flat' });
ledger.exportClass('4', 'csv',  { columns: ['code', 'name'] });

Validation

import { validateAccountCodeFormat, AccountNotFoundError } from 'ohadakit';

// Format check (no registry lookup required)
const fmt = validateAccountCodeFormat('4111');  // Result<string>

// Typed error handling via Result
const result = ledger.get('9999');
if (!result.ok && result.error instanceof AccountNotFoundError) {
  console.error(result.error.message);
}

// Batch validation
const { valid, invalid } = ledger.validateBatch(['4111', '5121', '9999']);
// valid   → [{ code, account }, …]
// invalid → [{ code, error }, …]

Integrating with an Accounting App

OhadaKit manages the chart of accounts only. Journal entries, balances, and financial statements are your application's responsibility — they link to OhadaKit via account codes used as foreign keys.

┌────────────────────────┐       ┌────────────────────────┐
│      Your App          │       │      OhadaKit          │
│                        │       │                        │
│  Journal entries  ─────┼─ FK ──▶  AccountBook           │
│  Balances         ─────┼─ FK ──▶    ├─ Official chart   │
│  Trial balance    ─────┼─ FK ──▶    ├─ Custom accounts  │
│  Financial stmts       │       │    ├─ Label overrides  │
│                        │       │    ├─ Notes            │
│  Your DB / API         │       │    └─ i18n + Export    │
└────────────────────────┘       └────────────────────────┘
import { AccountBook, MemoryStorage } from 'ohadakit';

const book = new AccountBook({ storage: new MemoryStorage() });
await book.initialize();

// Validate codes before persisting journal entries
function createEntry(debitCode: string, creditCode: string, amount: number) {
  if (!book.has(debitCode))  throw new Error(`Unknown account: ${debitCode}`);
  if (!book.has(creditCode)) throw new Error(`Unknown account: ${creditCode}`);
  return { debitCode, creditCode, amount, date: new Date() };
}

// Resolve display names
function accountLabel(code: string): string {
  return book.getAccountOrNull(code)?.name ?? `Unknown (${code})`;
}

// Persist chart state alongside your application data
async function saveState(db: Database) {
  const snapshot = await book.snapshot();
  await db.put('ohadakit:chart-state', JSON.stringify(snapshot));
}

Responsibility boundary

| Concern | Owner | |---------|-------| | Official chart of accounts (1 000+ entries) | OhadaKit | | Custom sub-accounts & label overrides | OhadaKit — AccountBook | | Account notes | OhadaKit — AccountBook | | Name translations (fr / en / pt / es) | OhadaKit | | Journal entries & postings | Your app | | Account balances & trial balance | Your app | | Financial statements | Your app | | Authentication & authorisation | Your app |


OHADA Chart Structure

SYSCOHADA uses a decimal codification across 9 classes (codes 1–9). Each class digit is the first digit of the account codes within that class — it is a grouping designator, not a postable account code. Accounts proper are identified by codes of 2 to 4 digits, constituting 3 levels:

[Class — grouping designator, not a postable account]
  e.g. 4  →  Tiers

[Account levels — postable codes, 2 to 4 digits]
  41      Main account    (2 digits)   ← first postable level
  411     Sub-account     (3 digits)
  4111    Detail          (4 digits)   ← base maximum per SYSCOHADA

The base codification of SYSCOHADA is limited to a maximum of four digits for divisional accounts (comptes divisionnaires). Enterprises may open further subdivisions beyond four digits to meet operational needs without altering the mandatory normative codes.

| Class | Label | Notes | |-------|-------|-------| | 1 | Ressources durables | Mandatory | | 2 | Actif immobilisé | Mandatory | | 3 | Stocks | Mandatory | | 4 | Tiers | Mandatory | | 5 | Trésorerie | Mandatory | | 6 | Charges des activités ordinaires | Mandatory | | 7 | Produits des activités ordinaires | Mandatory | | 8 | Autres charges et autres produits | Mandatory | | 9 | Engagements hors bilan et comptabilité analytique de gestion (CAGE) | Optional |

Class 9 is split into two sub-sections: accounts 90–91 record off-balance-sheet commitments (engagements hors bilan); accounts 92–99 are reserved for management accounting (comptabilité analytique de gestion, CAGE). Its use is explicitly optional under the SYSCOHADA Uniform Act.


Environment Support

| Environment | Support | |-------------|---------| | Node.js ≥ 16 | ✅ ESM + CJS | | Modern browsers | ✅ UMD + IIFE bundles via ohadakit/browser | | TypeScript | ✅ Full type declarations included | | Bun / Deno | ✅ ESM entry point |


Contributing

Contributions are welcome. Please open an issue first to discuss significant changes.

# Clone and install
git clone https://github.com/Dahkenangnon/ohadakit.git
cd ohadakit
npm install

# Run tests
npm test

# Type-check
npm run type-check

# Build
npm run build

All pull requests must pass npm run typecheck && npm test before review.


License

Apache-2.0 © Justin Dah-kenangnon


GitHub · npm · Issues · [email protected]