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
144 changes: 144 additions & 0 deletions src/handlers/create-xero-repeating-invoice.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { xeroClient } from "../clients/xero-client.js";
import { XeroClientResponse } from "../types/tool-response.js";
import { formatError } from "../helpers/format-error.js";
import {
RepeatingInvoice,
Schedule,
LineItemTracking,
CurrencyCode,
LineAmountTypes,
} from "xero-node";
import { getClientHeaders } from "../helpers/get-client-headers.js";

export type LineAmountTypeInput = "EXCLUSIVE" | "INCLUSIVE" | "NO_TAX";

function mapLineAmountType(
input?: LineAmountTypeInput,
): LineAmountTypes | undefined {
switch (input) {
case "EXCLUSIVE":
return LineAmountTypes.Exclusive;
case "INCLUSIVE":
return LineAmountTypes.Inclusive;
case "NO_TAX":
return LineAmountTypes.NoTax;
default:
return undefined;
}
}

export interface RepeatingInvoiceLineItem {
description: string;
quantity: number;
unitAmount: number;
accountCode: string;
taxType: string;
itemCode?: string;
tracking?: LineItemTracking[];
}

export interface RepeatingInvoiceScheduleInput {
period: number;
unit: "WEEKLY" | "MONTHLY";
startDate: string;
dueDate?: number;
dueDateType?: string;
endDate?: string;
}

export interface CreateRepeatingInvoiceInput {
contactId: string;
schedule: RepeatingInvoiceScheduleInput;
lineItems: RepeatingInvoiceLineItem[];
type?: "ACCREC" | "ACCPAY";
status?: "DRAFT" | "AUTHORISED";
reference?: string;
brandingThemeId?: string;
currencyCode?: string;
lineAmountType?: LineAmountTypeInput;
approvedForSending?: boolean;
sendCopy?: boolean;
markAsSent?: boolean;
includePDF?: boolean;
}

function buildSchedule(input: RepeatingInvoiceScheduleInput): Schedule {
return {
period: input.period,
unit: Schedule.UnitEnum[input.unit as keyof typeof Schedule.UnitEnum],
startDate: input.startDate,
dueDate: input.dueDate,
dueDateType: input.dueDateType
? Schedule.DueDateTypeEnum[
input.dueDateType as keyof typeof Schedule.DueDateTypeEnum
]
: undefined,
endDate: input.endDate,
};
}

function buildRepeatingInvoice(
input: CreateRepeatingInvoiceInput,
): RepeatingInvoice {
return {
type:
RepeatingInvoice.TypeEnum[
(input.type ?? "ACCREC") as keyof typeof RepeatingInvoice.TypeEnum
],
contact: { contactID: input.contactId },
schedule: buildSchedule(input.schedule),
lineItems: input.lineItems,
status:
RepeatingInvoice.StatusEnum[
(input.status ?? "DRAFT") as keyof typeof RepeatingInvoice.StatusEnum
],
reference: input.reference,
brandingThemeID: input.brandingThemeId,
currencyCode: input.currencyCode
? CurrencyCode[input.currencyCode as keyof typeof CurrencyCode]
: undefined,
lineAmountTypes: mapLineAmountType(input.lineAmountType),
approvedForSending: input.approvedForSending,
sendCopy: input.sendCopy,
markAsSent: input.markAsSent,
includePDF: input.includePDF,
};
}

/**
* Create a repeating-invoice template in Xero
*/
export async function createXeroRepeatingInvoice(
input: CreateRepeatingInvoiceInput,
): Promise<XeroClientResponse<RepeatingInvoice>> {
try {
await xeroClient.authenticate();

const repeatingInvoice = buildRepeatingInvoice(input);

const response = await xeroClient.accountingApi.createRepeatingInvoices(
xeroClient.tenantId,
{ repeatingInvoices: [repeatingInvoice] },
true, // summarizeErrors
undefined, // idempotencyKey
getClientHeaders(),
);

const created = response.body.repeatingInvoices?.[0];
if (!created) {
throw new Error("Repeating invoice creation failed.");
}

return {
result: created,
isError: false,
error: null,
};
} catch (error) {
return {
result: null,
isError: true,
error: formatError(error),
};
}
}
53 changes: 53 additions & 0 deletions src/handlers/delete-xero-repeating-invoice.handler.ts
Original file line number Diff line number Diff line change
@@ -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 { RepeatingInvoice } from "xero-node";
import { getClientHeaders } from "../helpers/get-client-headers.js";

