Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/handlers/create-xero-manual-journal.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function createManualJournal(
accountCode: journalLine.accountCode,
description: journalLine.description,
taxType: journalLine.taxType,
// TODO: tracking can be added here
tracking: journalLine.tracking,
})),
date: date,
lineAmountTypes: lineAmountTypes,
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/update-xero-manual-journal.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async function updateManualJournal(
accountCode: journalLine.accountCode,
description: journalLine.description, // Optional: description for the manual journal line
taxType: journalLine.taxType, // Optional: tax type for the manual journal line
// TODO: tracking can be added here
tracking: journalLine.tracking, // Optional: up to 2 tracking categories per line
})),
date: date, // Optional: YYYY-MM-DD format
lineAmountTypes: lineAmountTypes, // Optional: ManualJournal.LineAmountTypesEnum.EXCLUSIVE
Expand Down
49 changes: 49 additions & 0 deletions src/helpers/__tests__/format-tracking.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, it, expect } from "vitest";
import { TrackingCategory } from "xero-node";
import { formatTracking } from "../format-tracking.js";

describe("formatTracking", () => {
it("returns null when tracking is undefined", () => {
expect(formatTracking(undefined)).toBeNull();
});

it("returns null when tracking is an empty array", () => {
expect(formatTracking([])).toBeNull();
});

it("formats a single category/option pair", () => {
const tracking: TrackingCategory[] = [{ name: "Property", option: "CG" }];
expect(formatTracking(tracking)).toBe("Property/CG");
});

it("formats multiple category/option pairs comma-separated", () => {
const tracking: TrackingCategory[] = [
{ name: "Property", option: "CG" },
{ name: "Region", option: "East" },
];
expect(formatTracking(tracking)).toBe("Property/CG, Region/East");
});

it("renders readable text rather than [object Object]", () => {
const tracking: TrackingCategory[] = [{ name: "Property", option: "ES" }];
expect(formatTracking(tracking)).not.toContain("[object Object]");
});

it("falls back to the category name when option is missing", () => {
const tracking: TrackingCategory[] = [{ name: "Property" }];
expect(formatTracking(tracking)).toBe("Property");
});

it("skips entries that have neither name nor option", () => {
const tracking: TrackingCategory[] = [
{ name: "Property", option: "CG" },
{},
];
expect(formatTracking(tracking)).toBe("Property/CG");
});

it("returns null when every entry is empty", () => {
const tracking: TrackingCategory[] = [{}, {}];
expect(formatTracking(tracking)).toBeNull();
});
});
28 changes: 28 additions & 0 deletions src/helpers/format-tracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TrackingCategory } from "xero-node";

/**
* Format a line's tracking categories as readable `category/option` text.
*
* Manual journal lines (and other line types) can carry up to two tracking
* categories. Printing the raw array yields `[object Object]`, so this renders
* each entry as `name/option` (or just `name` when the option is absent) and
* joins multiple entries with commas.
*
* @returns the formatted string, or `null` when there is nothing to show so
* callers can omit the line cleanly.
*/
export const formatTracking = (
tracking?: TrackingCategory[],
): string | null => {
if (!tracking || tracking.length === 0) {
return null;
}

const formatted = tracking
.map((category) =>
[category.name, category.option].filter(Boolean).join("/"),
)
.filter((entry) => entry.length > 0);

return formatted.length > 0 ? formatted.join(", ") : null;
};
26 changes: 26 additions & 0 deletions src/helpers/tracking-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { z } from "zod";

/**
* Shared Zod schema for a single tracking category/option pair on a line.
* Used by invoice and manual journal tools so the shape stays consistent.
* Both the Xero `LineItemTracking` and `TrackingCategory` types accept these
* three fields, so the same schema works on invoice lines and manual journal lines.
*/
export const trackingSchema = z.object({
name: z
.string()
.describe(
"The name of the tracking category. Can be obtained from the list-tracking-categories tool",
),
option: z
.string()
.describe(
"The name of the tracking option. Can be obtained from the list-tracking-categories tool",
),
trackingCategoryID: z
.string()
.describe(
"The ID of the tracking category. \
Can be obtained from the list-tracking-categories tool",
),
});
8 changes: 1 addition & 7 deletions src/tools/create/create-invoice.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,9 @@ import { z } from "zod";
import { createXeroInvoice } from "../../handlers/create-xero-invoice.handler.js";
import { DeepLinkType, getDeepLink } from "../../helpers/get-deeplink.js";
import { CreateXeroTool } from "../../helpers/create-xero-tool.js";
import { trackingSchema } from "../../helpers/tracking-schema.js";
import { Invoice } from "xero-node";

const trackingSchema = z.object({
name: z.string().describe("The name of the tracking category. Can be obtained from the list-tracking-categories tool"),
option: z.string().describe("The name of the tracking option. Can be obtained from the list-tracking-categories tool"),
trackingCategoryID: z.string().describe("The ID of the tracking category. \
Can be obtained from the list-tracking-categories tool"),
});

