From d0c9ffecb165e9bcfc53015240a4eedcfb84b750 Mon Sep 17 00:00:00 2001 From: Richard Davies Date: Sun, 7 Jun 2026 19:03:55 +0100 Subject: [PATCH] feat: add tracking-category/option filtering to list-profit-and-loss Add optional trackingCategoryID, trackingOptionID, trackingCategoryID2 and trackingOptionID2 params to the list-profit-and-loss tool, exposing the tracking filters the Xero P&L report endpoint natively supports (per-option breakdown, single-option filter, or cross-tab against a second category). Refactor listXeroProfitAndLoss and the inner fetch to take a single ListProfitAndLossParams object, mirroring the sibling list-report-balance-sheet handler. This removes the fragile positional argument passing and structurally fixes a pre-existing bug: the handler previously called fetchProfitAndLoss with only 5 positional args, so paymentsOnly landed in the standardLayout slot and the real paymentsOnly was dropped (both flags were broken). It also corrects the SDK argument order to categoryID, categoryID2, optionID, optionID2. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../list-xero-profit-and-loss.handler.ts | 70 +++++++++---------- src/tools/list/list-profit-and-loss.tool.ts | 24 ++++--- src/types/list-profit-and-loss-params.ts | 15 ++++ 3 files changed, 64 insertions(+), 45 deletions(-) create mode 100644 src/types/list-profit-and-loss-params.ts diff --git a/src/handlers/list-xero-profit-and-loss.handler.ts b/src/handlers/list-xero-profit-and-loss.handler.ts index e19a7bfc..5a2484b5 100644 --- a/src/handlers/list-xero-profit-and-loss.handler.ts +++ b/src/handlers/list-xero-profit-and-loss.handler.ts @@ -1,35 +1,41 @@ import { xeroClient } from "../clients/xero-client.js"; import { XeroClientResponse } from "../types/tool-response.js"; +import { ListProfitAndLossParams } from "../types/list-profit-and-loss-params.js"; import { formatError } from "../helpers/format-error.js"; import { getClientHeaders } from "../helpers/get-client-headers.js"; import { ReportWithRow } from "xero-node"; -// Define the valid timeframe options -type TimeframeType = "MONTH" | "QUARTER" | "YEAR" | undefined; - /** * Internal function to fetch profit and loss data from Xero */ async function fetchProfitAndLoss( - fromDate?: string, - toDate?: string, - periods?: number, - timeframe?: TimeframeType, - standardLayout?: boolean, - paymentsOnly?: boolean, + params: ListProfitAndLossParams, ): Promise { await xeroClient.authenticate(); + const { + fromDate, + toDate, + periods, + timeframe, + trackingCategoryID, + trackingOptionID, + trackingCategoryID2, + trackingOptionID2, + standardLayout, + paymentsOnly, + } = params; + const response = await xeroClient.accountingApi.getReportProfitAndLoss( xeroClient.tenantId, fromDate, toDate, periods, timeframe, - undefined, // trackingCategoryID - undefined, // trackingOptionID - undefined, // trackingCategoryID2 - undefined, // trackingOptionID2 + trackingCategoryID, + trackingCategoryID2, + trackingOptionID, + trackingOptionID2, standardLayout, paymentsOnly, getClientHeaders(), @@ -40,33 +46,23 @@ async function fetchProfitAndLoss( /** * List profit and loss report from Xero - * @param fromDate Optional start date for the report (YYYY-MM-DD) - * @param toDate Optional end date for the report (YYYY-MM-DD) - * @param periods Optional number of periods for the report - * @param timeframe Optional timeframe for the report (MONTH, QUARTER, YEAR) - * @param trackingCategoryID Optional tracking category ID - * @param trackingOptionID Optional tracking option ID - * @param trackingCategoryID2 Optional second tracking category ID - * @param trackingOptionID2 Optional second tracking option ID - * @param standardLayout Optional boolean to use standard layout - * @param paymentsOnly Optional boolean to include only accounts with payments + * @param params Optional parameters for the report: + * - fromDate: Optional start date for the report (YYYY-MM-DD) + * - toDate: Optional end date for the report (YYYY-MM-DD) + * - periods: Optional number of periods for the report + * - timeframe: Optional timeframe for the report (MONTH, QUARTER, YEAR) + * - trackingCategoryID: Optional tracking category ID + * - trackingOptionID: Optional tracking option ID + * - trackingCategoryID2: Optional second tracking category ID + * - trackingOptionID2: Optional second tracking option ID + * - standardLayout: Optional boolean to use standard layout + * - paymentsOnly: Optional boolean to include only accounts with payments */ export async function listXeroProfitAndLoss( - fromDate?: string, - toDate?: string, - periods?: number, - timeframe?: TimeframeType, - standardLayout?: boolean, - paymentsOnly?: boolean, + params: ListProfitAndLossParams, ): Promise> { try { - const profitAndLoss = await fetchProfitAndLoss( - fromDate, - toDate, - periods, - timeframe, - paymentsOnly, - ); + const profitAndLoss = await fetchProfitAndLoss(params); if (!profitAndLoss) { return { @@ -88,4 +84,4 @@ export async function listXeroProfitAndLoss( error: formatError(error), }; } -} \ No newline at end of file +} diff --git a/src/tools/list/list-profit-and-loss.tool.ts b/src/tools/list/list-profit-and-loss.tool.ts index 72e6aba1..f5178139 100644 --- a/src/tools/list/list-profit-and-loss.tool.ts +++ b/src/tools/list/list-profit-and-loss.tool.ts @@ -10,18 +10,26 @@ const ListProfitAndLossTool = CreateXeroTool( toDate: z.string().optional().describe("Optional end date in YYYY-MM-DD format"), periods: z.number().optional().describe("Optional number of periods to compare"), timeframe: z.enum(["MONTH", "QUARTER", "YEAR"]).optional().describe("Optional timeframe for the report (MONTH, QUARTER, YEAR)"), + trackingCategoryID: z.string().optional().describe("Optional tracking category ID to scope the report to a tracking dimension. Obtain IDs from the list-tracking-categories tool. Supply this alone for a per-option breakdown (one column per option in the category); supply it together with trackingOptionID to filter the whole P&L to that single option."), + trackingOptionID: z.string().optional().describe("Optional tracking option ID. Obtain IDs from the list-tracking-categories tool. Must be supplied together with trackingCategoryID; it filters the whole P&L to that single option of that category."), + trackingCategoryID2: z.string().optional().describe("Optional SECOND tracking category ID for a cross-tab against a different category. Obtain IDs from the list-tracking-categories tool. This targets a second category (cross-tab), NOT a second option of the same category."), + trackingOptionID2: z.string().optional().describe("Optional second tracking option ID. Obtain IDs from the list-tracking-categories tool. Must be supplied together with trackingCategoryID2; it filters to that single option of the second category."), standardLayout: z.boolean().optional().describe("Optional flag to use standard layout"), paymentsOnly: z.boolean().optional().describe("Optional flag to include only accounts with payments"), }, async (args) => { - const response = await listXeroProfitAndLoss( - args?.fromDate, - args?.toDate, - args?.periods, - args?.timeframe, - args?.standardLayout, - args?.paymentsOnly, - ); + const response = await listXeroProfitAndLoss({ + fromDate: args?.fromDate, + toDate: args?.toDate, + periods: args?.periods, + timeframe: args?.timeframe, + trackingCategoryID: args?.trackingCategoryID, + trackingOptionID: args?.trackingOptionID, + trackingCategoryID2: args?.trackingCategoryID2, + trackingOptionID2: args?.trackingOptionID2, + standardLayout: args?.standardLayout, + paymentsOnly: args?.paymentsOnly, + }); if (response.error !== null) { return { diff --git a/src/types/list-profit-and-loss-params.ts b/src/types/list-profit-and-loss-params.ts new file mode 100644 index 00000000..3047b223 --- /dev/null +++ b/src/types/list-profit-and-loss-params.ts @@ -0,0 +1,15 @@ +import { timeframeType } from "./timeframeType.js"; + +// Define an interface for the profit and loss parameters +export interface ListProfitAndLossParams { + fromDate?: string; + toDate?: string; + periods?: number; + timeframe?: timeframeType; + trackingCategoryID?: string; + trackingOptionID?: string; + trackingCategoryID2?: string; + trackingOptionID2?: string; + standardLayout?: boolean; + paymentsOnly?: boolean; +}