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
54 changes: 54 additions & 0 deletions src/handlers/create-xero-credit-note-allocation.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { xeroClient } from "../clients/xero-client.js";
import { XeroClientResponse } from "../types/tool-response.js";
import { formatError } from "../helpers/format-error.js";
import { Allocation, Allocations } from "xero-node";
import { getClientHeaders } from "../helpers/get-client-headers.js";

export type AllocationLine = {
invoiceId: string;
amount: number;
date: string;
};

function toAllocations(lines: AllocationLine[]): Allocations {
return {
allocations: lines.map((line) => ({
invoice: { invoiceID: line.invoiceId },
amount: line.amount,
date: line.date,
})),
};
}

/**
* Apply a credit note to one or more invoices.
*/
export async function createXeroCreditNoteAllocation(
creditNoteId: string,
allocations: AllocationLine[],
): Promise<XeroClientResponse<Allocation[]>> {
try {
await xeroClient.authenticate();

const response = await xeroClient.accountingApi.createCreditNoteAllocation(
xeroClient.tenantId,
creditNoteId,
toAllocations(allocations),
undefined, // summarizeErrors
undefined, // idempotencyKey
getClientHeaders(),
);

return {
result: response.body.allocations ?? [],
isError: false,
error: null,
};
} catch (error) {
return {
result: null,
isError: true,
error: formatError(error),
};
}
}
49 changes: 49 additions & 0 deletions src/handlers/create-xero-overpayment-allocation.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { xeroClient } from "../clients/xero-client.js";
import { XeroClientResponse } from "../types/tool-response.js";
import { formatError } from "../helpers/format-error.js";
import { Allocation, Allocations } from "xero-node";
import { getClientHeaders } from "../helpers/get-client-headers.js";
import { AllocationLine } from "./create-xero-credit-note-allocation.handler.js";

function toAllocations(lines: AllocationLine[]): Allocations {
return {
allocations: lines.map((line) => ({
invoice: { invoiceID: line.invoiceId },
amount: line.amount,
date: line.date,
})),
};
}

/**
* Apply an overpayment to one or more invoices.
*/
export async function createXeroOverpaymentAllocation(
overpaymentId: string,
allocations: AllocationLine[],
): Promise<XeroClientResponse<Allocation[]>> {
try {
await xeroClient.authenticate();

const response = await xeroClient.accountingApi.createOverpaymentAllocations(
xeroClient.tenantId,
overpaymentId,
toAllocations(allocations),
undefined, // summarizeErrors
undefined, // idempotencyKey
getClientHeaders(),
);

return {
result: response.body.allocations ?? [],
isError: false,
error: null,
};
} catch (error) {
return {
result: null,
isError: true,
error: formatError(error),
};
}
}
49 changes: 49 additions & 0 deletions src/handlers/create-xero-prepayment-allocation.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { xeroClient } from "../clients/xero-client.js";
import { XeroClientResponse } from "../types/tool-response.js";
import { formatError } from "../helpers/format-error.js";
import { Allocation, Allocations } from "xero-node";
import { getClientHeaders } from "../helpers/get-client-headers.js";
import { AllocationLine } from "./create-xero-credit-note-allocation.handler.js";

function toAllocations(lines: AllocationLine[]): Allocations {
return {
allocations: lines.map((line) => ({
invoice: { invoiceID: line.invoiceId },
amount: line.amount,
date: line.date,
})),
};
}