const lineItemSchema = z.object({
description: z.string().describe("The description of the line item"),
quantity: z.number().describe("The quantity of the line item"),
Expand Down
54 changes: 34 additions & 20 deletions src/tools/create/create-manual-journal.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { CreateXeroTool } from "../../helpers/create-xero-tool.js";
import { createXeroManualJournal } from "../../handlers/create-xero-manual-journal.handler.js";
import { DeepLinkType, getDeepLink } from "../../helpers/get-deeplink.js";
import { ensureError } from "../../helpers/ensure-error.js";
import { trackingSchema } from "../../helpers/tracking-schema.js";
import { formatTracking } from "../../helpers/format-tracking.js";
import { LineAmountTypes, ManualJournal } from "xero-node";

const CreateManualJournalTool = CreateXeroTool(
Expand Down Expand Up @@ -33,7 +35,15 @@ const CreateManualJournalTool = CreateXeroTool(
.string()
.optional()
.describe("Optional tax type for the manual journal line"),
// TODO: TODO: tracking can be added here
tracking: z
.array(trackingSchema)
.max(2)
.optional()
.describe(
"Up to 2 tracking categories and options can be added to the journal line. \
Can be obtained from the list-tracking-categories tool. \
Only use if prompted by the user.",
),
}),
)
.describe(
Expand Down Expand Up @@ -105,25 +115,29 @@ const CreateManualJournalTool = CreateXeroTool(
? `Status: ${manualJournal.status}`
: "No status",
manualJournal.journalLines
? manualJournal.journalLines.map((line) => ({
type: "text" as const,
text: [
`Line Amount: ${line.lineAmount}`,
line.accountCode
? `Account Code: ${line.accountCode}`
: "No account code",
line.description
? `Description: ${line.description}`
: "No description",
line.taxType
? `Tax Type: ${line.taxType}`
: "No tax type",
`Tax Amount: ${line.taxAmount}`,
]
.filter(Boolean)
.join("\n"),
}))
: [{ type: "text" as const, text: "No journal lines" }],
? manualJournal.journalLines
.map((line) =>
[
`Line Amount: ${line.lineAmount}`,
line.accountCode
? `Account Code: ${line.accountCode}`
: "No account code",
line.description
? `Description: ${line.description}`
: "No description",
line.taxType
? `Tax Type: ${line.taxType}`
: "No tax type",
`Tax Amount: ${line.taxAmount}`,
formatTracking(line.tracking)
? `Tracking: ${formatTracking(line.tracking)}`
: null,
]
.filter(Boolean)
.join("\n"),
)
.join("\n\n")
: "No journal lines",
`Show on Cash Basis Reports: ${manualJournal.showOnCashBasisReports}`,
deepLink ? `Link to view: ${deepLink}` : null,
]
Expand Down
4 changes: 4 additions & 0 deletions src/tools/list/list-manual-journals.tool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ManualJournal } from "xero-node";
import { listXeroManualJournals } from "../../handlers/list-xero-manual-journals.handler.js";
import { CreateXeroTool } from "../../helpers/create-xero-tool.js";
import { formatTracking } from "../../helpers/format-tracking.js";
import { z } from "zod";

const ListManualJournalsTool = CreateXeroTool(
Expand Down Expand Up @@ -71,6 +72,9 @@ If they want the next page, call this tool again with the next page number, modi
: "No description",
line.taxType ? `Tax Type: ${line.taxType}` : "No tax type",
`Tax Amount: ${line.taxAmount}`,
formatTracking(line.tracking)
? `Tracking: ${formatTracking(line.tracking)}`
: null,
]
.filter(Boolean)
.join("\n")
Expand Down
8 changes: 1 addition & 7 deletions src/tools/update/update-invoice.tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,9 @@ import { z } from "zod";
import { updateXeroInvoice } from "../../handlers/update-xero-invoice.handler.js";
import { DeepLinkType, getDeepLink } from "../../helpers/get-deeplink.js";
import { CreateXeroTool } from "../../helpers/create-xero-tool.js";
import { trackingSchema } from "../../helpers/tracking-schema.js";
import { Invoice } from "xero-node";

const trackingSchema = z.object({
name: z.string().describe("The name of the tracking category. Can be obtained from the list-tracking-categories tool"),
option: z.string().describe("The name of the tracking option. Can be obtained from the list-tracking-categories tool"),
trackingCategoryID: z.string().describe("The ID of the tracking category. \
Can be obtained from the list-tracking-categories tool"),
});

const lineItemSchema = z.object({
description: z.string().describe("The description of the line item"),
quantity: z.number().describe("The quantity of the line item"),
Expand Down
13 changes: 11 additions & 2 deletions src/tools/update/update-manual-journal-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { z } from "zod";
import { CreateXeroTool } from "../../helpers/create-xero-tool.js";
import { DeepLinkType, getDeepLink } from "../../helpers/get-deeplink.js";
import { ensureError } from "../../helpers/ensure-error.js";
import { trackingSchema } from "../../helpers/tracking-schema.js";
import { LineAmountTypes, ManualJournal } from "xero-node";
import { updateXeroManualJournal } from "../../handlers/update-xero-manual-journal.handler.js";

const UpdateManualJournalTool = CreateXeroTool(
"update-manual-journal",
"Update a manual journal in Xero. Only works on draft manual journals.\
"Update a manual journal in Xero. Works on both draft and posted manual journals.\
Do not modify line items or parameters that have not been specified by the user.",
{
narration: z
Expand All @@ -31,7 +32,15 @@ const UpdateManualJournalTool = CreateXeroTool(
.string()
.optional()
.describe("Optional tax type for the manual journal line"),
// TODO: TODO: tracking can be added here
tracking: z
.array(trackingSchema)
.max(2)
.optional()
.describe(
"Up to 2 tracking categories and options can be added to the journal line. \
Can be obtained from the list-tracking-categories tool. \
Only use if prompted by the user.",
),
}),
)
.describe(
Expand Down