@debito/hippo-lib
v1.13.0
Published
Double-entry accounting library for CouchDB
Maintainers
Readme
Hippo-lib - Universal Double Entry Accounting Library
A comprehensive JavaScript library for double-entry accounting system using CouchDB for data storage. Works in both Node.js and web browsers.
Features
- ✅ Universal compatibility - Works in Node.js and browsers (with bundlers)
- ✅ Double-entry accounting system with automatic balance validation
- ✅ Chart of Accounts (COA) management with hierarchy validation and templates
- ✅ Journal entries with posting system, force deletion, and reversal capabilities
- ✅ Trial balance reporting with formatted output
- ✅ Account templates for dynamic vendor, customer, and employee account creation
- ✅ Journal templates for standardized transaction patterns
- ✅ CouchDB integration with universal HTTP client
- ✅ Comprehensive ledger management with centralized balance updates and audit trail
- ✅ Account filtering by tags for categorization and search
- ✅ Date filtering for ledger entries with CouchDB query optimization
- ✅ Template key tagging for audit trails and business intelligence
- ✅ Date-ordered ledger listing with CouchDB-level sorting
Installation
npm install @debito/hippo-libQuick Start
Node.js Usage
const hippoLib = require('@debito/hippo-lib');
// Initialize with database credentials
await hippoLib.init('username', 'password', 'database-name');
// Load Chart of Accounts template
await hippoLib.COA.loadTemplate();
// Create accounts and journal entries
const cash = await hippoLib.Account.findByKey('cash');
await cash.updateBalance(1000);Browser Usage
1. Install and bundle with webpack/rollup/vite:
npm install @debito/hippo-lib2. Use in your JavaScript:
// Import with ES modules (bundler required)
import hippoLib from '@debito/hippo-lib';
// Initialize with database credentials
await hippoLib.init('username', 'password', 'database-name');
// All functionality works exactly the same as Node.js
await hippoLib.COA.loadTemplate();
const cash = await hippoLib.Account.findByKey('cash');
await cash.updateBalance(1000);Configuration
Simple Initialization
No configuration files needed! Just call the init method:
// Node.js with environment variables
require('dotenv').config();
const hippoLib = require('@debito/hippo-lib');
await hippoLib.init(
process.env.COUCHDB_USERNAME,
process.env.COUCHDB_PASSWORD,
process.env.HIPPO_DATABASE_NAME
);Optional: .env file for Node.js
Create a .env file in your project root:
COUCHDB_USERNAME=admin
COUCHDB_PASSWORD=password
HIPPO_DATABASE_NAME=accounting-dbComplete API Reference
Core Classes
hippoLib (Main Entry Point)
const hippoLib = require('@debito/hippo-lib');
// Initialize the library
await hippoLib.init(username, password, database);
// Get database information
const dbInfo = await hippoLib.getDatabaseInfo();Available exports:
Account- Account management classLedgerEntry- Individual ledger entry classJournalEntry- Journal entry classCOA- Chart of Accounts utilitiesTrialBalance- Trial balance reportingLedgerTemplate- Account template utilitiesJournalTemplate- Journal entry template utilities
Account Class
Manages individual accounts with balance tracking and validation.
Properties:
key- Unique account identifier (string)label- Display name (string)balance- Current balance (number, positive for debits in debit accounts)accountType- Account type ('asset', 'liability', 'equity', 'revenue', 'expense')hierarchy- Hierarchical path (dot-separated string)tags- Array of categorization tags
Static Methods:
// Create new account
const account = await Account.new(key, label, accountType, balance, hierarchy, tags);
// Get account by internal ID
const account = await Account.get(accountId);
// Find account by key (preferred method)
const account = await Account.findByKey(key);
// List all accounts
const accounts = await Account.list();
// List accounts by tags
const vendorAccounts = await Account.listByTag('vendor');
const taggedAccounts = await Account.listByTag(['cash', 'petty']);Instance Methods:
// Update balance directly (use sparingly - prefer journal entries)
await account.updateBalance(newBalance);
// Update balance from ledger activity (used internally)
await account.updateBalance('debit', 500, 'add');
await account.updateBalance('credit', 300, 'reverse');
// Save changes to database
await account.save();
// Remove account (balance must be zero)
await account.remove();
// Check for unsaved changes
const hasChanges = account.isDirty();
// Get list of changed fields
const changes = account.getChanges();
// Revert unsaved changes
account.rollback();
// Check if account increases with debits
const isDebitAccount = account.isDebitAccount();
// Add ledger entry (used internally)
const ledgerEntry = await account.addLedgerEntry(amount, type, description, journalEntryId);Example Usage:
// Create a new cash account
const cash = await Account.new(
'petty-cash', // key
'Petty Cash Fund', // label
'asset', // accountType
500, // initial balance
'assets.cash-bank.petty-cash', // hierarchy
['cash', 'petty'] // tags
);
// Update the balance
await cash.updateBalance(750);
// Find an existing account
const existingAccount = await Account.findByKey('petty-cash');JournalEntry Class
Manages double-entry journal entries with automatic validation.
Properties:
id- Journal entry ID (string)lines- Array of journal linesdescription- Entry description (string)date- Entry date (ISO string)status- Entry status ('posted', 'reversed', 'deleted')tags- Array of categorization tags
Static Methods:
// Create new journal entry
const journal = await JournalEntry.new(description, tags);
// Get journal entry by ID
const journal = await JournalEntry.get(journalId);
// List all journal entries (sorted by date, latest first)
const journals = await JournalEntry.list();
// List with custom sorting
const oldestFirst = await JournalEntry.list({ descending: false });Instance Methods:
// Add line to journal entry
journal.addLine(accountKey, type, amount, description);
// Validate entry balances (debits = credits)
const isValid = journal.validate();
// Get total debit amount
const debitTotal = journal.getDebitTotal();
// Get total credit amount
const creditTotal = journal.getCreditTotal();
// Get balance (should be 0 for valid entries)
const balance = journal.getBalance();
// Post entry (creates ledger entries, updates account balances)
await journal.post();
// Save metadata changes (description, tags) without affecting ledger
await journal.save();
// Force delete entry and reverse all balances (ADMIN FUNCTION)
await journal.forceDelete(true);
// Reverse entry with offsetting journal entry
const reversingEntry = await journal.reverse('Correction needed');Example Usage:
// Create a sales transaction
const entry = await JournalEntry.new('Daily cash sales');
// Add debit line (cash received)
entry.addLine('cash', 'debit', 500, 'Cash received from sales');
// Add credit line (revenue recognized)
entry.addLine('sales-revenue', 'credit', 500, 'Product sales revenue');
// Validate and post
if (entry.validate()) {
await entry.post();
}LedgerEntry Class
Individual ledger entry records for audit trail.
Properties:
accountId- Associated account ID (string)amount- Entry amount (number, always positive)type- 'debit' or 'credit'description- Entry description (string)journalEntryId- Associated journal entry ID (string)date- Entry date (ISO string)
Static Methods:
// Create new ledger entry (usually done automatically)
const ledger = await LedgerEntry.new(accountId, amount, type, description, journalEntryId);
// Get ledger entry by ID
const ledger = await LedgerEntry.get(entryId);
// List entries by account
const entries = await LedgerEntry.listByAccount(accountId);
// List entries by journal entry
const entries = await LedgerEntry.listByJournalEntry(journalEntryId);
// List all ledger entries
const entries = await LedgerEntry.list();
// List with date filtering options
const recentEntries = await LedgerEntry.list({
startDate: '2025-07-01T00:00:00.000Z', // From July 1st
endDate: '2025-07-31T23:59:59.999Z', // To July 31st
descending: false // Oldest first
});
// Combine account and date filtering
const accountEntries = await LedgerEntry.listByAccount(accountId, {
startDate: new Date('2025-08-01').toISOString(),
descending: true // Latest first (default)
});Instance Methods:
// Save changes
await ledger.save();
// Check for changes
const hasChanges = ledger.isDirty();
// Get changes
const changes = ledger.getChanges();
// Revert changes
ledger.rollback();Chart of Accounts (COA) System
COA Class
Manages hierarchical chart of accounts with templates.
Static Methods:
// Load COA template and optionally create accounts
const coa = await COA.loadTemplate(templateName = 'default', createAccounts = true);
// Create accounts from template definition
const results = await COA.createAccountsFromTemplate(templateAccounts);
// Store COA template in database
const doc = await COA.storeCOATemplate(template, templateName);
// Load COA from database
const coa = await COA.loadCOAFromDB();
// Validate hierarchy path exists
const node = await COA.validateHierarchyPath(hierarchyPath);
// Check if path represents a group (non-posting) account
const isGroup = await COA.isGroupAccount(hierarchyPath);
// Get hierarchy paths at specific level
const paths = await COA.getHierarchyPaths(parentPath, level);
// Account management in COA
await COA.addAccountToCOA(accountData);
await COA.removeAccountFromCOA(accountKey);
await COA.updateAccountInCOA(accountKey, updates);Template Structure:
The COA template includes:
- Hierarchy: Nested tree structure with codes and labels
- Accounts: Predefined accounts with types and hierarchies
- Ledger Templates: Account creation patterns
- Journal Templates: Transaction patterns
Example Usage:
// Load default COA template
await COA.loadTemplate();
// Validate a hierarchy path
const node = await COA.validateHierarchyPath('assets.cash-bank.cash');
console.log(node.label); // "Cash"
// Check if it's a group account
const isGroup = await COA.isGroupAccount('assets.cash-bank');
console.log(isGroup); // true (group accounts can't have transactions)Template System
LedgerTemplate Class
Creates standardized accounts for vendors, customers, employees, etc.
Static Methods:
// Create account from template
const account = await LedgerTemplate.createAccountFromTemplate(templateKey, name, overrides);
// Get template definition
const template = await LedgerTemplate.getTemplate(templateKey);
// List all available templates
const templates = await LedgerTemplate.listTemplates();
// Add custom template
const coa = await LedgerTemplate.addTemplate(templateData);
// Remove template
const coa = await LedgerTemplate.removeTemplate(templateKey);
// Generate key from name
const key = LedgerTemplate.generateKey(name, pattern);
// Generate label from name
const label = LedgerTemplate.generateLabel(name, pattern);
// Utility methods
await LedgerTemplate.printTemplates();
await LedgerTemplate.demonstrateTemplate(templateKey, exampleName);Built-in Templates:
supplier- Supplier/vendor accounts (liability type)customer- Customer receivable accounts (asset type)employee- Employee payroll accounts (liability type)fixed-asset- Fixed asset accounts (asset type)
Example Usage:
// Create a new supplier account
const supplier = await LedgerTemplate.createAccountFromTemplate(
'supplier',
'ABC Manufacturing Corp'
);
// Create with custom options
const customer = await LedgerTemplate.createAccountFromTemplate(
'customer',
'XYZ Retail Store',
{
balance: 500,
tags: ['premium', 'corporate']
}
);
// List available templates
const templates = await LedgerTemplate.listTemplates();
console.log(templates.map(t => `${t.key}: ${t.label}`));JournalTemplate Class
Creates standardized journal entries from predefined transaction patterns.
Static Methods:
// Create journal entry from template
const journal = await JournalTemplate.createJournalEntryFromTemplate(
templateKey, // Template identifier
amount, // Transaction amount
options, // Variable substitutions (e.g., {vendor: 'abc-corp'})
description, // Entry description
date // Optional: ISO date string for historical entries
);Built-in Journal Templates:
purchase-stock-cash- Cash purchase from vendorpurchase-stock-bank- Bank payment purchase from vendorpurchase-stock-credit- Credit purchase (accounts payable)vendor-payment-cash- Cash payment to vendorvendor-payment-bank- Bank payment to vendorpurchase-general-cash- Generic cash purchase without vendor
Example Usage:
// Create a cash purchase entry
const purchase = await JournalTemplate.createJournalEntryFromTemplate(
'purchase-stock-cash',
1000,
{ vendor: 'abc-manufacturing' },
'Inventory purchase from ABC Manufacturing'
);
// Create a vendor payment
const payment = await JournalTemplate.createJournalEntryFromTemplate(
'vendor-payment-bank',
500,
{ vendor: 'abc-manufacturing' },
'Payment to ABC Manufacturing'
);Reporting
TrialBalance Class
Generates trial balance reports with formatted output.
Static Methods:
// Generate trial balance as of specific date (default: current date)
const trialBalance = await TrialBalance.asOfDate(date);
// Print formatted trial balance to console
TrialBalance.printToConsole(trialBalance, options);
// Get summary statistics
const summary = TrialBalance.getSummary(trialBalance);Trial Balance Data Structure:
{
metadata: {
asOfDate: "2024-01-01T00:00:00.000Z",
generatedAt: "2024-01-01T12:00:00.000Z",
totalAccounts: 25,
isBalanced: true
},
accounts: {
asset: [
{key: 'cash', label: 'Cash', balance: 1000, debit: 1000, credit: 0}
],
liability: [...],
equity: [...],
revenue: [...],
expense: [...]
},
totals: {
byGroup: {
asset: {debit: 5000, credit: 0, count: 10},
// ... other groups
},
overall: {debit: 15000, credit: 15000, difference: 0}
}
}Example Usage:
// Generate current trial balance
const tb = await TrialBalance.asOfDate();
// Print formatted report
TrialBalance.printToConsole(tb);
// Get summary data
const summary = TrialBalance.getSummary(tb);
console.log(`Total accounts: ${summary.totalAccounts}`);
console.log(`Is balanced: ${summary.isBalanced}`);
console.log(`Total debits: $${summary.totalDebits}`);
console.log(`Total credits: $${summary.totalCredits}`);
// Show accounts with zero balances
TrialBalance.printToConsole(tb, { showZeroBalances: true });Constants and Types
Account Types
const { ACCOUNT_TYPES } = require('@debito/hippo-lib/src/constants');
ACCOUNT_TYPES.ASSET // 'asset'
ACCOUNT_TYPES.LIABILITY // 'liability'
ACCOUNT_TYPES.EQUITY // 'equity'
ACCOUNT_TYPES.REVENUE // 'revenue'
ACCOUNT_TYPES.EXPENSE // 'expense'Transaction Types
const { DEBIT, CREDIT } = require('@debito/hippo-lib/src/constants');
DEBIT // 'debit'
CREDIT // 'credit'Journal Status
const { JOURNAL_STATUS } = require('@debito/hippo-lib/src/constants');
JOURNAL_STATUS.POSTED // 'posted'
JOURNAL_STATUS.REVERSED // 'reversed'
JOURNAL_STATUS.DELETED // 'deleted'Database Integration
Document ID Patterns
- Accounts:
account-{key}(e.g.,account-cash) - Journal Entries:
jentry_{timestamp}_{random}(e.g.,jentry_1640995200000_abc123) - Ledger Entries:
lentry_{timestamp}_{random}(e.g.,lentry_1640995200000_def456) - COA Settings:
settings-coa
Automatic Indexes
The library creates these CouchDB indexes automatically:
ledger-by-date- Ledger entries sorted by dateledger-by-account- Ledger entries by account IDledger-by-journal- Ledger entries by journal entry IDaccount-by-name- Accounts by name
Journal Entry Lifecycle Management
Overview
The journal entry system provides comprehensive lifecycle management including posting, reversal, and force deletion capabilities while maintaining proper accounting principles and audit trails.
Journal Entry Status Flow
NEW → POSTED → REVERSED (via reversal entry)
↘ DELETED (via force delete - admin only)Key Lifecycle Operations
1. Creating and Posting Entries
// Create new entry (automatically in POSTED status)
const entry = await JournalEntry.new('Sales transaction', ['sales']);
entry.addLine('cash', 'debit', 1000, 'Cash received');
entry.addLine('sales', 'credit', 1000, 'Sales revenue');
// Post the entry (creates ledger entries and updates balances)
await entry.post();2. Reversing Entries (Accounting Best Practice)
// Create offsetting reversal entry
const reversalEntry = await entry.reverse('Customer returned merchandise');
// Original entry status becomes 'reversed'
// New reversal entry is created with opposite amounts
// Both entries preserved for audit trail3. Force Deletion (Admin Function)
// CAUTION: Breaks audit trail - use only for corrections/cleanup
await entry.forceDelete(true); // Explicit confirmation required
// Removes all ledger entries
// Reverses all account balance changes
// Marks journal entry as 'deleted' (preserves document for audit)4. Centralized Balance Management
// All balance updates go through Account.updateBalance()
const account = await Account.findByKey('cash');
// Add activity (normal posting)
await account.updateBalance('debit', 500, 'add');
// Reverse activity (for deletions/corrections)
await account.updateBalance('debit', 500, 'reverse');Audit Trail and Data Integrity
- Immutable Principle: Posted entries cannot be modified, only reversed
- Complete Audit Trail: All transactions preserved with status tracking
- Balance Consistency: Centralized balance management prevents inconsistencies
- Force Delete Safety: Requires explicit confirmation and preserves audit trail
Testing the Lifecycle
# Run comprehensive lifecycle test
node test/test-journal-entry-lifecycle.jsThis test validates:
- ✅ Journal entry creation and posting
- ✅ Ledger entry creation and account balance updates
- ✅ Force deletion with balance reversal
- ✅ Complete cleanup and audit trail preservation
Date Filtering System
Overview
The date filtering system provides powerful query capabilities for ledger entries with CouchDB-level optimization for performance.
Key Features
- CouchDB Query Integration: Date filters applied at database level using
$gteand$lteoperators - Backward Compatibility: All existing code continues to work without changes
- Flexible Options: Combine date filtering with other query options
- Universal Support: Works with all LedgerEntry list methods
Date Filtering Options
Available Parameters:
startDate- ISO date string (inclusive) - filters entries on or after this dateendDate- ISO date string (inclusive) - filters entries on or before this datedescending- Boolean (default: true) - sort order for results
Supported Methods:
LedgerEntry.list(options)LedgerEntry.listByAccount(accountId, options)LedgerEntry.listByJournalEntry(journalId, options)
Date Filtering Examples
// Filter entries from last month
const lastMonth = new Date();
lastMonth.setMonth(lastMonth.getMonth() - 1);
const monthlyEntries = await LedgerEntry.list({
startDate: lastMonth.toISOString()
});
// Filter entries for specific date range
const julyEntries = await LedgerEntry.list({
startDate: '2025-07-01T00:00:00.000Z',
endDate: '2025-07-31T23:59:59.999Z'
});
// Get account-specific entries for date range
const cashAccount = await Account.findByKey('cash');
const cashJulyEntries = await LedgerEntry.listByAccount(cashAccount._doc._id, {
startDate: '2025-07-01T00:00:00.000Z',
endDate: '2025-07-31T23:59:59.999Z',
descending: false // Oldest first
});
// Filter entries older than specific date
const oldEntries = await LedgerEntry.list({
endDate: '2025-06-30T23:59:59.999Z'
});Date Inheritance System
Ledger entries automatically inherit dates from journal entries:
// Manual journal entry with custom date
const entry = await JournalEntry.new('Historical entry', ['historical']);
entry._doc.date = '2025-01-15T10:00:00.000Z'; // Set custom date
entry.addLine('cash', 'debit', 1000, 'Historical cash entry');
entry.addLine('sales', 'credit', 1000, 'Historical sales');
await entry.save(); // Ledger entries inherit this date
// Template entry with custom date
const templateEntry = await JournalTemplate.createJournalEntryFromTemplate(
'purchase-stock-cash',
500,
{ vendor: 'vendor-key' },
'Historical purchase',
'2025-01-20T00:00:00.000Z' // Custom date parameter
);Account Tag Filtering
Filter accounts by tags for better organization:
// Find all vendor accounts
const vendorAccounts = await Account.listByTag('vendor');
// Find accounts with multiple possible tags
const cashAccounts = await Account.listByTag(['cash', 'petty', 'bank']);
// Case-insensitive partial matching
const supplierAccounts = await Account.listByTag('supplier');Complete Usage Examples
Basic Accounting Workflow
const hippoLib = require('@debito/hippo-lib');
// 1. Initialize
await hippoLib.init('admin', 'password', 'accounting-db');
// 2. Load Chart of Accounts
await hippoLib.COA.loadTemplate();
// 3. Create vendor account from template
const vendor = await hippoLib.LedgerTemplate.createAccountFromTemplate(
'supplier',
'Office Supply Corp'
);
// 4. Create purchase using journal template
const purchase = await hippoLib.JournalTemplate.createJournalEntryFromTemplate(
'purchase-stock-credit',
250,
{ vendor: vendor.key },
'Office supplies purchase'
);
// 5. Generate trial balance
const trialBalance = await hippoLib.TrialBalance.asOfDate();
hippoLib.TrialBalance.printToConsole(trialBalance);Manual Journal Entry Creation
// Create a complex journal entry manually
const journal = await hippoLib.JournalEntry.new('Monthly payroll');
// Add multiple lines
journal.addLine('wages-expense', 'debit', 5000, 'Gross wages');
journal.addLine('payroll-taxes-expense', 'debit', 500, 'Employer payroll taxes');
journal.addLine('employee-taxes-payable', 'credit', 1000, 'Employee tax withholdings');
journal.addLine('wages-payable', 'credit', 4500, 'Net wages payable');
// Validate and save
if (journal.validate()) {
await journal.save();
console.log('Payroll journal entry posted successfully');
} else {
console.log('Entry does not balance!');
}Account Management
// Create accounts with hierarchy and tags
const equipment = await hippoLib.Account.new(
'pos-system',
'Point of Sale System',
'asset',
2500,
'assets.fixed-assets.equipment.pos-system',
['equipment', 'technology', 'depreciable']
);
// Update account properties
equipment.tags.push('critical');
await equipment.save();
// Search and filter accounts
const allAccounts = await hippoLib.Account.list();
const assetAccounts = allAccounts.filter(acc => acc.accountType === 'asset');
const cashAccounts = allAccounts.filter(acc => acc.tags.includes('cash'));Browser Compatibility
Supported Bundlers
- ✅ Webpack (all versions)
- ✅ Rollup
- ✅ Parcel
- ✅ Browserify
- ✅ esbuild
- ✅ Vite
Browser Requirements
- Modern browsers with ES6+ support
- Fetch API support (or axios polyfill)
- Module bundler for CommonJS imports
Example React Integration
import React, { useEffect, useState } from 'react';
import hippoLib from '@debito/hippo-lib';
function AccountingDashboard() {
const [accounts, setAccounts] = useState([]);
const [trialBalance, setTrialBalance] = useState(null);
const [initialized, setInitialized] = useState(false);
useEffect(() => {
async function initializeAccounting() {
try {
// Initialize hippo-lib
await hippoLib.init(
process.env.REACT_APP_COUCH_USER,
process.env.REACT_APP_COUCH_PASS,
process.env.REACT_APP_COUCH_DB
);
// Load COA template
await hippoLib.COA.loadTemplate();
// Load accounts and trial balance
const accountList = await hippoLib.Account.list();
const tb = await hippoLib.TrialBalance.asOfDate();
setAccounts(accountList);
setTrialBalance(tb);
setInitialized(true);
} catch (error) {
console.error('Failed to initialize accounting:', error);
}
}
initializeAccounting();
}, []);
if (!initialized) return <div>Loading accounting system...</div>;
return (
<div>
<h1>Accounting Dashboard</h1>
<section>
<h2>Accounts ({accounts.length})</h2>
{accounts.map(account => (
<div key={account.key}>
<strong>{account.label}</strong>: ${account.balance}
<span> ({account.accountType})</span>
</div>
))}
</section>
<section>
<h2>Trial Balance Summary</h2>
{trialBalance && (
<div>
<p>Total Debits: ${trialBalance.totals.overall.debit}</p>
<p>Total Credits: ${trialBalance.totals.overall.credit}</p>
<p>Balanced: {trialBalance.metadata.isBalanced ? '✅' : '❌'}</p>
</div>
)}
</section>
</div>
);
}
export default AccountingDashboard;Data Storage
This library uses CouchDB for data persistence. Make sure you have a CouchDB instance running and accessible.
CouchDB Setup:
- Install CouchDB locally or use a cloud service
- Create a database for your application
- Configure authentication credentials
- Set CORS settings for browser access (if needed)
Testing
Run the test suite:
# Run comprehensive test suite
npm test
# Individual tests
node test/test-clean.js # General accounting operations
node test/test-trial-balance.js # Trial balance functionality
node test/test-coa-load.js # COA template loading
node test/test-accounts-by-tag.js # Account tag filtering
node test/test-template-tagging.js # Template key tagging system
node test/test-ledger-date-ordering.js # Date-ordered ledger listing
node test/test-date-filtering-simple.js # Date filtering functionality
node test/test-journal-entry-lifecycle.js # Journal entry posting, deletion, and reversal
node test/test-journal-template.js # Journal templates
node test/test-ledger-template.js # Account templates
node test/test-couchdb-driver.js # CouchDB driverComplete Browser Guide
For detailed browser usage examples including React, Vue.js, and vanilla JavaScript setups, see WEB-USAGE.md.
Contributing
Issues and pull requests are welcome on the project repository.
License
ISC
@debito/hippo-lib v1.10.0 - Universal double-entry accounting for Node.js and browsers 🎉
