Skip to content

Commit 7495083

Browse files
bristolaTheRegan
andauthored
Tracking Categories and Options Tools (#69)
* Add list tracking categories tool * Add create tracking categories tool * Update and create categories and options * Cleanup * Fixes * Remove tracking category deep link Because Tracking Categories don't have deep links * Remove tracking category deep links --------- Co-authored-by: Regan Ashworth <regan.ashworth@gmail.com>
1 parent 7348845 commit 7495083

14 files changed

+545
-1
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { xeroClient } from "../clients/xero-client.js";
2+
import { XeroClientResponse } from "../types/tool-response.js";
3+
import { formatError } from "../helpers/format-error.js";
4+
import { getClientHeaders } from "../helpers/get-client-headers.js";
5+
import { TrackingCategory } from "xero-node";
6+
7+
async function createTrackingCategory(
8+
name: string
9+
): Promise<TrackingCategory | undefined> {
10+
xeroClient.authenticate();
11+
12+
const trackingCategory: TrackingCategory = {
13+
name: name
14+
};
15+
16+
const response = await xeroClient.accountingApi.createTrackingCategory(
17+
xeroClient.tenantId, // xeroTenantId
18+
trackingCategory,
19+
undefined, // idempotencyKey
20+
getClientHeaders() // options
21+
);
22+
23+
const createdTrackingCategory = response.body.trackingCategories?.[0];
24+
25+
return createdTrackingCategory;
26+
}
27+
28+
export async function createXeroTrackingCategory(
29+
name: string
30+
): Promise<XeroClientResponse<TrackingCategory>> {
31+
try {
32+
const createdTrackingCategory = await createTrackingCategory(name);
33+
34+
if (!createdTrackingCategory) {
35+
throw new Error("Tracking Category creation failed.");
36+
}
37+
38+
return {
39+
result: createdTrackingCategory,
40+
isError: false,
41+
error: null
42+
};
43+
} catch (error) {
44+
return {
45+
result: null,
46+
isError: true,
47+
error: formatError(error)
48+
};
49+
}
50+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { xeroClient } from "../clients/xero-client.js";
2+
import { XeroClientResponse } from "../types/tool-response.js";
3+
import { formatError } from "../helpers/format-error.js";
4+
import { getClientHeaders } from "../helpers/get-client-headers.js";
5+
import { TrackingOption } from "xero-node";
6+
7+
async function createTrackingOption(
8+
trackingCategoryId: string,
9+
name: string
10+
): Promise<TrackingOption | undefined> {
11+
xeroClient.authenticate();
12+
13+
const response = await xeroClient.accountingApi.createTrackingOptions(
14+
xeroClient.tenantId,
15+
trackingCategoryId,
16+
{
17+
name: name
18+
},
19+
undefined, // idempotencyKey
20+
getClientHeaders()
21+
);
22+
23+
const createdTrackingOption = response.body.options?.[0];
24+
25+
return createdTrackingOption;
26+
}
27+
28+
export async function createXeroTrackingOptions(
29+
trackingCategoryId: string,
30+
optionNames: string[]
31+
): Promise<XeroClientResponse<TrackingOption[]>> {
32+
try {
33+
const createdOptions = await Promise.all(
34+
optionNames.map(async optionName => await createTrackingOption(trackingCategoryId, optionName))
35+
);
36+
37+
return {
38+
result: createdOptions
39+
.filter(Boolean)
40+
.map(option => option as TrackingOption),
41+
isError: false,
42+
error: null
43+
};
44+
} catch (error) {
45+
return {
46+
result: null,
47+
isError: true,
48+
error: formatError(error)
49+
};
50+
}
51+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { xeroClient } from "../clients/xero-client.js";
2+
import { TrackingCategory } from "xero-node";
3+
import { getClientHeaders } from "../helpers/get-client-headers.js";
4+
import { XeroClientResponse } from "../types/tool-response.js";
5+
import { formatError } from "../helpers/format-error.js";
6+
7+
async function getTrackingCategories(
8+
includeArchived?: boolean
9+
): Promise<TrackingCategory[]> {
10+
await xeroClient.authenticate();
11+
12+
const response = await xeroClient.accountingApi.getTrackingCategories(
13+
xeroClient.tenantId, // xeroTenantId
14+
undefined, // where
15+
undefined, // order
16+
includeArchived, // includeArchived
17+
getClientHeaders()
18+
);
19+
20+
return response.body.trackingCategories ?? [];
21+
}
22+
23+
export async function listXeroTrackingCategories(
24+
includeArchived?: boolean
25+
): Promise<XeroClientResponse<TrackingCategory[]>> {
26+
try {
27+
const trackingCategories = await getTrackingCategories(includeArchived);
28+
29+
return {
30+
result: trackingCategories,
31+
isError: false,
32+
error: null
33+
};
34+
} catch (error) {
35+
return {
36+
result: null,
37+
isError: true,
38+
error: formatError(error)
39+
};
40+
}
41+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { TrackingCategory } from "xero-node";
2+
import { xeroClient } from "../clients/xero-client.js";
3+
import { formatError } from "../helpers/format-error.js";
4+
import { getClientHeaders } from "../helpers/get-client-headers.js";
5+
import { XeroClientResponse } from "../types/tool-response.js";
6+
7+
type TrackingCategoryStatus = "ACTIVE" | "ARCHIVED";
8+
9+
async function getTrackingCategory(trackingCategoryId: string): Promise<TrackingCategory | undefined> {
10+
await xeroClient.authenticate();
11+
12+
const response = await xeroClient.accountingApi.getTrackingCategory(
13+
xeroClient.tenantId,
14+
trackingCategoryId,
15+
getClientHeaders()
16+
);
17+
18+
return response.body.trackingCategories?.[0];
19+
}
20+
21+
async function updateTrackingCategory(
22+
trackingCategoryId: string,
23+
existingTrackingCategory: TrackingCategory,
24+
name?: string,
25+
status?: TrackingCategoryStatus
26+
): Promise<TrackingCategory | undefined> {
27+
const trackingCategory: TrackingCategory = {
28+
trackingCategoryID: trackingCategoryId,
29+
name: name ? name : existingTrackingCategory.name,
30+
status: status ? TrackingCategory.StatusEnum[status] : existingTrackingCategory.status
31+
};
32+
33+
await xeroClient.accountingApi.updateTrackingCategory(
34+
xeroClient.tenantId,
35+
trackingCategoryId,
36+
trackingCategory,
37+
undefined, // idempotencyKey
38+
getClientHeaders()
39+
);
40+
41+
return trackingCategory;
42+
}
43+
44+
export async function updateXeroTrackingCategory(
45+
trackingCategoryId: string,
46+
name?: string,
47+
status?: TrackingCategoryStatus
48+
): Promise<XeroClientResponse<TrackingCategory>> {
49+
try {
50+
const existingTrackingCategory = await getTrackingCategory(trackingCategoryId);
51+
52+
if (!existingTrackingCategory) {
53+
throw new Error("Could not find tracking category.");
54+
}
55+
56+
const updatedTrackingCategory = await updateTrackingCategory(
57+
trackingCategoryId,
58+
existingTrackingCategory,
59+
name,
60+
status
61+
);
62+
63+
if (!updatedTrackingCategory) {
64+
throw new Error("Failed to update tracking category.");
65+
}
66+
67+
return {
68+
result: existingTrackingCategory,
69+
isError: false,
70+
error: null
71+
};
72+
} catch (error) {
73+
return {
74+
result: null,
75+
isError: true,
76+
error: formatError(error),
77+
};
78+
}
79+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { TrackingOption } from "xero-node";
2+
import { xeroClient } from "../clients/xero-client.js";
3+
import { formatError } from "../helpers/format-error.js";
4+
import { getClientHeaders } from "../helpers/get-client-headers.js";
5+
import { XeroClientResponse } from "../types/tool-response.js";
6+
7+
type TrackingOptionStatus = "ACTIVE" | "ARCHIVED";
8+
9+
interface TrackingOptionItem {
10+
trackingOptionId: string,
11+
name?: string,
12+
status?: TrackingOptionStatus
13+
}
14+
15+
async function getTrackingOptions(trackingCategoryId: string): Promise<TrackingOption[] | undefined> {
16+
await xeroClient.authenticate();
17+
18+
const response = await xeroClient.accountingApi.getTrackingCategory(
19+
xeroClient.tenantId,
20+
trackingCategoryId,
21+
getClientHeaders()
22+
);
23+
24+
return response.body.trackingCategories?.[0].options;
25+
}
26+
27+
async function updateTrackingOption(
28+
trackingCategoryId: string,
29+
trackingOptionId: string,
30+
existingTrackingOption: TrackingOption,
31+
name?: string,
32+
status?: TrackingOptionStatus
33+
): Promise<TrackingOption | undefined> {
34+
const trackingOption: TrackingOption = {
35+
trackingOptionID: trackingOptionId,
36+
name: name ? name : existingTrackingOption.name,
37+
status: status ? TrackingOption.StatusEnum[status] : existingTrackingOption.status
38+
};
39+
40+
await xeroClient.accountingApi.updateTrackingOptions(
41+
xeroClient.tenantId,
42+
trackingCategoryId,
43+
trackingOptionId,
44+
trackingOption,
45+
undefined, // idempotencyKey
46+
getClientHeaders()
47+
);
48+
49+
return trackingOption;
50+
}
51+
52+
export async function updateXeroTrackingOption(
53+
trackingCategoryId: string,
54+
options: TrackingOptionItem[]
55+
): Promise<XeroClientResponse<TrackingOption[]>> {
56+
try {
57+
58+
const existingTrackingOptions = await getTrackingOptions(trackingCategoryId);
59+
60+
if (!existingTrackingOptions) {
61+
throw new Error("Could not find tracking options.");
62+
}
63+
64+
const updatedTrackingOptions = await Promise.all(options?.map(async (option) => {
65+
const existingTrackingOption = existingTrackingOptions
66+
.find(existingOption => existingOption.trackingOptionID === option.trackingOptionId);
67+
68+
return existingTrackingOption
69+
? await updateTrackingOption(trackingCategoryId, option.trackingOptionId, existingTrackingOption, option.name, option.status)
70+
: undefined;
71+
}));
72+
73+
if (!updatedTrackingOptions) {
74+
throw new Error("Failed to update tracking options.");
75+
}
76+
77+
return {
78+
result: updatedTrackingOptions
79+
.filter(Boolean)
80+
.map(option => option as TrackingOption),
81+
isError: false,
82+
error: null
83+
};
84+
} catch (error) {
85+
return {
86+
result: null,
87+
isError: true,
88+
error: formatError(error),
89+
};
90+
}
91+
}

src/helpers/format-tracking-option.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { TrackingOption } from "xero-node";
2+
3+
export const formatTrackingOption = (option: TrackingOption): string => {
4+
return [
5+
`Option ID: ${option.trackingOptionID}`,
6+
`Name: ${option.name}`,
7+
`Status: ${option.status}`
8+
].join("\n");
9+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { z } from "zod";
2+
import { CreateXeroTool } from "../../helpers/create-xero-tool.js";
3+
import { createXeroTrackingCategory } from "../../handlers/create-xero-tracking-category.handler.js";
4+
5+
const CreateTrackingCategoryTool = CreateXeroTool(
6+
"create-tracking-category",
7+
`Create a tracking category in Xero.`,
8+
{
9+
name: z.string()
10+
},
11+
async ({ name }) => {
12+
const response = await createXeroTrackingCategory(name);
13+
14+
if (response.isError) {
15+
return {
16+
content: [
17+
{
18+
type: "text" as const,
19+
text: `Error while creating tracking category: ${response.error}`
20+
}
21+
]
22+
};
23+
}
24+
25+
const trackingCategory = response.result;
26+
27+
return {
28+
content: [
29+
{
30+
type: "text" as const,
31+
text: `Created the tracking category "${trackingCategory.name}" (${trackingCategory.trackingCategoryID}).`
32+
},
33+
]
34+
};
35+
}
36+
);
37+
38+
export default CreateTrackingCategoryTool;

0 commit comments

Comments
 (0)