/**
* Delete a repeating-invoice template in Xero.
*
* Xero has no DELETE endpoint for repeating invoices. Deletion is a POST to
* /RepeatingInvoices carrying the template's ID and status=DELETED. (The API also does
* not support editing an existing template — POST-with-ID is only valid for deletion —
* so create and delete are the only mutations available.)
*/
export async function deleteXeroRepeatingInvoice(
repeatingInvoiceId: string,
): Promise<XeroClientResponse<RepeatingInvoice>> {
try {
await xeroClient.authenticate();

const response = await xeroClient.accountingApi.updateOrCreateRepeatingInvoices(
xeroClient.tenantId,
{
repeatingInvoices: [
{
repeatingInvoiceID: repeatingInvoiceId,
status: RepeatingInvoice.StatusEnum.DELETED,
},
],
},
true, // summarizeErrors
undefined, // idempotencyKey
getClientHeaders(),
);

const deleted = response.body.repeatingInvoices?.[0];
if (!deleted) {
throw new Error("Repeating invoice deletion failed.");
}

return {
result: deleted,
isError: false,
error: null,
};
} catch (error) {
return {
result: null,
isError: true,
error: formatError(error),
};
}
}
46 changes: 46 additions & 0 deletions src/handlers/get-xero-repeating-invoice.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { xeroClient } from "../clients/xero-client.js";
import { XeroClientResponse } from "../types/tool-response.js";
import { formatError } from "../helpers/format-error.js";
import { RepeatingInvoice } from "xero-node";
import { getClientHeaders } from "../helpers/get-client-headers.js";

async function getRepeatingInvoice(
repeatingInvoiceId: string,
): Promise<RepeatingInvoice | undefined> {
await xeroClient.authenticate();

const response = await xeroClient.accountingApi.getRepeatingInvoice(
xeroClient.tenantId,
repeatingInvoiceId, // repeatingInvoiceID
getClientHeaders(),
);

return response.body.repeatingInvoices?.[0];
}

/**
* Get a single repeating-invoice template (incl. line items) from Xero
*/
export async function getXeroRepeatingInvoice(
repeatingInvoiceId: string,
): Promise<XeroClientResponse<RepeatingInvoice>> {
try {
const repeatingInvoice = await getRepeatingInvoice(repeatingInvoiceId);

if (!repeatingInvoice) {
throw new Error("Repeating invoice not found.");
}

return {
result: repeatingInvoice,
isError: false,
error: null,
};
} catch (error) {
return {
result: null,
isError: true,
error: formatError(error),
};
}
}
45 changes: 45 additions & 0 deletions src/handlers/list-xero-repeating-invoices.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { xeroClient } from "../clients/xero-client.js";
import { XeroClientResponse } from "../types/tool-response.js";
import { formatError } from "../helpers/format-error.js";
import { RepeatingInvoice } from "xero-node";
import { getClientHeaders } from "../helpers/get-client-headers.js";

async function getRepeatingInvoices(
where?: string,
order?: string,
): Promise<RepeatingInvoice[]> {
await xeroClient.authenticate();

const response = await xeroClient.accountingApi.getRepeatingInvoices(
xeroClient.tenantId,
where, // where
order, // order
getClientHeaders(),
);

return response.body.repeatingInvoices ?? [];
}

