diff --git a/src/consts/deeplinks.ts b/src/consts/deeplinks.ts index e91122b9..70e5c81d 100644 --- a/src/consts/deeplinks.ts +++ b/src/consts/deeplinks.ts @@ -32,3 +32,7 @@ export const manualJournalDeepLink = (journalId: string) => { export const billDeepLink = (orgShortCode: string, billId: string) => { return `https://go.xero.com/organisationlogin/default.aspx?shortcode=${orgShortCode}&redirecturl=/AccountsPayable/Edit.aspx?InvoiceID=${billId}`; }; + +export const accountDeepLink = (orgShortCode: string, accountId: string) => { + return `https://go.xero.com/app/${orgShortCode}/accounts/settings/${accountId}`; +}; diff --git a/src/handlers/archive-xero-account.handler.ts b/src/handlers/archive-xero-account.handler.ts new file mode 100644 index 00000000..9c73865e --- /dev/null +++ b/src/handlers/archive-xero-account.handler.ts @@ -0,0 +1,53 @@ +import { xeroClient } from "../clients/xero-client.js"; +import { XeroClientResponse } from "../types/tool-response.js"; +import { formatError } from "../helpers/format-error.js"; +import { Account, Accounts } from "xero-node"; +import { getClientHeaders } from "../helpers/get-client-headers.js"; + +async function archiveAccount( + accountId: string, +): Promise { + await xeroClient.authenticate(); + + const account: Account = { + status: Account.StatusEnum.ARCHIVED, + }; + + const accounts: Accounts = { + accounts: [account], + }; + + const response = await xeroClient.accountingApi.updateAccount( + xeroClient.tenantId, + accountId, + accounts, + undefined, // idempotencyKey + getClientHeaders(), + ); + + return response.body.accounts?.[0]; +} + +export async function archiveXeroAccount( + accountId: string, +): Promise> { + try { + const archivedAccount = await archiveAccount(accountId); + + if (!archivedAccount) { + throw new Error("Account archival failed."); + } + + return { + result: archivedAccount, + isError: false, + error: null, + }; + } catch (error) { + return { + result: null, + isError: true, + error: formatError(error), + }; + } +} diff --git a/src/handlers/create-xero-account.handler.ts b/src/handlers/create-xero-account.handler.ts new file mode 100644 index 00000000..e16768c1 --- /dev/null +++ b/src/handlers/create-xero-account.handler.ts @@ -0,0 +1,70 @@ +import { xeroClient } from "../clients/xero-client.js"; +import { XeroClientResponse } from "../types/tool-response.js"; +import { formatError } from "../helpers/format-error.js"; +import { Account, AccountType } from "xero-node"; +import { getClientHeaders } from "../helpers/get-client-headers.js"; + +async function createAccount( + name: string, + code: string, + type: AccountType, + description?: string, + taxType?: string, + bankAccountNumber?: string, +): Promise { + await xeroClient.authenticate(); + + const account: Account = { + name, + code, + type, + description, + taxType, + bankAccountNumber, + }; + + const response = await xeroClient.accountingApi.createAccount( + xeroClient.tenantId, + account, + undefined, // idempotencyKey + getClientHeaders(), + ); + + return response.body.accounts?.[0]; +} + +export async function createXeroAccount( + name: string, + code: string, + type: AccountType, + description?: string, + taxType?: string, + bankAccountNumber?: string, +): Promise> { + try { + const createdAccount = await createAccount( + name, + code, + type, + description, + taxType, + bankAccountNumber, + ); + + if (!createdAccount) { + throw new Error("Account creation failed."); + } + + return { + result: createdAccount, + isError: false, + error: null, + }; + } catch (error) { + return { + result: null, + isError: true, + error: formatError(error), + }; + } +} diff --git a/src/handlers/update-xero-account.handler.ts b/src/handlers/update-xero-account.handler.ts new file mode 100644 index 00000000..9ecc1896 --- /dev/null +++ b/src/handlers/update-xero-account.handler.ts @@ -0,0 +1,98 @@ +import { xeroClient } from "../clients/xero-client.js"; +import { XeroClientResponse } from "../types/tool-response.js"; +import { formatError } from "../helpers/format-error.js"; +import { Account, Accounts, AccountType, CurrencyCode } from "xero-node"; +import { getClientHeaders } from "../helpers/get-client-headers.js"; + +async function updateAccount( + accountId: string, + name?: string, + code?: string, + type?: AccountType, + description?: string, + taxType?: string, + currencyCode?: string, + enablePaymentsToAccount?: boolean, + showInExpenseClaims?: boolean, + reportingCode?: string, + reportingCodeName?: string, + addToWatchlist?: boolean, +): Promise { + await xeroClient.authenticate(); + + const account: Account = { + name, + code, + type, + description, + taxType, + currencyCode: currencyCode as unknown as CurrencyCode, + enablePaymentsToAccount, + showInExpenseClaims, + reportingCode, + reportingCodeName, + addToWatchlist, + }; + + const accounts: Accounts = { + accounts: [account], + }; + + const response = await xeroClient.accountingApi.updateAccount( + xeroClient.tenantId, + accountId, + accounts, + undefined, // idempotencyKey + getClientHeaders(), + ); + + return response.body.accounts?.[0]; +} + +export async function updateXeroAccount( + accountId: string, + name?: string, + code?: string, + type?: AccountType, + description?: string, + taxType?: string, + currencyCode?: string, + enablePaymentsToAccount?: boolean, + showInExpenseClaims?: boolean, + reportingCode?: string, + reportingCodeName?: string, + addToWatchlist?: boolean, +): Promise> { + try { + const updatedAccount = await updateAccount( + accountId, + name, + code, + type, + description, + taxType, + currencyCode, + enablePaymentsToAccount, + showInExpenseClaims, + reportingCode, + reportingCodeName, + addToWatchlist, + ); + + if (!updatedAccount) { + throw new Error("Account update failed."); + } + + return { + result: updatedAccount, + isError: false, + error: null, + }; + } catch (error) { + return { + result: null, + isError: true, + error: formatError(error), + }; + } +} diff --git a/src/helpers/get-deeplink.ts b/src/helpers/get-deeplink.ts index 6a9c0833..88a65a1a 100644 --- a/src/helpers/get-deeplink.ts +++ b/src/helpers/get-deeplink.ts @@ -7,6 +7,7 @@ import { manualJournalDeepLink, quoteDeepLink, billDeepLink, + accountDeepLink, } from "../consts/deeplinks.js"; export enum DeepLinkType { @@ -17,6 +18,7 @@ export enum DeepLinkType { QUOTE, PAYMENT, BILL, + ACCOUNT, } /** @@ -48,5 +50,7 @@ export const getDeepLink = async (type: DeepLinkType, itemId: string) => { return paymentDeepLink(orgShortCode, itemId); case DeepLinkType.BILL: return billDeepLink(orgShortCode, itemId); + case DeepLinkType.ACCOUNT: + return accountDeepLink(orgShortCode, itemId); } }; diff --git a/src/tools/create/create-account.tool.ts b/src/tools/create/create-account.tool.ts new file mode 100644 index 00000000..842609ea --- /dev/null +++ b/src/tools/create/create-account.tool.ts @@ -0,0 +1,115 @@ +import { createXeroAccount } from "../../handlers/create-xero-account.handler.js"; +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 { AccountType } from "xero-node"; + +const CreateAccountTool = CreateXeroTool( + "create-account", + "Create an account in Xero's chart of accounts.\ + When an account is created, a deep link to the account in Xero is returned. \ + This deep link can be used to view the account in Xero directly. \ + This link should be displayed to the user.", + { + name: z.string().describe("Name of the account (max 150 chars)"), + code: z.string().describe("A unique alphanumeric account code (max 10 chars)"), + type: z + .enum([ + "BANK", + "CURRENT", + "CURRLIAB", + "DEPRECIATN", + "DIRECTCOSTS", + "EQUITY", + "EXPENSE", + "FIXED", + "INVENTORY", + "LIABILITY", + "NONCURRENT", + "OTHERINCOME", + "OVERHEADS", + "PREPAYMENT", + "REVENUE", + "SALES", + "TERMLIAB", + "PAYG", + ]) + .describe("The account type"), + description: z + .string() + .optional() + .describe( + "Description of the account (max 4000 chars, not valid for bank accounts)", + ), + taxType: z + .string() + .optional() + .describe("The tax type for the account"), + bankAccountNumber: z + .string() + .optional() + .describe("Bank account number (only for BANK type accounts)"), + }, + async ({ name, code, type, description, taxType, bankAccountNumber }) => { + try { + const response = await createXeroAccount( + name, + code, + type as unknown as AccountType, + description, + taxType, + bankAccountNumber, + ); + + if (response.isError) { + return { + content: [ + { + type: "text" as const, + text: `Error creating account: ${response.error}`, + }, + ], + }; + } + + const account = response.result; + + const deepLink = account.accountID + ? await getDeepLink(DeepLinkType.ACCOUNT, account.accountID) + : null; + + return { + content: [ + { + type: "text" as const, + text: [ + "Account created successfully:", + `Name: ${account.name}`, + `Code: ${account.code}`, + `ID: ${account.accountID}`, + `Type: ${account.type}`, + `Status: ${account.status}`, + deepLink ? `Link to view: ${deepLink}` : null, + ] + .filter(Boolean) + .join("\n"), + }, + ], + }; + } catch (error) { + const err = ensureError(error); + + return { + content: [ + { + type: "text" as const, + text: `Error creating account: ${err.message}`, + }, + ], + }; + } + }, +); + +export default CreateAccountTool; diff --git a/src/tools/create/index.ts b/src/tools/create/index.ts index 1062a911..02b5ad11 100644 --- a/src/tools/create/index.ts +++ b/src/tools/create/index.ts @@ -1,3 +1,4 @@ +import CreateAccountTool from "./create-account.tool.js"; import CreateBankTransactionTool from "./create-bank-transaction.tool.js"; import CreateContactTool from "./create-contact.tool.js"; import CreateCreditNoteTool from "./create-credit-note.tool.js"; @@ -11,6 +12,7 @@ import CreateTrackingCategoryTool from "./create-tracking-category.tool.js"; import CreateTrackingOptionsTool from "./create-tracking-options.tool.js"; export const CreateTools = [ + CreateAccountTool, CreateContactTool, CreateCreditNoteTool, CreateManualJournalTool, diff --git a/src/tools/update/archive-account.tool.ts b/src/tools/update/archive-account.tool.ts new file mode 100644 index 00000000..177fe8d1 --- /dev/null +++ b/src/tools/update/archive-account.tool.ts @@ -0,0 +1,71 @@ +import { archiveXeroAccount } from "../../handlers/archive-xero-account.handler.js"; +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"; + +const ArchiveAccountTool = CreateXeroTool( + "archive-account", + "Archive an account in Xero's chart of accounts. \ + This sets the account status to ARCHIVED. Only accounts with status ACTIVE can be archived. \ + When an account is archived, a deep link to the account in Xero is returned. \ + This deep link can be used to view the account in Xero directly. \ + This link should be displayed to the user.", + { + accountId: z.string().describe("The Xero ID of the account to archive"), + }, + async ({ accountId }) => { + try { + const response = await archiveXeroAccount(accountId); + + if (response.isError) { + return { + content: [ + { + type: "text" as const, + text: `Error archiving account: ${response.error}`, + }, + ], + }; + } + + const account = response.result; + + const deepLink = account.accountID + ? await getDeepLink(DeepLinkType.ACCOUNT, account.accountID) + : null; + + return { + content: [ + { + type: "text" as const, + text: [ + "Account archived successfully:", + `Name: ${account.name}`, + `Code: ${account.code}`, + `ID: ${account.accountID}`, + `Type: ${account.type}`, + `Status: ${account.status}`, + deepLink ? `Link to view: ${deepLink}` : null, + ] + .filter(Boolean) + .join("\n"), + }, + ], + }; + } catch (error) { + const err = ensureError(error); + + return { + content: [ + { + type: "text" as const, + text: `Error archiving account: ${err.message}`, + }, + ], + }; + } + }, +); + +export default ArchiveAccountTool; diff --git a/src/tools/update/index.ts b/src/tools/update/index.ts index d198d69c..8ef0ebc9 100644 --- a/src/tools/update/index.ts +++ b/src/tools/update/index.ts @@ -1,3 +1,5 @@ +import ArchiveAccountTool from "./archive-account.tool.js"; +import UpdateAccountTool from "./update-account.tool.js"; import ApprovePayrollTimesheetTool from "./approve-payroll-timesheet.tool.js"; import RevertPayrollTimesheetTool from "./revert-payroll-timesheet.tool.js"; import UpdateBankTransactionTool from "./update-bank-transaction.tool.js"; @@ -14,6 +16,8 @@ import UpdateTrackingCategoryTool from "./update-tracking-category.tool.js"; import UpdateTrackingOptionsTool from "./update-tracking-options.tool.js"; export const UpdateTools = [ + UpdateAccountTool, + ArchiveAccountTool, UpdateContactTool, UpdateCreditNoteTool, UpdateInvoiceTool, diff --git a/src/tools/update/update-account.tool.ts b/src/tools/update/update-account.tool.ts new file mode 100644 index 00000000..f748f953 --- /dev/null +++ b/src/tools/update/update-account.tool.ts @@ -0,0 +1,162 @@ +import { updateXeroAccount } from "../../handlers/update-xero-account.handler.js"; +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 { AccountType } from "xero-node"; + +const UpdateAccountTool = CreateXeroTool( + "update-account", + "Update an account in Xero's chart of accounts.\ + When an account is updated, a deep link to the account in Xero is returned. \ + This deep link can be used to view the account in Xero directly. \ + This link should be displayed to the user.", + { + accountId: z.string().describe("The Xero ID of the account to update"), + name: z + .string() + .optional() + .describe("Name of the account (max 150 chars)"), + code: z + .string() + .optional() + .describe("A unique alphanumeric account code (max 10 chars)"), + type: z + .enum([ + "BANK", + "CURRENT", + "CURRLIAB", + "DEPRECIATN", + "DIRECTCOSTS", + "EQUITY", + "EXPENSE", + "FIXED", + "INVENTORY", + "LIABILITY", + "NONCURRENT", + "OTHERINCOME", + "OVERHEADS", + "PREPAYMENT", + "REVENUE", + "SALES", + "TERMLIAB", + "PAYG", + ]) + .optional() + .describe("The account type"), + description: z + .string() + .optional() + .describe( + "Description of the account (max 4000 chars, not valid for bank accounts)", + ), + taxType: z + .string() + .optional() + .describe("The tax type for the account"), + currencyCode: z + .string() + .optional() + .describe("The currency code (e.g., USD, GBP, NZD)"), + enablePaymentsToAccount: z + .boolean() + .optional() + .describe("Whether the account accepts payment applications"), + showInExpenseClaims: z + .boolean() + .optional() + .describe("Whether the account is available for expense claims"), + reportingCode: z + .string() + .optional() + .describe("Custom reporting code"), + reportingCodeName: z + .string() + .optional() + .describe("Name of the reporting code"), + addToWatchlist: z + .boolean() + .optional() + .describe("Whether the account is shown on the dashboard watchlist"), + }, + async ({ + accountId, + name, + code, + type, + description, + taxType, + currencyCode, + enablePaymentsToAccount, + showInExpenseClaims, + reportingCode, + reportingCodeName, + addToWatchlist, + }) => { + try { + const response = await updateXeroAccount( + accountId, + name, + code, + type as unknown as AccountType, + description, + taxType, + currencyCode, + enablePaymentsToAccount, + showInExpenseClaims, + reportingCode, + reportingCodeName, + addToWatchlist, + ); + + if (response.isError) { + return { + content: [ + { + type: "text" as const, + text: `Error updating account: ${response.error}`, + }, + ], + }; + } + + const account = response.result; + + const deepLink = account.accountID + ? await getDeepLink(DeepLinkType.ACCOUNT, account.accountID) + : null; + + return { + content: [ + { + type: "text" as const, + text: [ + "Account updated successfully:", + `Name: ${account.name}`, + `Code: ${account.code}`, + `ID: ${account.accountID}`, + `Type: ${account.type}`, + `Status: ${account.status}`, + deepLink ? `Link to view: ${deepLink}` : null, + ] + .filter(Boolean) + .join("\n"), + }, + ], + }; + } catch (error) { + const err = ensureError(error); + + return { + content: [ + { + type: "text" as const, + text: `Error updating account: ${err.message}`, + }, + ], + }; + } + }, +); + +export default UpdateAccountTool;