From 6569561b870b0eb17536e7298a26403092274425 Mon Sep 17 00:00:00 2001 From: delphine-demeulenaere Date: Tue, 8 Apr 2025 16:47:03 +0200 Subject: [PATCH 1/5] add more tests for AT's (WIP) --- tests/lib/templates/accountTemplates.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/lib/templates/accountTemplates.test.js b/tests/lib/templates/accountTemplates.test.js index 3503be0..fa1651b 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_nl", + name_fr: "name_nl", id: 808080, text: "Main liquid content", text_parts: [ From 817eb570703a947ded5ee1a269fb4df67ce4c8cd Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Mon, 28 Jul 2025 11:34:32 +0200 Subject: [PATCH 2/5] Add update to changelog and bump version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caa3194..51fcddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,4 +51,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 From 8151798c8d9b6ba2ef3e87aa55cf3954d80bae07 Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Wed, 17 Dec 2025 13:25:32 +0100 Subject: [PATCH 3/5] Activate create-test command for account templates --- lib/api/sfApi.js | 54 +++++ lib/liquidTestGenerator.js | 278 ++++++++++++++++---------- lib/utils/liquidTestUtils.js | 76 +++++-- tests/lib/liquidTestGenerator.test.js | 20 +- 4 files changed, 297 insertions(+), 131 deletions(-) diff --git a/lib/api/sfApi.js b/lib/api/sfApi.js index eae4dd5..d53447e 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,33 @@ 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) { + apiUtils.responseErrorHandler(error); + } +} + // Liquid Linter // attributes should be JSON async function verifyLiquid(firmId, attributes) { @@ -674,6 +725,8 @@ module.exports = { findAccountTemplateByName, addSharedPartToAccountTemplate, removeSharedPartFromAccountTemplate, + getAccountTemplateCustom, + getAccountTemplateResults, readTestRun, createTestRun, createPreviewRun, @@ -688,6 +741,7 @@ module.exports = { findReconciliationInWorkflow, findReconciliationInWorkflows, getAccountDetails, + findAccountByNumber, verifyLiquid, getFirmDetails, }; diff --git a/lib/liquidTestGenerator.js b/lib/liquidTestGenerator.js index ac497da..f4f6ced 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 = { + ...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); @@ -59,123 +97,157 @@ async function testGenerator(url, testName, reconciledStatus = true) { } 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, + }, + }; + + // 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/tests/lib/liquidTestGenerator.test.js b/tests/lib/liquidTestGenerator.test.js index f522755..e12c8bd 100644 --- a/tests/lib/liquidTestGenerator.test.js +++ b/tests/lib/liquidTestGenerator.test.js @@ -28,6 +28,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 +168,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); @@ -175,7 +177,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 +227,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); @@ -247,7 +250,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); }); @@ -265,7 +269,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 +309,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); @@ -330,7 +335,8 @@ describe("liquidTestGenerator", () => { }), }), }), - }) + }), + "reconciliationText" ); }); }); From bb835b7eda93b94c83a013e920f1614b6520963a Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Wed, 17 Dec 2025 13:27:30 +0100 Subject: [PATCH 4/5] Fix tests for AT --- tests/lib/templates/accountTemplates.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/templates/accountTemplates.test.js b/tests/lib/templates/accountTemplates.test.js index fa1651b..3863c50 100644 --- a/tests/lib/templates/accountTemplates.test.js +++ b/tests/lib/templates/accountTemplates.test.js @@ -13,8 +13,8 @@ describe("AccountTemplate", () => { const textParts = { part_1: "Part 1: updated content" }; const template = { name_nl: "name_nl", - name_en: "name_nl", - name_fr: "name_nl", + name_en: "", + name_fr: "", id: 808080, text: "Main liquid content", text_parts: [ From 52707268985e6ade11666e7d0a287f26511fe43a Mon Sep 17 00:00:00 2001 From: Michiel Degezelle Date: Wed, 17 Dec 2025 13:31:29 +0100 Subject: [PATCH 5/5] Add tests for account templates liquid tests --- tests/lib/liquidTestGenerator.test.js | 276 ++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/tests/lib/liquidTestGenerator.test.js b/tests/lib/liquidTestGenerator.test.js index e12c8bd..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"); @@ -340,5 +342,279 @@ describe("liquidTestGenerator", () => { ); }); }); + + 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" + ); + }); + }); }); });