/**
* List repeating-invoice templates from Xero
*/
export async function listXeroRepeatingInvoices(
where?: string,
order?: string,
): Promise<XeroClientResponse<RepeatingInvoice[]>> {
try {
const repeatingInvoices = await getRepeatingInvoices(where, order);

return {
result: repeatingInvoices,
isError: false,
error: null,
};
} catch (error) {
return {
result: null,
isError: true,
error: formatError(error),
};
}
}
101 changes: 101 additions & 0 deletions src/tools/create/create-repeating-invoice.tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { z } from "zod";
import { createXeroRepeatingInvoice } from "../../handlers/create-xero-repeating-invoice.handler.js";
import { CreateXeroTool } from "../../helpers/create-xero-tool.js";

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"),
unitAmount: z.number().describe("The price per unit of the line item"),
accountCode: z.string().describe("The account code of the line item - can be obtained from the list-accounts tool"),
taxType: z.string().describe("The tax type of the line item - can be obtained from the list-tax-rates tool"),
itemCode: z.string().describe("The item code of the line item - can be obtained from the list-items tool \
If the item is not listed, add without an item code and ask the user if they would like to add an item code.").optional(),
tracking: z.array(trackingSchema).describe("Up to 2 tracking categories and options can be added to the line item. \
Can be obtained from the list-tracking-categories tool. \
Only use if prompted by the user.").optional(),
});

const scheduleSchema = z.object({
period: z.number().describe("How many units between invoices, e.g. 1 (every 1) or 2 (every 2)."),
unit: z.enum(["WEEKLY", "MONTHLY"]).describe("The repeat unit."),
startDate: z.string().describe("Date the first invoice is generated (YYYY-MM-DD)."),
dueDate: z.number().describe("Payment-terms value used with dueDateType, e.g. 20 or 31.").optional(),
dueDateType: z
.enum([
"DAYSAFTERBILLDATE",
"DAYSAFTERBILLMONTH",
"DAYSAFTERINVOICEDATE",
"DAYSAFTERINVOICEMONTH",
"OFCURRENTMONTH",
"OFFOLLOWINGMONTH",
])
.describe("Payment-terms type used with dueDate.")
.optional(),
endDate: z.string().describe("Optional date the schedule ends (YYYY-MM-DD).").optional(),
});

const CreateRepeatingInvoiceTool = CreateXeroTool(
"create-repeating-invoice",
"Create a repeating-invoice template in Xero. The template auto-generates invoices on the \
given schedule. Defaults to a DRAFT ACCREC (sales) template unless told otherwise.",
{
contactId: z.string().describe("The ID of the contact for the template. Can be obtained from the list-contacts tool."),
schedule: scheduleSchema,
lineItems: z.array(lineItemSchema),
type: z.enum(["ACCREC", "ACCPAY"]).describe("ACCREC for sales templates, ACCPAY for bill templates. Default ACCREC.").optional(),
status: z.enum(["DRAFT", "AUTHORISED"]).describe("DRAFT (default) or AUTHORISED.").optional(),
reference: z.string().describe("A reference for the generated invoices.").optional(),
brandingThemeId: z.string().describe("Optional branding theme ID.").optional(),
currencyCode: z.string().describe("Optional ISO currency code, e.g. GBP.").optional(),
lineAmountType: z.enum(["EXCLUSIVE", "INCLUSIVE", "NO_TAX"]).describe("Whether line amounts are tax exclusive, inclusive, or no tax.").optional(),
approvedForSending: z.boolean().describe("Whether Xero emails the generated invoice to the contact.").optional(),
sendCopy: z.boolean().describe("Whether to send a copy to the sender's email.").optional(),
markAsSent: z.boolean().describe("Whether to mark the generated invoice as sent.").optional(),
includePDF: z.boolean().describe("Whether to attach a PDF to the emailed invoice.").optional(),
},
async (params) => {
const result = await createXeroRepeatingInvoice(params);

if (result.isError) {
return {
content: [
{
type: "text" as const,
text: `Error creating repeating invoice: ${result.error}`,
},
],
};
}

const ri = result.result;

return {
content: [
{
type: "text" as const,
text: [
"Repeating invoice created successfully:",
`ID: ${ri?.repeatingInvoiceID}`,
`Type: ${ri?.type}`,
`Status: ${ri?.status}`,
`Contact: ${ri?.contact?.name}`,
ri?.schedule ? `Schedule: every ${ri.schedule.period} ${ri.schedule.unit}` : null,
ri?.schedule?.nextScheduledDate ? `Next Scheduled: ${ri.schedule.nextScheduledDate}` : null,
ri?.total != null ? `Total: ${ri.total}` : null,
]
.filter(Boolean)
.join("\n"),
},
],
};
},
);

export default CreateRepeatingInvoiceTool;
Loading