UNPKG

@debito/hippo-lib

Version:

Double-entry accounting library for CouchDB

270 lines (227 loc) 10.2 kB
import JournalEntry from '../JournalEntry.js'; import COA from '../coa.js'; import config from '../config.js'; // Getter to access database instance function getDb() { return config.db; } /** * JournalTemplate class for creating journal entries from predefined templates */ class JournalTemplate { /** * List all available journal templates * @returns {Promise<Array>} Array of journal template objects */ static async listTemplates() { try { // Get templates from COA settings const coa = await COA.loadCOAFromDB(); return coa.journelTemplates || []; } catch (error) { throw new Error(`Failed to list journal templates: ${error.message}`); } } /** * Create a journal entry from a template * @param {string} templateKey - The key of the template to use * @param {number} amount - The transaction amount * @param {Object} options - Options containing placeholder values (e.g., {vendor: "abc-corp"}) * @param {string} [description] - Optional description for the journal entry * @param {string} [date] - Optional ISO date string for the journal entry * @param {Object} [meta] - Optional meta object (e.g., {imageUrls: ["url1", "url2"]}) * @param {string} [externalRefId] - Optional external reference ID for duplicate prevention * @param {Array} [tags] - Optional array of additional tags to attach to the journal entry * @returns {Promise<JournalEntry>} The created and saved journal entry */ static async createJournalEntryFromTemplate(templateKey, amount, options = {}, description = null, date = null, meta = null, externalRefId = null, tags = []) { if (!templateKey || !amount) { throw new Error('Template key and amount are required'); } // Get the COA document to access journal templates const coaDoc = await COA.loadCOAFromDB(); if (!coaDoc.journelTemplates) { throw new Error('No journal templates found in COA'); } // Find the specified template const template = coaDoc.journelTemplates.find(t => t.key === templateKey); if (!template) { throw new Error(`Journal template '${templateKey}' not found`); } // Create the journal entry with description and template tag const entryDescription = description || template.description || `Entry from template: ${template.label}`; const journalEntry = await JournalEntry.new(entryDescription, tags.concat([templateKey]), externalRefId, date); // Set the meta object if provided if (meta) { journalEntry.meta = { ...journalEntry.meta, ...meta }; } // Add template information to the journal entry journalEntry._doc.templateInfo = { key: template.key, label: template.label, description: template.description }; // Process each line in the template for (const line of template.lines) { // Resolve account keys by replacing placeholders const accountKey = this._resolveAccountKey(line.accountKey, options); // Create line descriptions let lineDescription = line.description || entryDescription; lineDescription = lineDescription.replace('{description}', entryDescription); // Add the lines to the journal entry journalEntry.addLine(accountKey, line.type, amount, lineDescription); } // Post the journal entry (creates ledger entries and updates balances) await journalEntry.post(); return journalEntry; } /** * Add a new journal template * @param {Object} templateData - Template definition * @returns {Promise<Object>} Updated COA document */ static async addTemplate(templateData) { try { // Validate template data const requiredFields = ['key', 'label', 'lines']; for (const field of requiredFields) { if (!templateData[field]) { throw new Error(`Template must have ${field}`); } } if (!Array.isArray(templateData.lines) || templateData.lines.length === 0) { throw new Error('Template must have at least one line'); } // Validate each line for (const line of templateData.lines) { if (!line.accountKey || !line.type) { throw new Error('Each line must have accountKey and type'); } if (!['debit', 'credit'].includes(line.type)) { throw new Error('Line type must be "debit" or "credit"'); } } // Get current COA const coa = await COA.loadCOAFromDB(); // Check if template already exists const existingTemplates = coa.journelTemplates || []; 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 || '', tags: templateData.tags || [], lines: templateData.lines }; // Add to templates array if (!coa.journelTemplates) { coa.journelTemplates = []; } coa.journelTemplates.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 journal template: ${error.message}`); } } /** * Remove a journal template * @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.journelTemplates) { throw new Error('No templates found'); } // Find template const templateIndex = coa.journelTemplates.findIndex(t => t.key === templateKey); if (templateIndex === -1) { throw new Error(`Template '${templateKey}' not found`); } // Remove template coa.journelTemplates.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 journal template: ${error.message}`); } } /** * Update an existing journal template * @param {string} templateKey - Key of template to update * @param {Object} updateData - Data to update {label, description, tags} * @returns {Promise<Object>} Updated COA document */ static async updateTemplate(templateKey, updateData) { try { // Get current COA const coa = await COA.loadCOAFromDB(); if (!coa.journelTemplates) { throw new Error('No templates found'); } // Find template const templateIndex = coa.journelTemplates.findIndex(t => t.key === templateKey); if (templateIndex === -1) { throw new Error(`Template '${templateKey}' not found`); } // Get existing template const existingTemplate = coa.journelTemplates[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 }; // Replace template in array coa.journelTemplates[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 journal template: ${error.message}`); } } /** * Resolve account key by replacing placeholders with actual values * @param {string} accountKey - The account key that may contain placeholders * @param {Object} options - Options containing placeholder values * @returns {string} The resolved account key * @private */ static _resolveAccountKey(accountKey, options) { if (!accountKey) { throw new Error('Account key is required'); } // If it's a regular account key (no placeholder), return as-is if (!accountKey.startsWith('#')) { return accountKey; } // Extract the placeholder key (remove the $ prefix) const placeholderKey = accountKey.substring(1); // Look for the key in options if (options[placeholderKey]) { return options[placeholderKey]; } throw new Error(`Placeholder '${placeholderKey}' not found in options. Available: ${Object.keys(options).join(', ')}`); } } export default JournalTemplate;