/**
* Apply a prepayment to one or more invoices.
*/
export async function createXeroPrepaymentAllocation(
prepaymentId: string,
allocations: AllocationLine[],
): Promise<XeroClientResponse<Allocation[]>> {
try {
await xeroClient.authenticate();

const response = await xeroClient.accountingApi.createPrepaymentAllocations(
xeroClient.tenantId,
prepaymentId,
toAllocations(allocations),
undefined, // summarizeErrors
undefined, // idempotencyKey
getClientHeaders(),
);

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

async function getOverpayments(
contactId: string | undefined,
page: number,
pageSize: number = 10,
): Promise<Overpayment[]> {
await xeroClient.authenticate();

const response = await xeroClient.accountingApi.getOverpayments(
xeroClient.tenantId,
undefined, // ifModifiedSince
contactId ? `Contact.ContactID=guid("${contactId}")` : undefined, // where
"UpdatedDateUTC DESC", // order
page, // page
undefined, // unitdp
pageSize, // pageSize
getClientHeaders(),
);

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

/**
* List overpayments from Xero
*/
export async function listXeroOverpayments(
page: number = 1,
contactId?: string,
pageSize: number = 10,
): Promise<XeroClientResponse<Overpayment[]>> {
try {
const overpayments = await getOverpayments(contactId, page, pageSize);

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

async function getPrepayments(
contactId: string | undefined,
page: number,
pageSize: number = 10,
): Promise<Prepayment[]> {
await xeroClient.authenticate();

const response = await xeroClient.accountingApi.getPrepayments(
xeroClient.tenantId,
undefined, // ifModifiedSince
contactId ? `Contact.ContactID=guid("${contactId}")` : undefined, // where
"UpdatedDateUTC DESC", // order
page, // page
undefined, // unitdp
pageSize, // pageSize
getClientHeaders(),
);

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

/**
* List prepayments from Xero
*/
export async function listXeroPrepayments(
page: number = 1,
contactId?: string,
pageSize: number = 10,
): Promise<XeroClientResponse<Prepayment[]>> {
try {
const prepayments = await getPrepayments(contactId, page, pageSize);

return {
result: prepayments,
isError: false,
error: null,
};
} catch (error) {
return {
result: null,
isError: true,
error: formatError(error),
};
}
}
22 changes: 22 additions & 0 deletions src/helpers/allocation-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from "zod";

// One invoice that a credit note / overpayment / prepayment is applied to.
// Shared by all three create-allocation tools. Mirrors the SDK Allocation model
// (only the target invoice id, amount, and date are needed on the request body —
// the source object is identified by the endpoint path param).
export const allocationLineSchema = z.object({
invoiceId: z
.string()
.describe(
"The ID of the invoice to apply this allocation to. Obtain from the list-invoices tool. The invoice must be AUTHORISED and not fully paid.",
),
amount: z
.number()
.positive()
.describe(
"The amount to apply to this invoice (must be positive and not exceed the invoice's amount due or the source's remaining balance).",
),
date: z
.string()
.describe("The date the allocation is applied, in YYYY-MM-DD format."),
});
57 changes: 57 additions & 0 deletions src/tools/create/create-credit-note-allocation.tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { z } from "zod";
import { createXeroCreditNoteAllocation } from "../../handlers/create-xero-credit-note-allocation.handler.js";
import { CreateXeroTool } from "../../helpers/create-xero-tool.js";
import { allocationLineSchema } from "../../helpers/allocation-schema.js";

const CreateCreditNoteAllocationTool = CreateXeroTool(
"create-credit-note-allocation",
`Apply a credit note to one or more invoices in Xero. Provide the credit note ID (from
list-credit-notes) and one allocation per target invoice (invoice ID from list-invoices,
the amount to apply, and the date). The credit note must be AUTHORISED with remaining
credit; each invoice must be AUTHORISED and not fully paid. The total allocated cannot
exceed the credit note's remaining credit.`,
{
creditNoteId: z
.string()
.describe("The ID of the credit note to allocate. Obtain from the list-credit-notes tool."),
allocations: z
.array(allocationLineSchema)
.min(1)
.describe("One entry per invoice the credit note is applied to."),
},
async ({ creditNoteId, allocations }) => {
const response = await createXeroCreditNoteAllocation(creditNoteId, allocations);
if (response.isError) {
return {
content: [
{
type: "text" as const,
text: `Error creating credit note allocation: ${response.error}`,
},
],
};
}

const applied = response.result ?? [];

return {
content: [
{
type: "text" as const,
text: `Applied credit note ${creditNoteId} to ${applied.length} invoice(s):`,
},
...applied.map((allocation) => ({
type: "text" as const,
text: [
`Allocation ID: ${allocation.allocationID ?? "(not returned)"}`,
`Invoice ID: ${allocation.invoice?.invoiceID ?? "Unknown"}`,
`Amount: ${allocation.amount}`,
`Date: ${allocation.date}`,
].join("\n"),
})),
],
};
},
);

export default CreateCreditNoteAllocationTool;
57 changes: 57 additions & 0 deletions src/tools/create/create-overpayment-allocation.tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { z } from "zod";
import { createXeroOverpaymentAllocation } from "../../handlers/create-xero-overpayment-allocation.handler.js";
import { CreateXeroTool } from "../../helpers/create-xero-tool.js";
import { allocationLineSchema } from "../../helpers/allocation-schema.js";

const CreateOverpaymentAllocationTool = CreateXeroTool(
"create-overpayment-allocation",
`Apply an overpayment to one or more invoices in Xero. Provide the overpayment ID (from
list-overpayments) and one allocation per target invoice (invoice ID from list-invoices,
the amount to apply, and the date). The overpayment must have remaining credit; each
invoice must be AUTHORISED and not fully paid. The total allocated cannot exceed the
overpayment's remaining credit.`,
{
overpaymentId: z
.string()
.describe("The ID of the overpayment to allocate. Obtain from the list-overpayments tool."),
allocations: z
.array(allocationLineSchema)
.min(1)
.describe("One entry per invoice the overpayment is applied to."),
},
async ({ overpaymentId, allocations }) => {
const response = await createXeroOverpaymentAllocation(overpaymentId, allocations);
if (response.isError) {
return {
content: [
{
type: "text" as const,
text: `Error creating overpayment allocation: ${response.error}`,
},
],
};
}

const applied = response.result ?? [];

return {
content: [
{
type: "text" as const,
text: `Applied overpayment ${overpaymentId} to ${applied.length} invoice(s):`,
},
...applied.map((allocation) => ({
type: "text" as const,
text: [
`Allocation ID: ${allocation.allocationID ?? "(not returned)"}`,
`Invoice ID: ${allocation.invoice?.invoiceID ?? "Unknown"}`,
`Amount: ${allocation.amount}`,
`Date: ${allocation.date}`,
].join("\n"),
})),
],
};
},
);

export default CreateOverpaymentAllocationTool;
Loading