diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd9b67a..b6821676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## [Unreleased] +Added `get-results` command. It fetches the computed results and custom data of a reconciliation or account in a live company file (identified by its Silverfin URL) and prints them as JSON. Usage: `silverfin get-results -u `. Supports `-o, --output ` to write the JSON to a file instead of stdout. + ## [1.56.1] (08/06/2026) Increase the waiting time for the test runs to avoid timeout errors. diff --git a/bin/cli.js b/bin/cli.js index 79858a13..6a276277 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -3,6 +3,7 @@ const toolkit = require("../index"); const liquidTestGenerator = require("../lib/liquidTestGenerator"); const liquidTestRunner = require("../lib/liquidTestRunner"); +const resultsReader = require("../lib/resultsReader"); const { ExportFileInstanceGenerator } = require("../lib/exportFileInstanceGenerator"); const stats = require("../lib/cli/stats"); const { Command, Option } = require("commander"); @@ -527,6 +528,28 @@ program liquidTestGenerator.testGenerator(options.url, testName, reconciledStatus); }); +// GET RESULTS — read back a live company file's computed results + customs as JSON +program + .command("get-results") + .description("Fetch the computed results and custom data of a reconciliation or account in a live company file, printed as JSON") + .requiredOption("-u, --url ", "Specify the full Silverfin URL of the reconciliation/account in the company file (mandatory)") + .option("-o, --output ", "Write the JSON to a file instead of stdout (optional)") + .action(async (options) => { + const data = await resultsReader.fetchResults(options.url); + if (!data) { + process.exitCode = 1; + return; + } + const json = JSON.stringify(data, null, 2); + if (options.output) { + const fs = require("fs"); + fs.writeFileSync(options.output, json); + consola.success(`Wrote results to ${options.output}`); + } else { + console.log(json); + } + }); + // Check Liquid Test dependencies for a reconciliation template program .command("check-dependencies") diff --git a/lib/resultsReader.js b/lib/resultsReader.js new file mode 100644 index 00000000..6a92079f --- /dev/null +++ b/lib/resultsReader.js @@ -0,0 +1,58 @@ +const SF = require("./api/sfApi"); +const Utils = require("./utils/liquidTestUtils"); +const { consola } = require("consola"); + +/** + * Fetch the computed results and custom data of a reconciliation or account + * in a LIVE company file, identified by its Silverfin URL. + * + * Returns a plain object (no I/O) so it is easy to test and to serialise. + * Reuses the same getters and URL parsing as `create-test`. + * + * @param {String} url Full Silverfin URL of the reconciliation/account in the company file + * @returns {Promise} { templateType, firmId, companyId, periodId, ... , results, custom } or null on failure + */ +async function fetchResults(url) { + const parameters = Utils.extractURL(url); + + switch (parameters.templateType) { + case "reconciliationText": { + const customResponse = await SF.getReconciliationCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); + const resultsResponse = await SF.getReconciliationResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.reconciliationId); + return { + templateType: parameters.templateType, + firmId: parameters.firmId, + companyId: parameters.companyId, + periodId: parameters.ledgerId, + reconciliationId: parameters.reconciliationId, + results: resultsResponse?.data ?? null, + custom: customResponse?.data ?? null, + }; + } + case "accountTemplate": { + const account = await SF.findAccountByNumber(parameters.firmId, parameters.companyId, parameters.ledgerId, parameters.accountId); + const accountId = account?.account?.id; + if (!accountId) { + consola.error(`Account "${parameters.accountId}" could not be resolved in this company file.`); + return null; + } + const customResponse = await SF.getAccountTemplateCustom("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, accountId); + const resultsResponse = await SF.getAccountTemplateResults("firm", parameters.firmId, parameters.companyId, parameters.ledgerId, accountId); + return { + templateType: parameters.templateType, + firmId: parameters.firmId, + companyId: parameters.companyId, + periodId: parameters.ledgerId, + accountNumber: account.account.number, + accountId, + results: resultsResponse?.data ?? null, + custom: customResponse?.data ?? null, + }; + } + default: + consola.error(`Unsupported template type for URL: ${url}`); + return null; + } +} + +module.exports = { fetchResults }; diff --git a/tests/lib/resultsReader.test.js b/tests/lib/resultsReader.test.js new file mode 100644 index 00000000..054b9475 --- /dev/null +++ b/tests/lib/resultsReader.test.js @@ -0,0 +1,80 @@ +jest.mock("../../lib/api/sfApi"); +jest.mock("../../lib/utils/liquidTestUtils"); +jest.mock("consola"); + +const SF = require("../../lib/api/sfApi"); +const Utils = require("../../lib/utils/liquidTestUtils"); +const { consola } = require("consola"); +const { fetchResults } = require("../../lib/resultsReader"); + +describe("resultsReader.fetchResults", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("fetches results + customs for a reconciliation URL", async () => { + Utils.extractURL.mockReturnValue({ + templateType: "reconciliationText", + firmId: "96", + companyId: "100", + ledgerId: "200", + reconciliationId: "300", + }); + SF.getReconciliationCustom.mockResolvedValue({ data: [{ namespace: "ns", key: "k", value: "v" }] }); + SF.getReconciliationResults.mockResolvedValue({ data: { vol_1022_man: "5000.0" } }); + + const result = await fetchResults("https://live.getsilverfin.com/f/96/100/..."); + + expect(SF.getReconciliationResults).toHaveBeenCalledWith("firm", "96", "100", "200", "300"); + expect(result).toEqual({ + templateType: "reconciliationText", + firmId: "96", + companyId: "100", + periodId: "200", + reconciliationId: "300", + results: { vol_1022_man: "5000.0" }, + custom: [{ namespace: "ns", key: "k", value: "v" }], + }); + }); + + it("resolves the account and fetches results + customs for an account URL", async () => { + Utils.extractURL.mockReturnValue({ + templateType: "accountTemplate", + firmId: "96", + companyId: "100", + ledgerId: "200", + accountId: "610000", + }); + SF.findAccountByNumber.mockResolvedValue({ account: { id: 555, number: "610000", name: "Costs" } }); + SF.getAccountTemplateCustom.mockResolvedValue({ data: [{ namespace: "a", key: "b", value: 1 }] }); + SF.getAccountTemplateResults.mockResolvedValue({ data: { unreconciled_amount: "0.0" } }); + + const result = await fetchResults("https://live.getsilverfin.com/f/96/100/..."); + + expect(SF.getAccountTemplateResults).toHaveBeenCalledWith("firm", "96", "100", "200", 555); + expect(result).toMatchObject({ + templateType: "accountTemplate", + accountNumber: "610000", + accountId: 555, + results: { unreconciled_amount: "0.0" }, + custom: [{ namespace: "a", key: "b", value: 1 }], + }); + }); + + it("returns null and logs an error when the account cannot be resolved", async () => { + Utils.extractURL.mockReturnValue({ + templateType: "accountTemplate", + firmId: "96", + companyId: "100", + ledgerId: "200", + accountId: "999999", + }); + SF.findAccountByNumber.mockResolvedValue(null); + + const result = await fetchResults("https://live.getsilverfin.com/f/96/100/..."); + + expect(result).toBeNull(); + expect(consola.error).toHaveBeenCalled(); + expect(SF.getAccountTemplateResults).not.toHaveBeenCalled(); + }); +});