From d9b8565e1828d7deece90673be3961fc554d3dfb Mon Sep 17 00:00:00 2001 From: Mia Miu Date: Mon, 15 Jun 2026 14:08:25 +1200 Subject: [PATCH] feat: fill xero invoice and contact tool gaps --- README.md | 1 + .../add-xero-invoice-note.handler.test.ts | 49 ++++++++++++ src/handlers/add-xero-invoice-note.handler.ts | 48 +++++++++++ .../create-xero-invoice.handler.test.ts | 71 ++++++++++++++++ src/handlers/create-xero-invoice.handler.ts | 10 ++- .../list-xero-invoices.handler.test.ts | 63 +++++++++++++++ src/handlers/list-xero-invoices.handler.ts | 6 +- .../list-xero-profit-and-loss.handler.test.ts | 70 ++++++++++++++++ .../list-xero-profit-and-loss.handler.ts | 13 ++- .../update-xero-contact.handler.test.ts | 80 +++++++++++++++++++ src/handlers/update-xero-contact.handler.ts | 30 ++++++- src/tools/create/create-invoice.tool.ts | 16 +++- src/tools/list/list-invoices.tool.ts | 15 +++- src/tools/list/list-profit-and-loss.tool.ts | 6 +- src/tools/update/add-invoice-note.tool.ts | 47 +++++++++++ src/tools/update/index.ts | 2 + src/tools/update/update-contact.tool.ts | 59 ++++++++++++++ 17 files changed, 573 insertions(+), 13 deletions(-) create mode 100644 src/handlers/add-xero-invoice-note.handler.test.ts create mode 100644 src/handlers/add-xero-invoice-note.handler.ts create mode 100644 src/handlers/create-xero-invoice.handler.test.ts create mode 100644 src/handlers/list-xero-invoices.handler.test.ts create mode 100644 src/handlers/list-xero-profit-and-loss.handler.test.ts create mode 100644 src/handlers/update-xero-contact.handler.test.ts create mode 100644 src/tools/update/add-invoice-note.tool.ts diff --git a/README.md b/README.md index 40044369..6536b3ca 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ payroll.timesheets - `create-tracking-category`: Create a new tracking category - `create-tracking-option`: Create a new tracking option - `update-bank-transaction`: Update an existing bank transaction +- `add-invoice-note`: Add a history note to an invoice - `update-contact`: Update an existing contact - `update-invoice`: Update an existing draft invoice - `update-item`: Update an existing item diff --git a/src/handlers/add-xero-invoice-note.handler.test.ts b/src/handlers/add-xero-invoice-note.handler.test.ts new file mode 100644 index 00000000..99d603b7 --- /dev/null +++ b/src/handlers/add-xero-invoice-note.handler.test.ts @@ -0,0 +1,49 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const authenticateMock = vi.fn(); +const createInvoiceHistoryMock = vi.fn(); +const getClientHeadersMock = vi.fn(() => ({ "x-test": "header" })); + +vi.mock("../clients/xero-client.js", () => ({ + xeroClient: { + tenantId: "tenant-123", + authenticate: authenticateMock, + accountingApi: { + createInvoiceHistory: createInvoiceHistoryMock, + }, + }, +})); + +vi.mock("../helpers/get-client-headers.js", () => ({ + getClientHeaders: getClientHeadersMock, +})); + +describe("addXeroInvoiceNote", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("creates an invoice history record with the provided note", async () => { + createInvoiceHistoryMock.mockResolvedValue({ + body: { + historyRecords: [{ details: "Audit note" }], + }, + }); + + const { addXeroInvoiceNote } = await import("./add-xero-invoice-note.handler.js"); + + const result = await addXeroInvoiceNote("invoice-1", "Audit note"); + + expect(result.isError).toBe(false); + expect(result.result).toBe("Audit note"); + expect(createInvoiceHistoryMock).toHaveBeenCalledWith( + "tenant-123", + "invoice-1", + { + historyRecords: [{ details: "Audit note" }], + }, + undefined, + { "x-test": "header" }, + ); + }); +}); diff --git a/src/handlers/add-xero-invoice-note.handler.ts b/src/handlers/add-xero-invoice-note.handler.ts new file mode 100644 index 00000000..d2845144 --- /dev/null +++ b/src/handlers/add-xero-invoice-note.handler.ts @@ -0,0 +1,48 @@ +import { xeroClient } from "../clients/xero-client.js"; +import { formatError } from "../helpers/format-error.js"; +import { getClientHeaders } from "../helpers/get-client-headers.js"; +import { XeroClientResponse } from "../types/tool-response.js"; + +async function addInvoiceNote( + invoiceId: string, + note: string, +): Promise { + await xeroClient.authenticate(); + + const response = await xeroClient.accountingApi.createInvoiceHistory( + xeroClient.tenantId, + invoiceId, + { + historyRecords: [{ details: note }], + }, + undefined, + getClientHeaders(), + ); + + return response.body.historyRecords?.[0]?.details; +} + +export async function addXeroInvoiceNote( + invoiceId: string, + note: string, +): Promise> { + try { + const createdNote = await addInvoiceNote(invoiceId, note); + + if (!createdNote) { + throw new Error("Invoice note creation failed."); + } + + return { + result: createdNote, + isError: false, + error: null, + }; + } catch (error) { + return { + result: null, + isError: true, + error: formatError(error), + }; + } +} diff --git a/src/handlers/create-xero-invoice.handler.test.ts b/src/handlers/create-xero-invoice.handler.test.ts new file mode 100644 index 00000000..04ab5cc4 --- /dev/null +++ b/src/handlers/create-xero-invoice.handler.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { CurrencyCode, Invoice } from "xero-node"; + +const authenticateMock = vi.fn(); +const createInvoicesMock = vi.fn(); +const getClientHeadersMock = vi.fn(() => ({ "x-test": "header" })); + +vi.mock("../clients/xero-client.js", () => ({ + xeroClient: { + tenantId: "tenant-123", + authenticate: authenticateMock, + accountingApi: { + createInvoices: createInvoicesMock, + }, + }, +})); + +vi.mock("../helpers/get-client-headers.js", () => ({ + getClientHeaders: getClientHeadersMock, +})); + +describe("createXeroInvoice", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("passes currencyCode and currencyRate through to the created invoice", async () => { + createInvoicesMock.mockResolvedValue({ + body: { + invoices: [{ invoiceID: "inv-1", currencyCode: "EUR", currencyRate: 1.67 }], + }, + }); + + const { createXeroInvoice } = await import("./create-xero-invoice.handler.js"); + + const result = await createXeroInvoice( + "contact-1", + [ + { + description: "Consulting", + quantity: 1, + unitAmount: 120, + accountCode: "200", + taxType: "NONE", + }, + ], + Invoice.TypeEnum.ACCPAY, + "BILL-001", + "2026-06-15", + "EUR" as unknown as CurrencyCode, + 1.67, + ); + + expect(result.isError).toBe(false); + expect(createInvoicesMock).toHaveBeenCalledWith( + "tenant-123", + { + invoices: [ + expect.objectContaining({ + currencyCode: "EUR", + currencyRate: 1.67, + }), + ], + }, + true, + undefined, + undefined, + { "x-test": "header" }, + ); + }); +}); diff --git a/src/handlers/create-xero-invoice.handler.ts b/src/handlers/create-xero-invoice.handler.ts index 0ffb1be0..6ecd4df0 100644 --- a/src/handlers/create-xero-invoice.handler.ts +++ b/src/handlers/create-xero-invoice.handler.ts @@ -1,7 +1,7 @@ import { xeroClient } from "../clients/xero-client.js"; import { XeroClientResponse } from "../types/tool-response.js"; import { formatError } from "../helpers/format-error.js"; -import { Invoice, LineItemTracking } from "xero-node"; +import { CurrencyCode, Invoice, LineItemTracking } from "xero-node"; import { getClientHeaders } from "../helpers/get-client-headers.js"; interface InvoiceLineItem { @@ -20,6 +20,8 @@ async function createInvoice( type: Invoice.TypeEnum, reference: string | undefined, date: string | undefined, + currencyCode: CurrencyCode | undefined, + currencyRate: number | undefined, ): Promise { await xeroClient.authenticate(); @@ -37,6 +39,8 @@ async function createInvoice( ? { invoiceNumber: reference } : { reference: reference }), status: Invoice.StatusEnum.DRAFT, + currencyCode, + currencyRate, }; const response = await xeroClient.accountingApi.createInvoices( @@ -62,6 +66,8 @@ export async function createXeroInvoice( type: Invoice.TypeEnum = Invoice.TypeEnum.ACCREC, reference?: string, date?: string, + currencyCode?: CurrencyCode, + currencyRate?: number, ): Promise> { try { const createdInvoice = await createInvoice( @@ -70,6 +76,8 @@ export async function createXeroInvoice( type, reference, date, + currencyCode, + currencyRate, ); if (!createdInvoice) { diff --git a/src/handlers/list-xero-invoices.handler.test.ts b/src/handlers/list-xero-invoices.handler.test.ts new file mode 100644 index 00000000..e6bb825d --- /dev/null +++ b/src/handlers/list-xero-invoices.handler.test.ts @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const authenticateMock = vi.fn(); +const getInvoicesMock = vi.fn(); +const getClientHeadersMock = vi.fn(() => ({ "x-test": "header" })); + +vi.mock("../clients/xero-client.js", () => ({ + xeroClient: { + tenantId: "tenant-123", + authenticate: authenticateMock, + accountingApi: { + getInvoices: getInvoicesMock, + }, + }, +})); + +vi.mock("../helpers/get-client-headers.js", () => ({ + getClientHeaders: getClientHeadersMock, +})); + +describe("listXeroInvoices", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("forwards statuses to the Xero invoices endpoint", async () => { + const invoices = [{ invoiceID: "inv-1", status: "AUTHORISED" }]; + + getInvoicesMock.mockResolvedValue({ + body: { invoices }, + }); + + const { listXeroInvoices } = await import("./list-xero-invoices.handler.js"); + + const result = await listXeroInvoices( + 2, + ["contact-1"], + ["INV-001"], + ["AUTHORISED", "PAID"], + ); + + expect(result.isError).toBe(false); + expect(result.result).toBe(invoices); + expect(getInvoicesMock).toHaveBeenCalledWith( + "tenant-123", + undefined, + undefined, + "UpdatedDateUTC DESC", + undefined, + ["INV-001"], + ["contact-1"], + ["AUTHORISED", "PAID"], + 2, + false, + false, + undefined, + false, + 10, + undefined, + { "x-test": "header" }, + ); + }); +}); diff --git a/src/handlers/list-xero-invoices.handler.ts b/src/handlers/list-xero-invoices.handler.ts index 1eb3f26f..ba9191b1 100644 --- a/src/handlers/list-xero-invoices.handler.ts +++ b/src/handlers/list-xero-invoices.handler.ts @@ -7,6 +7,7 @@ import { getClientHeaders } from "../helpers/get-client-headers.js"; async function getInvoices( invoiceNumbers: string[] | undefined, contactIds: string[] | undefined, + statuses: string[] | undefined, page: number, ): Promise { await xeroClient.authenticate(); @@ -19,7 +20,7 @@ async function getInvoices( undefined, // iDs invoiceNumbers, // invoiceNumbers contactIds, // contactIDs - undefined, // statuses + statuses, // statuses page, false, // includeArchived false, // createdByMyApp @@ -39,9 +40,10 @@ export async function listXeroInvoices( page: number = 1, contactIds?: string[], invoiceNumbers?: string[], + statuses?: string[], ): Promise> { try { - const invoices = await getInvoices(invoiceNumbers, contactIds, page); + const invoices = await getInvoices(invoiceNumbers, contactIds, statuses, page); return { result: invoices, diff --git a/src/handlers/list-xero-profit-and-loss.handler.test.ts b/src/handlers/list-xero-profit-and-loss.handler.test.ts new file mode 100644 index 00000000..4dde3764 --- /dev/null +++ b/src/handlers/list-xero-profit-and-loss.handler.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const authenticateMock = vi.fn(); +const getReportProfitAndLossMock = vi.fn(); +const getClientHeadersMock = vi.fn(() => ({ "x-test": "header" })); + +vi.mock("../clients/xero-client.js", () => ({ + xeroClient: { + tenantId: "tenant-123", + authenticate: authenticateMock, + accountingApi: { + getReportProfitAndLoss: getReportProfitAndLossMock, + }, + }, +})); + +vi.mock("../helpers/get-client-headers.js", () => ({ + getClientHeaders: getClientHeadersMock, +})); + +describe("listXeroProfitAndLoss", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("forwards tracking options, standardLayout, and paymentsOnly to the SDK in the correct positions", async () => { + const report = { + reportName: "Profit and Loss", + reportDate: "2026-06-12", + rows: [], + }; + + getReportProfitAndLossMock.mockResolvedValue({ + body: { reports: [report] }, + }); + + const { listXeroProfitAndLoss } = await import( + "./list-xero-profit-and-loss.handler.js" + ); + + const result = await listXeroProfitAndLoss( + "2026-01-01", + "2026-01-31", + 1, + "MONTH", + "track-opt-1", + "track-opt-2", + true, + true, + ); + + expect(result.isError).toBe(false); + expect(result.result).toBe(report); + expect(authenticateMock).toHaveBeenCalledTimes(1); + expect(getReportProfitAndLossMock).toHaveBeenCalledWith( + "tenant-123", + "2026-01-01", + "2026-01-31", + 1, + "MONTH", + undefined, + undefined, + "track-opt-1", + "track-opt-2", + true, + true, + { "x-test": "header" }, + ); + }); +}); diff --git a/src/handlers/list-xero-profit-and-loss.handler.ts b/src/handlers/list-xero-profit-and-loss.handler.ts index e19a7bfc..6dfddbb8 100644 --- a/src/handlers/list-xero-profit-and-loss.handler.ts +++ b/src/handlers/list-xero-profit-and-loss.handler.ts @@ -15,6 +15,8 @@ async function fetchProfitAndLoss( toDate?: string, periods?: number, timeframe?: TimeframeType, + trackingOptionID1?: string, + trackingOptionID2?: string, standardLayout?: boolean, paymentsOnly?: boolean, ): Promise { @@ -27,9 +29,9 @@ async function fetchProfitAndLoss( periods, timeframe, undefined, // trackingCategoryID - undefined, // trackingOptionID undefined, // trackingCategoryID2 - undefined, // trackingOptionID2 + trackingOptionID1, + trackingOptionID2, standardLayout, paymentsOnly, getClientHeaders(), @@ -56,6 +58,8 @@ export async function listXeroProfitAndLoss( toDate?: string, periods?: number, timeframe?: TimeframeType, + trackingOptionID1?: string, + trackingOptionID2?: string, standardLayout?: boolean, paymentsOnly?: boolean, ): Promise> { @@ -65,6 +69,9 @@ export async function listXeroProfitAndLoss( toDate, periods, timeframe, + trackingOptionID1, + trackingOptionID2, + standardLayout, paymentsOnly, ); @@ -88,4 +95,4 @@ export async function listXeroProfitAndLoss( error: formatError(error), }; } -} \ No newline at end of file +} diff --git a/src/handlers/update-xero-contact.handler.test.ts b/src/handlers/update-xero-contact.handler.test.ts new file mode 100644 index 00000000..6c3d8293 --- /dev/null +++ b/src/handlers/update-xero-contact.handler.test.ts @@ -0,0 +1,80 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { Contact } from "xero-node"; + +const authenticateMock = vi.fn(); +const updateContactMock = vi.fn(); +const getClientHeadersMock = vi.fn(() => ({ "x-test": "header" })); + +vi.mock("../clients/xero-client.js", () => ({ + xeroClient: { + tenantId: "tenant-123", + authenticate: authenticateMock, + accountingApi: { + updateContact: updateContactMock, + }, + }, +})); + +vi.mock("../helpers/get-client-headers.js", () => ({ + getClientHeaders: getClientHeadersMock, +})); + +describe("updateXeroContact", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("passes default account codes, line amount types, and tracking categories through to Xero", async () => { + updateContactMock.mockResolvedValue({ + body: { contacts: [{ contactID: "contact-1", name: "ACME" }] }, + }); + + const { updateXeroContact } = await import("./update-xero-contact.handler.js"); + + const result = await updateXeroContact( + "contact-1", + "ACME", + undefined, + undefined, + undefined, + undefined, + undefined, + "400", + "200", + Contact.PurchasesDefaultLineAmountTypeEnum.EXCLUSIVE, + Contact.SalesDefaultLineAmountTypeEnum.INCLUSIVE, + [{ trackingCategoryName: "Department", trackingOptionName: "Finance" }], + [{ trackingCategoryName: "Region", trackingOptionName: "EMEA" }], + ); + + expect(result.isError).toBe(false); + expect(updateContactMock).toHaveBeenCalledWith( + "tenant-123", + "contact-1", + { + contacts: [ + expect.objectContaining({ + purchasesDefaultAccountCode: "400", + salesDefaultAccountCode: "200", + purchasesDefaultLineAmountType: "EXCLUSIVE", + salesDefaultLineAmountType: "INCLUSIVE", + purchasesTrackingCategories: [ + { + trackingCategoryName: "Department", + trackingOptionName: "Finance", + }, + ], + salesTrackingCategories: [ + { + trackingCategoryName: "Region", + trackingOptionName: "EMEA", + }, + ], + }), + ], + }, + undefined, + { "x-test": "header" }, + ); + }); +}); diff --git a/src/handlers/update-xero-contact.handler.ts b/src/handlers/update-xero-contact.handler.ts index ebd3f6ab..202c818e 100644 --- a/src/handlers/update-xero-contact.handler.ts +++ b/src/handlers/update-xero-contact.handler.ts @@ -1,7 +1,7 @@ import { xeroClient } from "../clients/xero-client.js"; import { XeroClientResponse } from "../types/tool-response.js"; import { formatError } from "../helpers/format-error.js"; -import { Contact, Phone, Address, Contacts } from "xero-node"; +import { Contact, Phone, Address, Contacts, SalesTrackingCategory } from "xero-node"; import { getClientHeaders } from "../helpers/get-client-headers.js"; async function updateContact( @@ -11,6 +11,16 @@ async function updateContact( email: string | undefined, phone: string | undefined, address: Address | undefined, + purchasesDefaultAccountCode: string | undefined, + salesDefaultAccountCode: string | undefined, + purchasesDefaultLineAmountType: + | Contact.PurchasesDefaultLineAmountTypeEnum + | undefined, + salesDefaultLineAmountType: + | Contact.SalesDefaultLineAmountTypeEnum + | undefined, + purchasesTrackingCategories: SalesTrackingCategory[] | undefined, + salesTrackingCategories: SalesTrackingCategory[] | undefined, contactId: string, ): Promise { await xeroClient.authenticate(); @@ -41,6 +51,12 @@ async function updateContact( }, ] : undefined, + purchasesDefaultAccountCode, + salesDefaultAccountCode, + purchasesDefaultLineAmountType, + salesDefaultLineAmountType, + purchasesTrackingCategories, + salesTrackingCategories, }; const contacts: Contacts = { @@ -70,6 +86,12 @@ export async function updateXeroContact( email?: string, phone?: string, address?: Address, + purchasesDefaultAccountCode?: string, + salesDefaultAccountCode?: string, + purchasesDefaultLineAmountType?: Contact.PurchasesDefaultLineAmountTypeEnum, + salesDefaultLineAmountType?: Contact.SalesDefaultLineAmountTypeEnum, + purchasesTrackingCategories?: SalesTrackingCategory[], + salesTrackingCategories?: SalesTrackingCategory[], ): Promise> { try { const updatedContact = await updateContact( @@ -79,6 +101,12 @@ export async function updateXeroContact( email, phone, address, + purchasesDefaultAccountCode, + salesDefaultAccountCode, + purchasesDefaultLineAmountType, + salesDefaultLineAmountType, + purchasesTrackingCategories, + salesTrackingCategories, contactId, ); diff --git a/src/tools/create/create-invoice.tool.ts b/src/tools/create/create-invoice.tool.ts index debe407f..d412ac2c 100644 --- a/src/tools/create/create-invoice.tool.ts +++ b/src/tools/create/create-invoice.tool.ts @@ -2,7 +2,7 @@ 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 { Invoice } from "xero-node"; +import { CurrencyCode, 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"), @@ -41,10 +41,20 @@ const CreateInvoiceTool = CreateXeroTool( If the type is not specified, the default is ACCREC."), reference: z.string().describe("A reference number for the invoice.").optional(), date: z.string().describe("The date the invoice was created (YYYY-MM-DD format).").optional(), + currencyCode: z.string().describe("Optional ISO 4217 currency code, for example EUR, USD, or GBP").optional(), + currencyRate: z.number().describe("Optional exchange rate to apply to the invoice").optional(), }, - async ({ contactId, lineItems, type, reference, date }) => { + async ({ contactId, lineItems, type, reference, date, currencyCode, currencyRate }) => { const xeroInvoiceType = type === "ACCREC" ? Invoice.TypeEnum.ACCREC : Invoice.TypeEnum.ACCPAY; - const result = await createXeroInvoice(contactId, lineItems, xeroInvoiceType, reference, date); + const result = await createXeroInvoice( + contactId, + lineItems, + xeroInvoiceType, + reference, + date, + currencyCode as CurrencyCode | undefined, + currencyRate, + ); if (result.isError) { return { content: [ diff --git a/src/tools/list/list-invoices.tool.ts b/src/tools/list/list-invoices.tool.ts index ac400fc9..c7dd58b3 100644 --- a/src/tools/list/list-invoices.tool.ts +++ b/src/tools/list/list-invoices.tool.ts @@ -15,13 +15,24 @@ const ListInvoicesTool = CreateXeroTool( { page: z.number(), contactIds: z.array(z.string()).optional(), + status: z.string().optional().describe("Optional single invoice status filter, for example DRAFT, SUBMITTED, AUTHORISED, or PAID"), + statuses: z + .array(z.string()) + .optional() + .describe("Optional list of invoice status filters, for example [DRAFT, AUTHORISED]"), invoiceNumbers: z .array(z.string()) .optional() .describe("If provided, invoice line items will also be returned"), }, - async ({ page, contactIds, invoiceNumbers }) => { - const response = await listXeroInvoices(page, contactIds, invoiceNumbers); + async ({ page, contactIds, status, statuses, invoiceNumbers }) => { + const statusFilters = statuses ?? (status ? [status] : undefined); + const response = await listXeroInvoices( + page, + contactIds, + invoiceNumbers, + statusFilters, + ); if (response.error !== null) { return { content: [ diff --git a/src/tools/list/list-profit-and-loss.tool.ts b/src/tools/list/list-profit-and-loss.tool.ts index 72e6aba1..ef85d911 100644 --- a/src/tools/list/list-profit-and-loss.tool.ts +++ b/src/tools/list/list-profit-and-loss.tool.ts @@ -10,6 +10,8 @@ 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)"), + trackingOptionID1: z.string().optional().describe("Optional tracking option ID 1"), + trackingOptionID2: z.string().optional().describe("Optional tracking option ID 2"), standardLayout: z.boolean().optional().describe("Optional flag to use standard layout"), paymentsOnly: z.boolean().optional().describe("Optional flag to include only accounts with payments"), }, @@ -19,6 +21,8 @@ const ListProfitAndLossTool = CreateXeroTool( args?.toDate, args?.periods, args?.timeframe, + args?.trackingOptionID1, + args?.trackingOptionID2, args?.standardLayout, args?.paymentsOnly, ); @@ -59,4 +63,4 @@ const ListProfitAndLossTool = CreateXeroTool( }, ); -export default ListProfitAndLossTool; \ No newline at end of file +export default ListProfitAndLossTool; diff --git a/src/tools/update/add-invoice-note.tool.ts b/src/tools/update/add-invoice-note.tool.ts new file mode 100644 index 00000000..7e43054c --- /dev/null +++ b/src/tools/update/add-invoice-note.tool.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; +import { addXeroInvoiceNote } from "../../handlers/add-xero-invoice-note.handler.js"; +import { CreateXeroTool } from "../../helpers/create-xero-tool.js"; +import { DeepLinkType, getDeepLink } from "../../helpers/get-deeplink.js"; + +const AddInvoiceNoteTool = CreateXeroTool( + "add-invoice-note", + "Add a history note to an invoice in Xero. Use this to annotate an invoice with audit or bookkeeping context.", + { + invoiceId: z.string().describe("The ID of the invoice to annotate."), + note: z.string().max(4000).describe("The history note to add to the invoice."), + }, + async ({ invoiceId, note }) => { + const response = await addXeroInvoiceNote(invoiceId, note); + + if (response.isError) { + return { + content: [ + { + type: "text" as const, + text: `Error adding invoice note: ${response.error}`, + }, + ], + }; + } + + const deepLink = await getDeepLink(DeepLinkType.INVOICE, invoiceId); + + return { + content: [ + { + type: "text" as const, + text: [ + "Invoice note added successfully:", + `Invoice ID: ${invoiceId}`, + `Note: ${response.result}`, + deepLink ? `Link to view: ${deepLink}` : null, + ] + .filter(Boolean) + .join("\n"), + }, + ], + }; + }, +); + +export default AddInvoiceNoteTool; diff --git a/src/tools/update/index.ts b/src/tools/update/index.ts index d198d69c..f5717757 100644 --- a/src/tools/update/index.ts +++ b/src/tools/update/index.ts @@ -1,4 +1,5 @@ import ApprovePayrollTimesheetTool from "./approve-payroll-timesheet.tool.js"; +import AddInvoiceNoteTool from "./add-invoice-note.tool.js"; import RevertPayrollTimesheetTool from "./revert-payroll-timesheet.tool.js"; import UpdateBankTransactionTool from "./update-bank-transaction.tool.js"; import UpdateContactTool from "./update-contact.tool.js"; @@ -14,6 +15,7 @@ import UpdateTrackingCategoryTool from "./update-tracking-category.tool.js"; import UpdateTrackingOptionsTool from "./update-tracking-options.tool.js"; export const UpdateTools = [ + AddInvoiceNoteTool, UpdateContactTool, UpdateCreditNoteTool, UpdateInvoiceTool, diff --git a/src/tools/update/update-contact.tool.ts b/src/tools/update/update-contact.tool.ts index 242af186..c6289813 100644 --- a/src/tools/update/update-contact.tool.ts +++ b/src/tools/update/update-contact.tool.ts @@ -3,6 +3,12 @@ import { z } from "zod"; import { DeepLinkType, getDeepLink } from "../../helpers/get-deeplink.js"; import { ensureError } from "../../helpers/ensure-error.js"; import { CreateXeroTool } from "../../helpers/create-xero-tool.js"; +import { Contact, SalesTrackingCategory } from "xero-node"; + +const trackingCategorySchema = z.object({ + trackingCategoryName: z.string(), + trackingOptionName: z.string(), +}); const UpdateContactTool = CreateXeroTool( "update-contact", @@ -17,6 +23,18 @@ const UpdateContactTool = CreateXeroTool( lastName: z.string().optional(), email: z.string().email().optional(), phone: z.string().optional(), + purchasesDefaultAccountCode: z.string().optional(), + salesDefaultAccountCode: z.string().optional(), + purchasesDefaultLineAmountType: z + .enum(["EXCLUSIVE", "INCLUSIVE", "NO_TAX"]) + .optional(), + salesDefaultLineAmountType: z + .enum(["EXCLUSIVE", "INCLUSIVE", "NO_TAX"]) + .optional(), + defaultPurchasesTrackingCategories: z + .array(trackingCategorySchema) + .optional(), + defaultSalesTrackingCategories: z.array(trackingCategorySchema).optional(), address: z .object({ addressLine1: z.string(), @@ -35,12 +53,30 @@ const UpdateContactTool = CreateXeroTool( lastName, email, phone, + purchasesDefaultAccountCode, + salesDefaultAccountCode, + purchasesDefaultLineAmountType, + salesDefaultLineAmountType, + defaultPurchasesTrackingCategories, + defaultSalesTrackingCategories, address, }: { contactId: string; name: string; email?: string; phone?: string; + purchasesDefaultAccountCode?: string; + salesDefaultAccountCode?: string; + purchasesDefaultLineAmountType?: "EXCLUSIVE" | "INCLUSIVE" | "NO_TAX"; + salesDefaultLineAmountType?: "EXCLUSIVE" | "INCLUSIVE" | "NO_TAX"; + defaultPurchasesTrackingCategories?: Array<{ + trackingCategoryName: string; + trackingOptionName: string; + }>; + defaultSalesTrackingCategories?: Array<{ + trackingCategoryName: string; + trackingOptionName: string; + }>; address?: { addressLine1: string; addressLine2?: string; @@ -53,6 +89,19 @@ const UpdateContactTool = CreateXeroTool( lastName?: string; }) => { try { + const mapTrackingCategories = ( + categories: + | Array<{ + trackingCategoryName: string; + trackingOptionName: string; + }> + | undefined, + ): SalesTrackingCategory[] | undefined => + categories?.map((category) => ({ + trackingCategoryName: category.trackingCategoryName, + trackingOptionName: category.trackingOptionName, + })); + const response = await updateXeroContact( contactId, name, @@ -61,6 +110,16 @@ const UpdateContactTool = CreateXeroTool( email, phone, address, + purchasesDefaultAccountCode, + salesDefaultAccountCode, + purchasesDefaultLineAmountType as + | Contact.PurchasesDefaultLineAmountTypeEnum + | undefined, + salesDefaultLineAmountType as + | Contact.SalesDefaultLineAmountTypeEnum + | undefined, + mapTrackingCategories(defaultPurchasesTrackingCategories), + mapTrackingCategories(defaultSalesTrackingCategories), ); if (response.isError) { return {