diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d9e2d..75edd00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## [1.53.0] (28/01/2026) +Added `create-test` command support for account templates (fetches template data, period data, and custom data). + ## [1.52.0] (12/01/2026) This update improves test execution performance when running tests with status checks across multiple template handles. Tests are now run in parallel for multiple handles when using the `--status` flag, significantly reducing the overall execution time. Previously, tests with status checks for multiple handles would run sequentially, but now they leverage parallel processing for better efficiency. This change only affects the `silverfin run-test` command when both multiple handles and the status flag are used together. @@ -69,4 +72,4 @@ For example: `silverfin update-reconciliation --id 12345` - Add tests for the exportFile class ## [1.38.0] (04/07/2025) -- Added a changelog.md file and logic to display the changes when updating to latest version +- Added a changelog.md file and logic to display the changes when updating to latest version \ No newline at end of file diff --git a/lib/api/sfApi.js b/lib/api/sfApi.js index 4a8454b..6e54717 100644 --- a/lib/api/sfApi.js +++ b/lib/api/sfApi.js @@ -404,6 +404,30 @@ async function removeSharedPartFromAccountTemplate(type, envId, sharedPartId, ac } } +async function getAccountTemplateCustom(type, envId, companyId, periodId, accountTemplateId, page = 1) { + const instance = AxiosFactory.createInstance(type, envId); + try { + const response = await instance.get(`/companies/${companyId}/periods/${periodId}/accounts/${accountTemplateId}/custom`, { params: { page: page, per_page: 200 } }); + apiUtils.responseSuccessHandler(response); + return response; + } catch (error) { + const response = await apiUtils.responseErrorHandler(error); + return response; + } +} + +async function getAccountTemplateResults(type, envId, companyId, periodId, accountTemplateId) { + const instance = AxiosFactory.createInstance(type, envId); + try { + const response = await instance.get(`/companies/${companyId}/periods/${periodId}/accounts/${accountTemplateId}/results`); + apiUtils.responseSuccessHandler(response); + return response; + } catch (error) { + const response = await apiUtils.responseErrorHandler(error); + return response; + } +} + async function createTestRun(firmId, attributes, templateType) { const instance = AxiosFactory.createInstance("firm", firmId); let response; @@ -615,6 +639,34 @@ async function getAccountDetails(firmId, companyId, periodId, accountId) { } } +async function findAccountByNumber(firmId, companyId, periodId, accountNumber, page = 1) { + const instance = AxiosFactory.createInstance("firm", firmId); + try { + const response = await instance.get(`companies/${companyId}/periods/${periodId}/accounts`, { + params: { page: page }, + }); + + const accounts = response.data; + + // No data - end of pagination + if (accounts.length === 0) { + return null; + } + + // Look for the account in this page + const account = accounts.find((acc) => acc.account.number === accountNumber); + if (account) { + return account; + } + + // Not found in this page, try next page + return findAccountByNumber(firmId, companyId, periodId, accountNumber, page + 1); + } catch (error) { + const response = await apiUtils.responseErrorHandler(error); + return response; + } +} + // Liquid Linter // attributes should be JSON async function verifyLiquid(firmId, attributes) { @@ -698,6 +750,8 @@ module.exports = { findAccountTemplateByName, addSharedPartToAccountTemplate, removeSharedPartFromAccountTemplate, + getAccountTemplateCustom, + getAccountTemplateResults, readTestRun, createTestRun, createPreviewRun, @@ -712,6 +766,7 @@ module.exports = { findReconciliationInWorkflow, findReconciliationInWorkflows, getAccountDetails, + findAccountByNumber, verifyLiquid, getFirmDetails, createExportFileInstance, diff --git a/lib/liquidTestGenerator.js b/lib/liquidTestGenerator.js index ac497da..9d6ac11 100644 --- a/lib/liquidTestGenerator.js +++ b/lib/liquidTestGenerator.js @@ -3,15 +3,17 @@ const { firmCredentials } = require("../lib/api/firmCredentials"); const Utils = require("./utils/liquidTestUtils"); const { consola } = require("consola"); const { ReconciliationText } = require("./templates/reconciliationText"); +const { AccountTemplate } = require("./templates/accountTemplate"); const { SharedPart } = require("./templates/sharedPart"); // MainProcess async function testGenerator(url, testName, reconciledStatus = true) { - // Liquid Test Object - const liquidTestObject = Utils.createBaseLiquidTest(testName); - - // Get parameters from URL provided + // Get parameters from URL provided and determine template type const parameters = Utils.extractURL(url); + const templateType = parameters.templateType; + + // Create appropriate base test structure + const liquidTestObject = Utils.createBaseLiquidTest(testName, templateType); // Check if firm is authorized if (!Object.hasOwn(firmCredentials.data, parameters.firmId)) { @@ -22,14 +24,50 @@ async function testGenerator(url, testName, reconciledStatus = true) { // Reconciled Status (CLI argument. True by default) liquidTestObject[testName].expectation.reconciled = reconciledStatus; - // Get Reconciliation Details - const responseDetails = await SF.readReconciliationTextDetails("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); - const reconciliationHandle = responseDetails.data.handle; + let responseDetails, templateHandle; + switch (templateType) { + case "reconciliationText": { + // Get Reconciliation Details + responseDetails = await SF.readReconciliationTextDetails("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); + templateHandle = responseDetails.data.handle; + break; + } + case "accountTemplate": { + try { + // Get account data (includes template ID reference) + responseDetails = await SF.findAccountByNumber(parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.accountId); + + // Extract the template ID from the account data + const accountTemplateId = responseDetails.account_reconciliation_template?.id; + if (!accountTemplateId) { + throw new Error(`No account template associated with account ${parameters.accountId}`); + } + + // Get the actual template details using the ID + const templateDetails = await SF.readAccountTemplateById("firm", parameters.firmId, accountTemplateId); + templateHandle = templateDetails.name_nl; + + liquidTestObject[testName].context.current_account = responseDetails.account.number; + } catch (error) { + consola.error(`Failed to get account template details: ${error.message}`); + process.exit(1); + } + break; + } + } // Get Workflow Information - const starredStatus = { - ...SF.findReconciliationInWorkflow(parameters.firmId, reconciliationHandle, parameters.companyId, parameters.ledgerId, parameters.workflowId), - }.starred; + let starredStatus; + switch (templateType) { + case "reconciliationText": + starredStatus = { + ...(await SF.findReconciliationInWorkflow(parameters.firmId, templateHandle, parameters.companyId, parameters.ledgerId, parameters.workflowId)), + }.starred; + break; + case "accountTemplate": + starredStatus = responseDetails.starred; // Already present in the response of the Account + break; + } // Get period data const responsePeriods = await SF.getPeriods(parameters.firmId, parameters.companyId); @@ -57,125 +95,159 @@ async function testGenerator(url, testName, reconciledStatus = true) { const periodCustoms = Utils.processCustom(allPeriodCustoms); liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].custom = periodCustoms; } - process.exit; - // Get all the text properties (Customs from current template) - const responseCustom = await SF.getReconciliationCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); - const currentReconCustom = Utils.processCustom(responseCustom.data); - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[reconciliationHandle] = { - starred: starredStatus, - custom: currentReconCustom, - }; + // Get all the text properties (customs) and results from current template + switch (templateType) { + case "reconciliationText": { + const responseCustom = await SF.getReconciliationCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); + const currentReconCustom = Utils.processCustom(responseCustom.data); + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[templateHandle] = { + starred: starredStatus, + custom: currentReconCustom, + }; + + // Get all the results generated in current template + const responseResults = await SF.getReconciliationResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); + liquidTestObject[testName].expectation.results = responseResults.data; + + break; + } + case "accountTemplate": { + const responseCustom = await SF.getAccountTemplateCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, responseDetails.account.id); + const currentAccountTemplateCustom = Utils.processCustom(responseCustom.data); + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].accounts = { + [responseDetails.account.number]: { + name: responseDetails.account.name, + value: Number(responseDetails.value), + custom: currentAccountTemplateCustom, + starred: starredStatus, + }, + }; + + // Get all the results generated in current template + const responseResults = await SF.getAccountTemplateResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, responseDetails.account.id); + liquidTestObject[testName].expectation.results = responseResults.data; - // Get all the results generated in current template - const responseResults = await SF.getReconciliationResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); - liquidTestObject[testName].expectation.results = responseResults.data; + break; + } + } // Get the code of the template - const reconciliationTextCode = await ReconciliationText.read(reconciliationHandle); - if (!reconciliationTextCode) { - consola.warn(`Reconciliation "${reconciliationHandle}" wasn't found`); + let templateCode; + switch (templateType) { + case "reconciliationText": + templateCode = await ReconciliationText.read(templateHandle); + break; + case "accountTemplate": + templateCode = await AccountTemplate.read(templateHandle); + break; + } + + if (!templateCode) { + consola.warn(`Template "${templateHandle}" wasn't found`); process.exit(); } - // Search for results from other reconciliations used in the liquid code (main and text_parts) - let resultsObj; - resultsObj = Utils.searchForResultsFromDependenciesInLiquid(reconciliationTextCode, reconciliationHandle); - - // Search for custom drops from other reconcilations used in the liquid code (main and text_parts) - let customsObj; - customsObj = Utils.searchForCustomsFromDependenciesInLiquid(reconciliationTextCode, reconciliationHandle); - - // Search for shared parts in the liquid code (main and text_parts) - const sharedPartsUsed = Utils.lookForSharedPartsInLiquid(reconciliationTextCode, reconciliationHandle); - if (sharedPartsUsed && sharedPartsUsed.length != 0) { - for (const sharedPartName of sharedPartsUsed) { - const sharedPartCode = await SharedPart.read(sharedPartName); - if (!sharedPartCode) { - consola.warn(`Shared part "${sharedPartName}" wasn't found`); - return; - } + if (templateType === "reconciliationText") { + // Search for results from other reconciliations used in the liquid code (main and text_parts) + let resultsObj; + resultsObj = Utils.searchForResultsFromDependenciesInLiquid(templateCode, templateHandle); - // Look for nested shared parts (in that case, add them to this same loop) - const nestedSharedParts = Utils.lookForSharedPartsInLiquid(sharedPartCode); - for (const nested of nestedSharedParts) { - if (!sharedPartsUsed.includes(nested)) { - sharedPartsUsed.push(nested); + // Search for custom drops from other reconcilations used in the liquid code (main and text_parts) + let customsObj; + customsObj = Utils.searchForCustomsFromDependenciesInLiquid(templateCode, templateHandle); + + // Search for shared parts in the liquid code (main and text_parts) + const sharedPartsUsed = Utils.lookForSharedPartsInLiquid(templateCode, templateHandle); + if (sharedPartsUsed && sharedPartsUsed.length != 0) { + for (const sharedPartName of sharedPartsUsed) { + const sharedPartCode = await SharedPart.read(sharedPartName); + if (!sharedPartCode) { + consola.warn(`Shared part "${sharedPartName}" wasn't found`); + return; } - } - // Search for results from other reconciliations in shared part (we append to existing collection) - resultsObj = Utils.searchForResultsFromDependenciesInLiquid(sharedPartCode, sharedPartCode.name, resultsObj); + // Look for nested shared parts (in that case, add them to this same loop) + const nestedSharedParts = Utils.lookForSharedPartsInLiquid(sharedPartCode); + for (const nested of nestedSharedParts) { + if (!sharedPartsUsed.includes(nested)) { + sharedPartsUsed.push(nested); + } + } - // Search for custom drops from other reconcilations in shared parts (we append to existing collection) - customsObj = Utils.searchForCustomsFromDependenciesInLiquid(sharedPartCode, sharedPartCode.name, customsObj); + // Search for results from other reconciliations in shared part (we append to existing collection) + resultsObj = Utils.searchForResultsFromDependenciesInLiquid(sharedPartCode, sharedPartCode.name, resultsObj); + + // Search for custom drops from other reconcilations in shared parts (we append to existing collection) + customsObj = Utils.searchForCustomsFromDependenciesInLiquid(sharedPartCode, sharedPartCode.name, customsObj); + } } - } - // Get results from dependencies reconciliations - if (Object.keys(resultsObj).length !== 0) { - // Search in each reconciliation - for (const [handle, resultsArray] of Object.entries(resultsObj)) { - try { - // Find reconciliation in Workflow to get id (depdeency template can be in a different Workflow) - const reconciliation = await SF.findReconciliationInWorkflows(parameters.firmId, handle, parameters.companyId, parameters.ledgerId); - if (reconciliation) { - // Fetch results - const reconciliationResults = await SF.getReconciliationResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, reconciliation.id); - // Add handle and results block to Liquid Test - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] = - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] || {}; - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results = - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results || {}; - // Search for results - for (const resultTag of resultsArray) { - // Add result to Liquid Test - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results[resultTag] = reconciliationResults.data[resultTag]; + // Get results from dependencies reconciliations + if (Object.keys(resultsObj).length !== 0) { + // Search in each reconciliation + for (const [handle, resultsArray] of Object.entries(resultsObj)) { + try { + // Find reconciliation in Workflow to get id (depdeency template can be in a different Workflow) + const reconciliation = await SF.findReconciliationInWorkflows(parameters.firmId, handle, parameters.companyId, parameters.ledgerId); + if (reconciliation) { + // Fetch results + const reconciliationResults = await SF.getReconciliationResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, reconciliation.id); + // Add handle and results block to Liquid Test + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] = + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] || {}; + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results = + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results || {}; + // Search for results + for (const resultTag of resultsArray) { + // Add result to Liquid Test + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].results[resultTag] = reconciliationResults.data[resultTag]; + } } + } catch (err) { + consola.error(err); } - } catch (err) { - consola.error(err); } } - } - // We already got the text properties from current reconciliation - if (Object.hasOwn(customsObj, reconciliationHandle)) { - delete customsObj[reconciliationHandle]; - } + // We already got the text properties from current reconciliation + if (Object.hasOwn(customsObj, templateHandle)) { + delete customsObj[templateHandle]; + } - // Get custom drops from dependency reconciliations - if (Object.keys(customsObj).length !== 0) { - // Search in each reconciliation - for (const [handle, customsArray] of Object.entries(customsObj)) { - try { - // Find reconciliation in Workflow to get id (depdeency template can be in a different Workflow) - const reconciliation = await SF.findReconciliationInWorkflows(parameters.firmId, handle, parameters.companyId, parameters.ledgerId); - if (reconciliation) { - // Fetch test properties - const reconciliationCustomResponse = await SF.getReconciliationCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, reconciliation.id); - const reconciliationCustomDrops = Utils.processCustom(reconciliationCustomResponse.data); - // Filter Customs - const dropsKeys = Object.keys(reconciliationCustomDrops); - const matchingKeys = dropsKeys.filter((key) => customsArray.indexOf(key) !== -1); - const filteredCustomDrops = {}; - for (const key of matchingKeys) { - filteredCustomDrops[key] = reconciliationCustomDrops[key]; + // Get custom drops from dependency reconciliations + if (Object.keys(customsObj).length !== 0) { + // Search in each reconciliation + for (const [handle, customsArray] of Object.entries(customsObj)) { + try { + // Find reconciliation in Workflow to get id (depdeency template can be in a different Workflow) + const reconciliation = await SF.findReconciliationInWorkflows(parameters.firmId, handle, parameters.companyId, parameters.ledgerId); + if (reconciliation) { + // Fetch test properties + const reconciliationCustomResponse = await SF.getReconciliationCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, reconciliation.id); + const reconciliationCustomDrops = Utils.processCustom(reconciliationCustomResponse.data); + // Filter Customs + const dropsKeys = Object.keys(reconciliationCustomDrops); + const matchingKeys = dropsKeys.filter((key) => customsArray.indexOf(key) !== -1); + const filteredCustomDrops = {}; + for (const key of matchingKeys) { + filteredCustomDrops[key] = reconciliationCustomDrops[key]; + } + // Add handle to Liquid Test + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] = + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] || {}; + // Add custom drops + liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].custom = filteredCustomDrops; } - // Add handle to Liquid Test - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] = - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle] || {}; - // Add custom drops - liquidTestObject[testName].data.periods[currentPeriodData.fiscal_year.end_date].reconciliations[handle].custom = filteredCustomDrops; + } catch (err) { + consola.error(err); } - } catch (err) { - consola.error(err); } } } // Get company drop used in the liquid code (main and text_parts) - const companyObj = Utils.getCompanyDependencies(reconciliationTextCode, reconciliationHandle); + const companyObj = Utils.getCompanyDependencies(templateCode, templateHandle); if (companyObj.standardDropElements.length !== 0 || companyObj.customDropElements.length !== 0) { liquidTestObject[testName].data.company = {}; @@ -242,7 +314,7 @@ async function testGenerator(url, testName, reconciledStatus = true) { } // Save YAML - Utils.exportYAML(reconciliationHandle, liquidTestObject); + Utils.exportYAML(templateHandle, liquidTestObject, templateType); } module.exports = { diff --git a/lib/utils/liquidTestUtils.js b/lib/utils/liquidTestUtils.js index e3ae9f6..ba66902 100644 --- a/lib/utils/liquidTestUtils.js +++ b/lib/utils/liquidTestUtils.js @@ -4,8 +4,8 @@ const fsUtils = require("./fsUtils"); const { consola } = require("consola"); // Create base Liquid Test object -function createBaseLiquidTest(testName) { - return { +function createBaseLiquidTest(testName, templateType = "reconciliationText") { + const baseStructure = { [testName]: { context: { period: "#Replace with period", @@ -13,38 +13,50 @@ function createBaseLiquidTest(testName) { data: { periods: { replace_period_name: { - reconciliations: {}, + reconciliations: {}, // Only for reconciliation texts }, }, }, expectation: { reconciled: "#Replace with reconciled status", results: {}, + rollforward: {}, }, }, }; + + // Add current_account for account templates + if (templateType === "accountTemplate") { + baseStructure[testName].context.current_account = "#Replace with current account"; + delete baseStructure[testName].data.periods.replace_period_name.reconciliations; // Remove reconciliations + } + + return baseStructure; } -// Provide a link to reconciliation in Silverfin -// Extract firm id, company id, period id, reconciliation id +// Provide a link to reconciliation or account template in Silverfin +// Extract template type, firm id, company id, period id, template id function extractURL(url) { try { const parts = url.split("?")[0].split("/f/")[1].split("/"); - let type; + let idType, templateType; if (parts.indexOf("reconciliation_texts") !== -1) { - type = "reconciliationId"; + idType = "reconciliationId"; + templateType = "reconciliationText"; } else if (parts.indexOf("account_entry") !== -1) { - type = "accountId"; + idType = "accountId"; + templateType = "accountTemplate"; } else { consola.error("Not possible to identify if it's a reconciliation text or account entry."); process.exit(1); } return { + templateType, firmId: parts[0], companyId: parts[1], ledgerId: parts[3], workflowId: parts[5], - [type]: parts[7], + [idType]: parts[7], }; } catch (err) { consola.error("The URL provided is not correct. Double check it and run the command again."); @@ -52,23 +64,45 @@ function extractURL(url) { } } -function generateFileName(handle, counter = 0) { +function generateFileName(handle, templateType, counter = 0) { let fileName = `${handle}_liquid_test.yml`; if (counter != 0) { fileName = `${handle}_${counter}_liquid_test.yml`; } - const filePath = `./reconciliation_texts/${handle}/tests/${fileName}`; + let filePath; + switch (templateType) { + case "reconciliationText": + filePath = `./reconciliation_texts/${handle}/tests/${fileName}`; + break; + case "accountTemplate": + filePath = `./account_templates/${handle}/tests/${fileName}`; + break; + default: + consola.error("Invalid template type"); + process.exit(1); + } if (fs.existsSync(filePath)) { - return generateFileName(handle, counter + 1); + return generateFileName(handle, templateType, counter + 1); } return filePath; } // Create YAML -function exportYAML(handle, liquidTestObject) { - fsUtils.createFolder(`./reconciliation_texts`); - fsUtils.createTemplateFolders("reconciliationText", handle, true); - const filePath = generateFileName(handle); +function exportYAML(handle, liquidTestObject, templateType) { + switch (templateType) { + case "reconciliationText": + fsUtils.createFolder(`./reconciliation_texts`); + fsUtils.createTemplateFolders("reconciliationText", handle, true); + break; + case "accountTemplate": + fsUtils.createFolder(`./account_templates`); + fsUtils.createTemplateFolders("accountTemplate", handle, true); + break; + default: + consola.error("Invalid template type"); + process.exit(1); + } + const filePath = generateFileName(handle, templateType); fs.writeFile( filePath, YAML.stringify(liquidTestObject, { @@ -138,20 +172,20 @@ function processCustom(customArray) { } // Company Drop used -function getCompanyDependencies(reconcilationObject, reconciliationHandle) { +function getCompanyDependencies(templateCode, templateHandle) { const reCompanySearch = RegExp(/company\.\w+(?:\.\w+\.\w+)?/g); // company.foo or company.custom.foo.bar // No main part ? - if (!reconcilationObject || !reconcilationObject.text) { - consola.warn(`Reconciliation "${reconciliationHandle}": no liquid code found`); + if (!templateCode || !templateCode.text) { + consola.warn(`Template "${templateHandle}": no liquid code found`); return { standardDropElements: [], customDropElements: [] }; } // Main Part - let companyFound = reconcilationObject.text.match(reCompanySearch) || []; + let companyFound = templateCode.text.match(reCompanySearch) || []; // Parts - for (const part of reconcilationObject.text_parts) { + for (const part of templateCode.text_parts) { const companyPart = part.content.match(reCompanySearch) || []; if (companyPart) { companyFound = companyFound.concat(companyPart); diff --git a/package-lock.json b/package-lock.json index 687d65f..a15f7da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "silverfin-cli", - "version": "1.52.0", + "version": "1.53.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "silverfin-cli", - "version": "1.52.0", + "version": "1.53.0", "license": "MIT", "dependencies": { "axios": "^1.6.2", @@ -30,13 +30,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -45,9 +45,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "dev": true, "license": "MIT", "engines": { @@ -55,21 +55,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -86,14 +86,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -103,13 +103,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -130,29 +130,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -162,9 +162,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -202,27 +202,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -287,13 +287,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -329,13 +329,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -455,13 +455,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -471,33 +471,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", "debug": "^4.3.1" }, "engines": { @@ -505,9 +505,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1265,9 +1265,9 @@ } }, "node_modules/@types/node": { - "version": "25.0.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.6.tgz", - "integrity": "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==", + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "dev": true, "license": "MIT", "dependencies": { @@ -1413,9 +1413,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1561,9 +1561,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", - "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1690,9 +1690,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001764", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", - "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "dev": true, "funding": [ { @@ -2031,9 +2031,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.279", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", + "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", "dev": true, "license": "ISC" }, diff --git a/package.json b/package.json index 2f115c6..6f4d3c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silverfin-cli", - "version": "1.52.0", + "version": "1.53.0", "description": "Command line tool for Silverfin template development", "main": "index.js", "license": "MIT", diff --git a/tests/lib/liquidTestGenerator.test.js b/tests/lib/liquidTestGenerator.test.js index f522755..09bdb44 100644 --- a/tests/lib/liquidTestGenerator.test.js +++ b/tests/lib/liquidTestGenerator.test.js @@ -3,6 +3,7 @@ const SF = require("../../lib/api/sfApi"); const { firmCredentials } = require("../../lib/api/firmCredentials"); const Utils = require("../../lib/utils/liquidTestUtils"); const { ReconciliationText } = require("../../lib/templates/reconciliationText"); +const { AccountTemplate } = require("../../lib/templates/accountTemplate"); const { SharedPart } = require("../../lib/templates/sharedPart"); const { consola } = require("consola"); @@ -10,6 +11,7 @@ const { consola } = require("consola"); jest.mock("../../lib/api/sfApi"); jest.mock("../../lib/api/firmCredentials"); jest.mock("../../lib/templates/reconciliationText"); +jest.mock("../../lib/templates/accountTemplate"); jest.mock("../../lib/templates/sharedPart"); jest.mock("consola"); @@ -28,6 +30,7 @@ describe("liquidTestGenerator", () => { const mockUrl = "https://live.getsilverfin.com/f/123/456/ledgers/789/workflows/101/reconciliation_texts/202"; const mockTestName = "unit_1_test_1"; const mockParameters = { + templateType: "reconciliationText", firmId: "123", companyId: "456", ledgerId: "789", @@ -167,7 +170,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); @@ -175,7 +179,7 @@ describe("liquidTestGenerator", () => { ReconciliationText.read.mockResolvedValue(false); await expect(testGenerator(mockUrl, mockTestName)).rejects.toThrow("Process.exit called with code undefined"); - expect(consola.warn).toHaveBeenCalledWith(`Reconciliation "${mockReconciliationHandle}" wasn't found`); + expect(consola.warn).toHaveBeenCalledWith(`Template "${mockReconciliationHandle}" wasn't found`); }); it("should read shared parts correctly", async () => { @@ -225,7 +229,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); @@ -247,7 +252,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); }); @@ -265,7 +271,7 @@ describe("liquidTestGenerator", () => { ReconciliationText.read.mockResolvedValue(false); await expect(testGenerator(mockUrl, mockTestName)).rejects.toThrow("Process.exit called with code undefined"); - expect(consola.warn).toHaveBeenCalledWith(`Reconciliation "${mockReconciliationHandle}" wasn't found`); + expect(consola.warn).toHaveBeenCalledWith(`Template "${mockReconciliationHandle}" wasn't found`); }); it("should warn and return gracefully for missing shared parts", async () => { @@ -305,7 +311,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); @@ -330,7 +337,282 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" + ); + }); + }); + + describe("account template test generation", () => { + const mockAccountUrl = "https://live.getsilverfin.com/f/123/456/ledgers/789/workflows/101/account_entry/5000"; + const mockAccountParameters = { + templateType: "accountTemplate", + firmId: "123", + companyId: "456", + ledgerId: "789", + workflowId: "101", + accountId: "5000", + }; + const mockAccountTemplateHandle = "test_account_template"; + const mockAccountResponse = { + account: { + id: 1001, + number: "5000", + name: "Test Account", + }, + value: "12345.67", + starred: false, + account_reconciliation_template: { + id: 9999, + }, + }; + const mockAccountTemplateDetails = { + name_nl: "test_account_template", + name_en: "Test Account Template", + id: 9999, + }; + const mockAccountTemplate = { + name_nl: "test_account_template", + id: 9999, + text: "Main liquid content for account template", + text_parts: [{ name: "part_1", content: "Part 1 content" }], + externally_managed: true, + }; + + beforeEach(() => { + // Override extractURL for account template tests + Utils.extractURL.mockReturnValue(mockAccountParameters); + Utils.createBaseLiquidTest.mockReturnValue({ + [mockTestName]: { + context: { period: "2024-12-31" }, + data: { + periods: { + replace_period_name: { + accounts: {}, + }, + }, + }, + expectation: { + reconciled: true, + results: {}, + }, + }, + }); + + // Mock AccountTemplate.read + AccountTemplate.read.mockResolvedValue(mockAccountTemplate); + + // Mock SF API calls for account templates + SF.findAccountByNumber = jest.fn().mockResolvedValue(mockAccountResponse); + SF.readAccountTemplateById = jest.fn().mockResolvedValue(mockAccountTemplateDetails); + SF.getAccountTemplateCustom = jest.fn().mockResolvedValue({ + data: [ + { + namespace: "account_namespace", + key: "account_key", + value: "account_value", + }, + ], + }); + SF.getAccountTemplateResults = jest.fn().mockResolvedValue({ + data: { account_result1: "value1", account_result2: "value2" }, + }); + }); + + it("should read account template correctly", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + // Verify account lookup + expect(SF.findAccountByNumber).toHaveBeenCalledWith( + mockAccountParameters.firmId, + mockAccountParameters.companyId, + mockAccountParameters.ledgerId, + mockAccountParameters.accountId + ); + + // Verify template details fetch + expect(SF.readAccountTemplateById).toHaveBeenCalledWith("firm", mockAccountParameters.firmId, 9999); + + // Verify AccountTemplate.read was called with the correct handle + expect(AccountTemplate.read).toHaveBeenCalledWith(mockAccountTemplateHandle); + }); + + it("should set current_account in context", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + context: expect.objectContaining({ + current_account: "5000", + period: "2024-12-31", + }), + }), + }), + "accountTemplate" + ); + }); + + it("should fetch account template custom and results", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + // Verify custom fetch + expect(SF.getAccountTemplateCustom).toHaveBeenCalledWith( + "firm", + mockAccountParameters.firmId, + mockAccountParameters.companyId, + mockAccountParameters.ledgerId, + mockAccountResponse.account.id + ); + + // Verify results fetch + expect(SF.getAccountTemplateResults).toHaveBeenCalledWith( + "firm", + mockAccountParameters.firmId, + mockAccountParameters.companyId, + mockAccountParameters.ledgerId, + mockAccountResponse.account.id + ); + + // Verify account data structure in test object + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + data: expect.objectContaining({ + periods: expect.objectContaining({ + "2024-12-31": expect.objectContaining({ + accounts: expect.objectContaining({ + 5000: expect.objectContaining({ + name: "Test Account", + value: 12345.67, + custom: expect.objectContaining({ + "account_namespace.account_key": "account_value", + }), + }), + }), + }), + }), + }), + expectation: expect.objectContaining({ + results: expect.objectContaining({ + account_result1: "value1", + account_result2: "value2", + }), + }), + }), + }), + "accountTemplate" + ); + }); + + it("should use starred status from account response", async () => { + const starredAccountResponse = { + ...mockAccountResponse, + starred: true, + }; + SF.findAccountByNumber.mockResolvedValue(starredAccountResponse); + + await testGenerator(mockAccountUrl, mockTestName); + + // The starred status should be extracted from response (not from workflow lookup) + expect(SF.findReconciliationInWorkflow).not.toHaveBeenCalled(); + }); + + it("should skip dependency resolution for account templates", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + // Verify shared parts are NOT searched (dependency resolution is skipped) + expect(SharedPart.read).not.toHaveBeenCalled(); + + // Verify the test object does not contain reconciliation dependencies + // (only account data should be present) + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + data: expect.objectContaining({ + periods: expect.objectContaining({ + "2024-12-31": expect.objectContaining({ + accounts: expect.any(Object), + // Should not have reconciliations object + }), + }), + }), + }), + }), + "accountTemplate" + ); + }); + + it("should handle missing account template association gracefully", async () => { + const accountWithoutTemplate = { + ...mockAccountResponse, + account_reconciliation_template: null, + }; + SF.findAccountByNumber.mockResolvedValue(accountWithoutTemplate); + + await expect(testGenerator(mockAccountUrl, mockTestName)).rejects.toThrow("Process.exit called with code 1"); + expect(consola.error).toHaveBeenCalledWith(expect.stringContaining("No account template associated with account")); + }); + + it("should handle missing account template file gracefully", async () => { + AccountTemplate.read.mockResolvedValue(false); + + await expect(testGenerator(mockAccountUrl, mockTestName)).rejects.toThrow("Process.exit called with code undefined"); + expect(consola.warn).toHaveBeenCalledWith(`Template "${mockAccountTemplateHandle}" wasn't found`); + }); + + it("should handle account lookup errors gracefully", async () => { + SF.findAccountByNumber.mockRejectedValue(new Error("Account not found")); + + await expect(testGenerator(mockAccountUrl, mockTestName)).rejects.toThrow("Process.exit called with code 1"); + expect(consola.error).toHaveBeenCalledWith(expect.stringContaining("Failed to get account template details")); + }); + + it("should process period custom data for account templates", async () => { + await testGenerator(mockAccountUrl, mockTestName); + + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + data: expect.objectContaining({ + periods: expect.objectContaining({ + "2024-12-31": expect.objectContaining({ + custom: expect.objectContaining({ + "pit_integration.code_1002": "yes", + }), + }), + }), + }), + }), + }), + "accountTemplate" + ); + }); + + it("should handle empty period custom data for account templates", async () => { + SF.getAllPeriodCustom.mockResolvedValue([]); + + await testGenerator(mockAccountUrl, mockTestName); + + // Should not add custom data if empty + expect(Utils.exportYAML).toHaveBeenCalledWith( + mockAccountTemplateHandle, + expect.objectContaining({ + [mockTestName]: expect.objectContaining({ + data: expect.objectContaining({ + periods: expect.objectContaining({ + "2024-12-31": expect.not.objectContaining({ + custom: expect.anything(), + }), + }), + }), + }), + }), + "accountTemplate" ); }); }); diff --git a/tests/lib/templates/accountTemplates.test.js b/tests/lib/templates/accountTemplates.test.js index 3503be0..3863c50 100644 --- a/tests/lib/templates/accountTemplates.test.js +++ b/tests/lib/templates/accountTemplates.test.js @@ -13,6 +13,8 @@ describe("AccountTemplate", () => { const textParts = { part_1: "Part 1: updated content" }; const template = { name_nl: "name_nl", + name_en: "", + name_fr: "", id: 808080, text: "Main liquid content", text_parts: [