@debito/hippo-lib
Version:
Double-entry accounting library for CouchDB
280 lines (246 loc) ⢠10.7 kB
JavaScript
import config from '../config.js';
import Account from '../Account.js';
import COA from '../coa.js';
// Getter to access database instance
function getDb() {
return config.db;
}
/**
* LedgerTemplate helper utility for creating accounts from templates
* Provides standardized account creation patterns for vendors, customers, etc.
*/
class LedgerTemplate {
/**
* Creates an account using a template
* @param {string} templateKey - Key of the template to use
* @param {string} name - Name to use for the account (e.g., "ABC Corporation")
* @param {Object} overrides - Optional field overrides {key, balance, hierarchy, tags, etc.}
* @returns {Promise<Account>} The created account instance
*/
static async createAccountFromTemplate(templateKey, name, overrides = {}) {
try {
// Get the template
const template = await this.getTemplate(templateKey);
if (!template) {
throw new Error(`Template '${templateKey}' not found`);
}
// Use override key if provided, otherwise generate from name and template pattern
const key = overrides.key || this.generateKey(name, template.keyPattern);
// Generate label from name
const label = this.generateLabel(name, template.labelPattern);
// Apply template values with overrides
const accountData = {
key,
label,
accountType: overrides.accountType || template.accountType,
balance: overrides.balance !== undefined ? overrides.balance : template.defaultBalance,
hierarchy: overrides.hierarchy || template.hierarchy,
tags: overrides.tags || [...template.tags]
};
// Validate that we have all required fields
if (!accountData.key || !accountData.label || !accountData.accountType) {
throw new Error('Template must specify key, label, and accountType');
}
// Create the account
const account = await Account.new(
accountData.key,
accountData.label,
accountData.accountType,
accountData.balance,
accountData.hierarchy,
accountData.tags
);
return account;
} catch (error) {
throw new Error(`Failed to create account from template: ${error.message}`);
}
}
/**
* Gets a template by key from the stored templates
* @param {string} templateKey - Key of the template
* @returns {Promise<Object|null>} Template object or null if not found
*/
static async getTemplate(templateKey) {
try {
const templates = await this.listTemplates();
return templates.find(template => template.key === templateKey) || null;
} catch (error) {
throw new Error(`Failed to get template: ${error.message}`);
}
}
/**
* Lists all available templates
* @returns {Promise<Array>} Array of template objects
*/
static async listTemplates() {
try {
// Get templates from COA settings
const coa = await COA.loadCOAFromDB();
return coa.ledgerTemplates || [];
} catch (error) {
throw new Error(`Failed to list templates: ${error.message}`);
}
}
/**
* Adds a new custom template
* @param {Object} templateData - Template definition
* @returns {Promise<Object>} Updated COA document
*/
static async addTemplate(templateData) {
try {
// Validate template data
const requiredFields = ['key', 'label', 'accountType', 'hierarchy', 'labelPattern', 'keyPattern'];
for (const field of requiredFields) {
if (!templateData[field]) {
throw new Error(`Template must have ${field}`);
}
}
// Get current COA
const coa = await COA.loadCOAFromDB();
// Check if template already exists
const existingTemplates = coa.ledgerTemplates || [];
const existingTemplate = existingTemplates.find(t => t.key === templateData.key);
if (existingTemplate) {
throw new Error(`Template '${templateData.key}' already exists`);
}
// Add template with defaults
const newTemplate = {
key: templateData.key,
label: templateData.label,
description: templateData.description || '',
accountType: templateData.accountType,
hierarchy: templateData.hierarchy,
tags: templateData.tags || [],
labelPattern: templateData.labelPattern,
keyPattern: templateData.keyPattern,
defaultBalance: templateData.defaultBalance || 0
};
// Add to templates array
if (!coa.ledgerTemplates) {
coa.ledgerTemplates = [];
}
coa.ledgerTemplates.push(newTemplate);
coa.updatedAt = new Date().toISOString();
// Update COA document
const response = await getDb().insert(coa);
coa._rev = response.rev;
return coa;
} catch (error) {
throw new Error(`Failed to add template: ${error.message}`);
}
}
/**
* Removes a custom template (cannot remove default templates)
* @param {string} templateKey - Key of template to remove
* @returns {Promise<Object>} Updated COA document
*/
static async removeTemplate(templateKey) {
try {
// Get current COA
const coa = await COA.loadCOAFromDB();
if (!coa.ledgerTemplates) {
throw new Error('No templates found');
}
// Find template
const templateIndex = coa.ledgerTemplates.findIndex(t => t.key === templateKey);
if (templateIndex === -1) {
throw new Error(`Template '${templateKey}' not found`);
}
// Remove template
coa.ledgerTemplates.splice(templateIndex, 1);
coa.updatedAt = new Date().toISOString();
// Update COA document
const response = await getDb().insert(coa);
coa._rev = response.rev;
return coa;
} catch (error) {
throw new Error(`Failed to remove template: ${error.message}`);
}
}
/**
* Generates a key from name using the pattern
* @param {string} name - The name to convert
* @param {string} pattern - The key pattern (e.g., "{name-slug}")
* @returns {string} Generated key
*/
static generateKey(name, pattern) {
// Convert name to slug format
const slug = name
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Replace multiple hyphens with single
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
// Replace pattern placeholders
return pattern
.replace('{name-slug}', slug)
.replace('{name}', name);
}
/**
* Generates a label from name using the pattern
* @param {string} name - The name to use
* @param {string} pattern - The label pattern (e.g., "{name}")
* @returns {string} Generated label
*/
static generateLabel(name, pattern) {
// Replace pattern placeholders
return pattern
.replace('{name}', name)
.replace('{name-slug}', this.generateKey(name, '{name-slug}'));
}
/**
* Utility method to print available templates to console
* @param {Array} templates - Optional templates array, if not provided will fetch from DB
*/
static async printTemplates(templates = null) {
try {
if (!templates) {
templates = await this.listTemplates();
}
console.log('\n' + '='.repeat(60));
console.log('AVAILABLE LEDGER TEMPLATES');
console.log('='.repeat(60));
if (templates.length === 0) {
console.log('No templates available');
return;
}
for (const template of templates) {
console.log(`\nš ${template.label.toUpperCase()} (${template.key})`);
console.log(` Description: ${template.description || 'No description'}`);
console.log(` Account Type: ${template.accountType}`);
console.log(` Hierarchy: ${template.hierarchy}`);
console.log(` Tags: [${template.tags.join(', ')}]`);
console.log(` Example Key: ${this.generateKey('ABC Corporation', template.keyPattern)}`);
console.log(` Example Label: ${this.generateLabel('ABC Corporation', template.labelPattern)}`);
}
console.log('\n' + '='.repeat(60));
} catch (error) {
console.error('Failed to print templates:', error.message);
}
}
/**
* Utility method to demonstrate template usage
* @param {string} templateKey - Template key to demonstrate
* @param {string} exampleName - Example name to use
*/
static async demonstrateTemplate(templateKey, exampleName = 'Example Company') {
try {
const template = await this.getTemplate(templateKey);
if (!template) {
throw new Error(`Template '${templateKey}' not found`);
}
console.log(`\nš TEMPLATE DEMONSTRATION: ${template.label} (${template.key})`);
console.log(`Using example name: "${exampleName}"`);
console.log(`Generated key: ${this.generateKey(exampleName, template.keyPattern)}`);
console.log(`Generated label: ${this.generateLabel(exampleName, template.labelPattern)}`);
console.log(`Account type: ${template.accountType}`);
console.log(`Hierarchy: ${template.hierarchy}`);
console.log(`Tags: [${template.tags.join(', ')}]`);
console.log(`Default balance: ${template.defaultBalance}`);
} catch (error) {
console.error('Template demonstration failed:', error.message);
}
}
}
export default LedgerTemplate;