UNPKG

@debito/hippo-lib

Version:

Double-entry accounting library for CouchDB

366 lines (322 loc) • 15.1 kB
import config from '../config.js'; import Account from '../Account.js'; import COA from '../coa.js'; import ErrorLog from '../ErrorLog.js'; import { v4 as uuidv4 } from 'uuid'; // 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, accountType} * @param {boolean} [skipCOAUpdate=false] - Skip COA sync (used during bulk book initialization) * @returns {Promise<Account>} The created account instance */ static async createAccountFromTemplate(templateKey, name, overrides = {}, skipCOAUpdate = false) { 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); // Check for duplicate account const existing = await Account.findByKey(key); if (existing) { throw new Error(`Account '${name}' already exists`); } // Generate label from name const label = this.generateLabel(name, template.labelPattern); // Apply template values with overrides const openingBalance = overrides.balance !== undefined ? overrides.balance : template.defaultBalance; const accountData = { key, label, accountType: overrides.accountType || template.accountType, balance: 0, // Always create account with zero balance initially 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 with template metadata const account = await Account.new( accountData.key, accountData.label, accountData.accountType, accountData.balance, accountData.hierarchy, accountData.tags, { key: template.key, id: template.key, // Using key as id for consistency label: template.label, description: template.description }, skipCOAUpdate ); // Set the opening balance property and create journal entry if needed if (openingBalance !== 0 && template.openingbalanceAccountKey) { // Update the opening balance property in the account document try { await account.createOpeningBalanceJournalEntry(template.openingbalanceAccountKey, openingBalance); } catch (error) { console.warn(`Warning: Failed to create opening balance journal for account ${account.key}: ${error.message}`); ErrorLog.log('warn', 'LedgerTemplate.createAccount.openingBalance', error, { entityType: 'Account', entityId: account.key }); } } 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 ledger templates from COA settings * @returns {Promise<Object[]>} Array of template definition 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, openingbalanceAccountKey: templateData.openingbalanceAccountKey || null }; // 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; COA.updateCache(coa); 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; COA.updateCache(coa); return coa; } catch (error) { throw new Error(`Failed to remove template: ${error.message}`); } } /** * Updates an existing template * @param {string} templateKey - Key of template to update * @param {Object} updateData - Data to update {label, description, tags, etc.} * @returns {Promise<Object>} Updated COA document */ static async updateTemplate(templateKey, updateData) { 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`); } // Get existing template const existingTemplate = coa.ledgerTemplates[templateIndex]; // Update template with provided data, keeping existing values for unspecified fields const updatedTemplate = { ...existingTemplate, label: updateData.label !== undefined ? updateData.label : existingTemplate.label, description: updateData.description !== undefined ? updateData.description : existingTemplate.description, tags: updateData.tags !== undefined ? updateData.tags : existingTemplate.tags, // Allow updating other fields if provided accountType: updateData.accountType !== undefined ? updateData.accountType : existingTemplate.accountType, hierarchy: updateData.hierarchy !== undefined ? updateData.hierarchy : existingTemplate.hierarchy, labelPattern: updateData.labelPattern !== undefined ? updateData.labelPattern : existingTemplate.labelPattern, keyPattern: updateData.keyPattern !== undefined ? updateData.keyPattern : existingTemplate.keyPattern, defaultBalance: updateData.defaultBalance !== undefined ? updateData.defaultBalance : existingTemplate.defaultBalance, openingbalanceAccountKey: updateData.openingbalanceAccountKey !== undefined ? updateData.openingbalanceAccountKey : existingTemplate.openingbalanceAccountKey }; // Replace template in array coa.ledgerTemplates[templateIndex] = updatedTemplate; coa.updatedAt = new Date().toISOString(); // Update COA document const response = await getDb().insert(coa); coa._rev = response.rev; COA.updateCache(coa); return coa; } catch (error) { throw new Error(`Failed to update 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) .replace('{uuid}', uuidv4()); } /** * 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;