@slates-integrations/zoho 0.2.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/docs/SPEC.md +155 -0
- package/logo.png +0 -0
- package/package.json +19 -0
- package/slate.json +17 -0
- package/src/auth.ts +339 -0
- package/src/config.ts +11 -0
- package/src/index.ts +44 -0
- package/src/lib/client.ts +733 -0
- package/src/lib/errors.ts +90 -0
- package/src/lib/urls.ts +77 -0
- package/src/spec.ts +12 -0
- package/src/tools/books-get-invoices.ts +114 -0
- package/src/tools/books-manage-contact.ts +168 -0
- package/src/tools/books-manage-expense.ts +142 -0
- package/src/tools/books-manage-invoice.ts +190 -0
- package/src/tools/crm-get-modules.ts +93 -0
- package/src/tools/crm-get-records.ts +137 -0
- package/src/tools/crm-get-related-records.ts +104 -0
- package/src/tools/crm-manage-record.ts +117 -0
- package/src/tools/crm-search-records.ts +109 -0
- package/src/tools/desk-get-tickets.ts +146 -0
- package/src/tools/desk-manage-contact.ts +125 -0
- package/src/tools/desk-manage-ticket.ts +126 -0
- package/src/tools/index.ts +16 -0
- package/src/tools/people-manage-employee.ts +163 -0
- package/src/tools/projects-get-portals.ts +50 -0
- package/src/tools/projects-manage-project.ts +164 -0
- package/src/tools/projects-manage-task.ts +108 -0
- package/src/triggers/crm-record-events.ts +143 -0
- package/src/triggers/desk-events.ts +121 -0
- package/src/triggers/index.ts +2 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ServiceError, badRequestError } from '@lowerdeck/error';
|
|
2
|
+
|
|
3
|
+
type ErrorResponse = {
|
|
4
|
+
status?: number;
|
|
5
|
+
statusText?: string;
|
|
6
|
+
data?: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
10
|
+
typeof value === 'object' && value !== null;
|
|
11
|
+
|
|
12
|
+
let pushMessage = (messages: string[], value: unknown) => {
|
|
13
|
+
if (typeof value !== 'string') return;
|
|
14
|
+
|
|
15
|
+
let trimmed = value.trim();
|
|
16
|
+
if (trimmed && !messages.includes(trimmed)) {
|
|
17
|
+
messages.push(trimmed);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let collectZohoMessages = (value: unknown, messages: string[]) => {
|
|
22
|
+
if (!isRecord(value)) {
|
|
23
|
+
pushMessage(messages, value);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (let key of ['message', 'error', 'error_description', 'code']) {
|
|
28
|
+
pushMessage(messages, value[key]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isRecord(value.details)) {
|
|
32
|
+
for (let [key, detailValue] of Object.entries(value.details)) {
|
|
33
|
+
if (typeof detailValue === 'string') {
|
|
34
|
+
pushMessage(messages, `${key}: ${detailValue}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(value.data)) {
|
|
40
|
+
for (let item of value.data) {
|
|
41
|
+
collectZohoMessages(item, messages);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let extractZohoMessage = (error: unknown) => {
|
|
47
|
+
let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined;
|
|
48
|
+
let messages: string[] = [];
|
|
49
|
+
|
|
50
|
+
collectZohoMessages(response?.data, messages);
|
|
51
|
+
|
|
52
|
+
if (messages.length > 0) {
|
|
53
|
+
return messages.join(' - ');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (error instanceof Error && error.message) {
|
|
57
|
+
return error.message;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return 'Unknown error';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export let zohoServiceError = (message: string) =>
|
|
64
|
+
new ServiceError(badRequestError({ message }));
|
|
65
|
+
|
|
66
|
+
export let zohoApiError = (error: unknown, operation = 'request') => {
|
|
67
|
+
if (error instanceof ServiceError) {
|
|
68
|
+
return error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined;
|
|
72
|
+
let status = response?.status;
|
|
73
|
+
let statusLabel =
|
|
74
|
+
status !== undefined
|
|
75
|
+
? `HTTP ${status}${response?.statusText ? ` ${response.statusText}` : ''}: `
|
|
76
|
+
: '';
|
|
77
|
+
|
|
78
|
+
let serviceError = zohoServiceError(
|
|
79
|
+
`Zoho API ${operation} failed: ${statusLabel}${extractZohoMessage(error)}`
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
serviceError.data.reason = 'zoho_api_error';
|
|
83
|
+
serviceError.data.upstreamStatus = status;
|
|
84
|
+
|
|
85
|
+
if (error instanceof Error) {
|
|
86
|
+
serviceError.setParent(error);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return serviceError;
|
|
90
|
+
};
|
package/src/lib/urls.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type Datacenter = 'us' | 'eu' | 'in' | 'au' | 'jp' | 'ca';
|
|
2
|
+
|
|
3
|
+
let accountsUrls: Record<Datacenter, string> = {
|
|
4
|
+
us: 'https://accounts.zoho.com',
|
|
5
|
+
eu: 'https://accounts.zoho.eu',
|
|
6
|
+
in: 'https://accounts.zoho.in',
|
|
7
|
+
au: 'https://accounts.zoho.com.au',
|
|
8
|
+
jp: 'https://accounts.zoho.jp',
|
|
9
|
+
ca: 'https://accounts.zoho.ca'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let apiBaseUrls: Record<Datacenter, string> = {
|
|
13
|
+
us: 'https://www.zohoapis.com',
|
|
14
|
+
eu: 'https://www.zohoapis.eu',
|
|
15
|
+
in: 'https://www.zohoapis.in',
|
|
16
|
+
au: 'https://www.zohoapis.com.au',
|
|
17
|
+
jp: 'https://www.zohoapis.jp',
|
|
18
|
+
ca: 'https://www.zohoapis.ca'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let deskBaseUrls: Record<Datacenter, string> = {
|
|
22
|
+
us: 'https://desk.zoho.com',
|
|
23
|
+
eu: 'https://desk.zoho.eu',
|
|
24
|
+
in: 'https://desk.zoho.in',
|
|
25
|
+
au: 'https://desk.zoho.com.au',
|
|
26
|
+
jp: 'https://desk.zoho.jp',
|
|
27
|
+
ca: 'https://desk.zoho.ca'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let peopleBaseUrls: Record<Datacenter, string> = {
|
|
31
|
+
us: 'https://people.zoho.com',
|
|
32
|
+
eu: 'https://people.zoho.eu',
|
|
33
|
+
in: 'https://people.zoho.in',
|
|
34
|
+
au: 'https://people.zoho.com.au',
|
|
35
|
+
jp: 'https://people.zoho.jp',
|
|
36
|
+
ca: 'https://people.zoho.ca'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let projectsBaseUrls: Record<Datacenter, string> = {
|
|
40
|
+
us: 'https://projectsapi.zoho.com',
|
|
41
|
+
eu: 'https://projectsapi.zoho.eu',
|
|
42
|
+
in: 'https://projectsapi.zoho.in',
|
|
43
|
+
au: 'https://projectsapi.zoho.com.au',
|
|
44
|
+
jp: 'https://projectsapi.zoho.jp',
|
|
45
|
+
ca: 'https://projectsapi.zoho.ca'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let locationToDatacenter: Record<string, Datacenter> = {
|
|
49
|
+
us: 'us',
|
|
50
|
+
eu: 'eu',
|
|
51
|
+
in: 'in',
|
|
52
|
+
au: 'au',
|
|
53
|
+
jp: 'jp',
|
|
54
|
+
ca: 'ca'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export let getAccountsUrl = (dc: Datacenter): string => accountsUrls[dc];
|
|
58
|
+
export let getApiBaseUrl = (dc: Datacenter): string => apiBaseUrls[dc];
|
|
59
|
+
export let getDeskBaseUrl = (dc: Datacenter): string => deskBaseUrls[dc];
|
|
60
|
+
export let getPeopleBaseUrl = (dc: Datacenter): string => peopleBaseUrls[dc];
|
|
61
|
+
export let getProjectsBaseUrl = (dc: Datacenter): string => projectsBaseUrls[dc];
|
|
62
|
+
|
|
63
|
+
export let datacenterFromLocation = (location: string): Datacenter => {
|
|
64
|
+
return locationToDatacenter[location] ?? 'us';
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export let datacenterFromApiDomain = (apiDomain?: string): Datacenter | undefined => {
|
|
68
|
+
if (!apiDomain) return undefined;
|
|
69
|
+
|
|
70
|
+
for (let [dc, baseUrl] of Object.entries(apiBaseUrls)) {
|
|
71
|
+
if (apiDomain.startsWith(baseUrl)) {
|
|
72
|
+
return dc as Datacenter;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return undefined;
|
|
77
|
+
};
|
package/src/spec.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SlateSpecification } from 'slates';
|
|
2
|
+
import { auth } from './auth';
|
|
3
|
+
import { config } from './config';
|
|
4
|
+
|
|
5
|
+
export let spec = SlateSpecification.create({
|
|
6
|
+
key: 'zoho',
|
|
7
|
+
name: 'Zoho',
|
|
8
|
+
description: undefined,
|
|
9
|
+
metadata: {},
|
|
10
|
+
config,
|
|
11
|
+
auth
|
|
12
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { ZohoBooksClient } from '../lib/client';
|
|
5
|
+
import type { Datacenter } from '../lib/urls';
|
|
6
|
+
import { zohoServiceError } from '../lib/errors';
|
|
7
|
+
|
|
8
|
+
export let booksGetInvoices = SlateTool.create(spec, {
|
|
9
|
+
name: 'Books Get Invoices',
|
|
10
|
+
key: 'books_get_invoices',
|
|
11
|
+
description: `List or retrieve invoices from Zoho Books. Supports filtering by status, customer, and pagination. Can also list organizations to find the organization ID needed for all Books operations.`,
|
|
12
|
+
instructions: [
|
|
13
|
+
'The organizationId is required. Use listOrganizations=true to discover available organizations.',
|
|
14
|
+
'Provide invoiceId to fetch a single invoice with full details.',
|
|
15
|
+
'Filter by status: "sent", "draft", "overdue", "paid", "void", "unpaid", "partially_paid".'
|
|
16
|
+
],
|
|
17
|
+
tags: {
|
|
18
|
+
readOnly: true
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
.input(
|
|
22
|
+
z.object({
|
|
23
|
+
organizationId: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Zoho Books organization ID (required unless listing organizations)'),
|
|
27
|
+
invoiceId: z.string().optional().describe('Specific invoice ID to fetch'),
|
|
28
|
+
status: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Filter by status (e.g., "sent", "draft", "overdue", "paid", "void")'),
|
|
32
|
+
customerId: z.string().optional().describe('Filter invoices by customer ID'),
|
|
33
|
+
page: z.number().optional().describe('Page number'),
|
|
34
|
+
perPage: z.number().optional().describe('Records per page (max 200)'),
|
|
35
|
+
sortColumn: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Sort by column (e.g., "date", "invoice_number", "total")'),
|
|
39
|
+
sortOrder: z.enum(['ascending', 'descending']).optional().describe('Sort order'),
|
|
40
|
+
listOrganizations: z
|
|
41
|
+
.boolean()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('If true, lists available organizations instead of invoices')
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
.output(
|
|
47
|
+
z.object({
|
|
48
|
+
invoices: z.array(z.record(z.string(), z.any())).optional().describe('Invoice records'),
|
|
49
|
+
organizations: z
|
|
50
|
+
.array(
|
|
51
|
+
z.object({
|
|
52
|
+
organizationId: z.string(),
|
|
53
|
+
name: z.string(),
|
|
54
|
+
isDefault: z.boolean().optional(),
|
|
55
|
+
currencyCode: z.string().optional()
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Available organizations (if listOrganizations is true)'),
|
|
60
|
+
hasMorePages: z.boolean().optional()
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
.handleInvocation(async ctx => {
|
|
64
|
+
let dc = (ctx.auth.datacenter || ctx.config.datacenter || 'us') as Datacenter;
|
|
65
|
+
|
|
66
|
+
if (ctx.input.listOrganizations) {
|
|
67
|
+
let result = await ZohoBooksClient.listOrganizations(ctx.auth.token, dc);
|
|
68
|
+
let orgs = (result?.organizations || []).map((o: any) => ({
|
|
69
|
+
organizationId: o.organization_id,
|
|
70
|
+
name: o.name,
|
|
71
|
+
isDefault: o.is_default_org,
|
|
72
|
+
currencyCode: o.currency_code
|
|
73
|
+
}));
|
|
74
|
+
return {
|
|
75
|
+
output: { organizations: orgs },
|
|
76
|
+
message: `Found **${orgs.length}** Zoho Books organizations.`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!ctx.input.organizationId) throw zohoServiceError('organizationId is required');
|
|
81
|
+
let client = new ZohoBooksClient({
|
|
82
|
+
token: ctx.auth.token,
|
|
83
|
+
datacenter: dc,
|
|
84
|
+
organizationId: ctx.input.organizationId
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (ctx.input.invoiceId) {
|
|
88
|
+
let result = await client.getInvoice(ctx.input.invoiceId);
|
|
89
|
+
return {
|
|
90
|
+
output: { invoices: [result?.invoice || result] },
|
|
91
|
+
message: `Fetched invoice **${result?.invoice?.invoice_number || ctx.input.invoiceId}**.`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let result = await client.listInvoices({
|
|
96
|
+
page: ctx.input.page,
|
|
97
|
+
perPage: ctx.input.perPage,
|
|
98
|
+
status: ctx.input.status,
|
|
99
|
+
customerId: ctx.input.customerId,
|
|
100
|
+
sortColumn: ctx.input.sortColumn,
|
|
101
|
+
sortOrder: ctx.input.sortOrder
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let invoices = result?.invoices || [];
|
|
105
|
+
let pageContext = result?.page_context;
|
|
106
|
+
return {
|
|
107
|
+
output: {
|
|
108
|
+
invoices,
|
|
109
|
+
hasMorePages: pageContext?.has_more_page ?? false
|
|
110
|
+
},
|
|
111
|
+
message: `Retrieved **${invoices.length}** invoices.`
|
|
112
|
+
};
|
|
113
|
+
})
|
|
114
|
+
.build();
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { ZohoBooksClient } from '../lib/client';
|
|
5
|
+
import type { Datacenter } from '../lib/urls';
|
|
6
|
+
import { zohoServiceError } from '../lib/errors';
|
|
7
|
+
|
|
8
|
+
let addressSchema = z.object({
|
|
9
|
+
attention: z.string().optional(),
|
|
10
|
+
address: z.string().optional().describe('Street 1 address'),
|
|
11
|
+
street: z.string().optional().describe('Deprecated alias for address'),
|
|
12
|
+
street2: z.string().optional().describe('Street 2 address'),
|
|
13
|
+
city: z.string().optional(),
|
|
14
|
+
state: z.string().optional(),
|
|
15
|
+
stateCode: z.string().optional(),
|
|
16
|
+
zip: z.string().optional(),
|
|
17
|
+
country: z.string().optional(),
|
|
18
|
+
fax: z.string().optional(),
|
|
19
|
+
phone: z.string().optional()
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export let booksManageContact = SlateTool.create(spec, {
|
|
23
|
+
name: 'Books Manage Contact',
|
|
24
|
+
key: 'books_manage_contact',
|
|
25
|
+
description: `Create, update, or delete contacts (customers/vendors) in Zoho Books. Manage contact details, billing/shipping addresses, payment terms, and contact persons.`,
|
|
26
|
+
instructions: [
|
|
27
|
+
'The organizationId is required for all Zoho Books operations.',
|
|
28
|
+
'For create, contactName is required.',
|
|
29
|
+
'Use contactType to specify "customer" or "vendor".'
|
|
30
|
+
],
|
|
31
|
+
tags: {
|
|
32
|
+
destructive: true
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
.input(
|
|
36
|
+
z.object({
|
|
37
|
+
organizationId: z.string().describe('Zoho Books organization ID'),
|
|
38
|
+
action: z
|
|
39
|
+
.enum(['create', 'update', 'delete', 'get', 'list'])
|
|
40
|
+
.describe('Operation to perform'),
|
|
41
|
+
contactId: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Contact ID (required for get, update, delete)'),
|
|
45
|
+
contactName: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Contact/company name (required for create)'),
|
|
49
|
+
contactType: z.enum(['customer', 'vendor']).optional().describe('Type of contact'),
|
|
50
|
+
companyName: z.string().optional().describe('Company name'),
|
|
51
|
+
email: z.string().optional().describe('Contact email address'),
|
|
52
|
+
phone: z.string().optional().describe('Contact phone number'),
|
|
53
|
+
website: z.string().optional().describe('Contact website'),
|
|
54
|
+
billingAddress: addressSchema.optional().describe('Billing address'),
|
|
55
|
+
shippingAddress: addressSchema.optional().describe('Shipping address'),
|
|
56
|
+
paymentTerms: z.number().optional().describe('Net payment terms in days'),
|
|
57
|
+
notes: z.string().optional().describe('Notes about the contact'),
|
|
58
|
+
page: z.number().optional().describe('Page number (for list action)'),
|
|
59
|
+
perPage: z.number().optional().describe('Records per page (for list action)')
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
.output(
|
|
63
|
+
z.object({
|
|
64
|
+
contact: z.record(z.string(), z.any()).optional().describe('Contact record'),
|
|
65
|
+
contacts: z
|
|
66
|
+
.array(z.record(z.string(), z.any()))
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('List of contacts (for list action)'),
|
|
69
|
+
deleted: z.boolean().optional(),
|
|
70
|
+
hasMorePages: z.boolean().optional()
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
.handleInvocation(async ctx => {
|
|
74
|
+
let dc = (ctx.auth.datacenter || ctx.config.datacenter || 'us') as Datacenter;
|
|
75
|
+
let client = new ZohoBooksClient({
|
|
76
|
+
token: ctx.auth.token,
|
|
77
|
+
datacenter: dc,
|
|
78
|
+
organizationId: ctx.input.organizationId
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (ctx.input.action === 'list') {
|
|
82
|
+
let result = await client.listContacts({
|
|
83
|
+
page: ctx.input.page,
|
|
84
|
+
perPage: ctx.input.perPage,
|
|
85
|
+
contactType: ctx.input.contactType
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
output: {
|
|
89
|
+
contacts: result?.contacts || [],
|
|
90
|
+
hasMorePages: result?.page_context?.has_more_page ?? false
|
|
91
|
+
},
|
|
92
|
+
message: `Retrieved **${(result?.contacts || []).length}** contacts.`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (ctx.input.action === 'get') {
|
|
97
|
+
if (!ctx.input.contactId) throw zohoServiceError('contactId is required for get');
|
|
98
|
+
let result = await client.getContact(ctx.input.contactId);
|
|
99
|
+
return {
|
|
100
|
+
output: { contact: result?.contact || result },
|
|
101
|
+
message: `Fetched contact **${result?.contact?.contact_name || ctx.input.contactId}**.`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let buildAddress = (address: z.infer<typeof addressSchema>) => ({
|
|
106
|
+
attention: address.attention,
|
|
107
|
+
address: address.address || address.street,
|
|
108
|
+
street2: address.street2,
|
|
109
|
+
city: address.city,
|
|
110
|
+
state: address.state,
|
|
111
|
+
state_code: address.stateCode,
|
|
112
|
+
zip: address.zip,
|
|
113
|
+
country: address.country,
|
|
114
|
+
fax: address.fax,
|
|
115
|
+
phone: address.phone
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
let buildData = () => {
|
|
119
|
+
let data: Record<string, any> = {};
|
|
120
|
+
if (ctx.input.contactName) data.contact_name = ctx.input.contactName;
|
|
121
|
+
if (ctx.input.contactType) data.contact_type = ctx.input.contactType;
|
|
122
|
+
if (ctx.input.companyName) data.company_name = ctx.input.companyName;
|
|
123
|
+
if (ctx.input.email) data.email = ctx.input.email;
|
|
124
|
+
if (ctx.input.phone) data.phone = ctx.input.phone;
|
|
125
|
+
if (ctx.input.website) data.website = ctx.input.website;
|
|
126
|
+
if (ctx.input.paymentTerms !== undefined) data.payment_terms = ctx.input.paymentTerms;
|
|
127
|
+
if (ctx.input.notes) data.notes = ctx.input.notes;
|
|
128
|
+
if (ctx.input.billingAddress) {
|
|
129
|
+
data.billing_address = buildAddress(ctx.input.billingAddress);
|
|
130
|
+
}
|
|
131
|
+
if (ctx.input.shippingAddress) {
|
|
132
|
+
data.shipping_address = buildAddress(ctx.input.shippingAddress);
|
|
133
|
+
}
|
|
134
|
+
return data;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (ctx.input.action === 'create') {
|
|
138
|
+
if (!ctx.input.contactName) throw zohoServiceError('contactName is required for create');
|
|
139
|
+
let result = await client.createContact(buildData());
|
|
140
|
+
let contact = result?.contact;
|
|
141
|
+
return {
|
|
142
|
+
output: { contact },
|
|
143
|
+
message: `Created contact **${contact?.contact_name}**.`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (ctx.input.action === 'update') {
|
|
148
|
+
if (!ctx.input.contactId) throw zohoServiceError('contactId is required for update');
|
|
149
|
+
let result = await client.updateContact(ctx.input.contactId, buildData());
|
|
150
|
+
let contact = result?.contact;
|
|
151
|
+
return {
|
|
152
|
+
output: { contact },
|
|
153
|
+
message: `Updated contact **${contact?.contact_name || ctx.input.contactId}**.`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (ctx.input.action === 'delete') {
|
|
158
|
+
if (!ctx.input.contactId) throw zohoServiceError('contactId is required for delete');
|
|
159
|
+
await client.deleteContact(ctx.input.contactId);
|
|
160
|
+
return {
|
|
161
|
+
output: { contact: { contact_id: ctx.input.contactId }, deleted: true },
|
|
162
|
+
message: `Deleted contact **${ctx.input.contactId}**.`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
throw zohoServiceError('Invalid Books contact action.');
|
|
167
|
+
})
|
|
168
|
+
.build();
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { SlateTool } from 'slates';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { spec } from '../spec';
|
|
4
|
+
import { ZohoBooksClient } from '../lib/client';
|
|
5
|
+
import type { Datacenter } from '../lib/urls';
|
|
6
|
+
import { zohoServiceError } from '../lib/errors';
|
|
7
|
+
|
|
8
|
+
export let booksManageExpense = SlateTool.create(spec, {
|
|
9
|
+
name: 'Books Manage Expense',
|
|
10
|
+
key: 'books_manage_expense',
|
|
11
|
+
description: `Create, update, delete, or list expenses in Zoho Books. Track business expenses with account categorization, amounts, dates, vendors, and custom fields.`,
|
|
12
|
+
instructions: [
|
|
13
|
+
'The organizationId is required for all Zoho Books operations.',
|
|
14
|
+
'For create, accountId, date, and amount are required.'
|
|
15
|
+
],
|
|
16
|
+
tags: {
|
|
17
|
+
destructive: true
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
.input(
|
|
21
|
+
z.object({
|
|
22
|
+
organizationId: z.string().describe('Zoho Books organization ID'),
|
|
23
|
+
action: z
|
|
24
|
+
.enum(['create', 'update', 'delete', 'get', 'list'])
|
|
25
|
+
.describe('Operation to perform'),
|
|
26
|
+
expenseId: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('Expense ID (required for update, delete, get)'),
|
|
30
|
+
accountId: z.string().optional().describe('Expense account ID (required for create)'),
|
|
31
|
+
date: z.string().optional().describe('Expense date (YYYY-MM-DD, required for create)'),
|
|
32
|
+
amount: z.number().optional().describe('Expense amount (required for create)'),
|
|
33
|
+
paidThroughAccountId: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('Account ID through which payment was made'),
|
|
37
|
+
vendorId: z.string().optional().describe('Vendor ID'),
|
|
38
|
+
description: z.string().optional().describe('Expense description'),
|
|
39
|
+
referenceNumber: z.string().optional().describe('Reference number'),
|
|
40
|
+
taxId: z.string().optional().describe('Tax ID to apply'),
|
|
41
|
+
isBillable: z.boolean().optional().describe('Whether the expense is billable'),
|
|
42
|
+
customerId: z.string().optional().describe('Customer ID (for billable expenses)'),
|
|
43
|
+
projectId: z.string().optional().describe('Project ID'),
|
|
44
|
+
currencyId: z.string().optional().describe('Currency ID'),
|
|
45
|
+
page: z.number().optional().describe('Page number (for list action)'),
|
|
46
|
+
perPage: z.number().optional().describe('Records per page (for list action)'),
|
|
47
|
+
status: z.string().optional().describe('Filter by status (for list action)')
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
.output(
|
|
51
|
+
z.object({
|
|
52
|
+
expense: z.record(z.string(), z.any()).optional().describe('Expense record'),
|
|
53
|
+
expenses: z.array(z.record(z.string(), z.any())).optional().describe('List of expenses'),
|
|
54
|
+
deleted: z.boolean().optional(),
|
|
55
|
+
hasMorePages: z.boolean().optional(),
|
|
56
|
+
apiMessage: z.string().optional()
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
.handleInvocation(async ctx => {
|
|
60
|
+
let dc = (ctx.auth.datacenter || ctx.config.datacenter || 'us') as Datacenter;
|
|
61
|
+
let client = new ZohoBooksClient({
|
|
62
|
+
token: ctx.auth.token,
|
|
63
|
+
datacenter: dc,
|
|
64
|
+
organizationId: ctx.input.organizationId
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (ctx.input.action === 'list') {
|
|
68
|
+
let result = await client.listExpenses({
|
|
69
|
+
page: ctx.input.page,
|
|
70
|
+
perPage: ctx.input.perPage,
|
|
71
|
+
status: ctx.input.status
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
output: {
|
|
75
|
+
expenses: result?.expenses || [],
|
|
76
|
+
hasMorePages: result?.page_context?.has_more_page ?? false
|
|
77
|
+
},
|
|
78
|
+
message: `Retrieved **${(result?.expenses || []).length}** expenses.`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (ctx.input.action === 'get') {
|
|
83
|
+
if (!ctx.input.expenseId) throw zohoServiceError('expenseId is required for get');
|
|
84
|
+
let result = await client.getExpense(ctx.input.expenseId);
|
|
85
|
+
return {
|
|
86
|
+
output: { expense: result?.expense || result },
|
|
87
|
+
message: `Fetched expense **${ctx.input.expenseId}**.`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let buildData = () => {
|
|
92
|
+
let data: Record<string, any> = {};
|
|
93
|
+
if (ctx.input.accountId) data.account_id = ctx.input.accountId;
|
|
94
|
+
if (ctx.input.date) data.date = ctx.input.date;
|
|
95
|
+
if (ctx.input.amount !== undefined) data.amount = ctx.input.amount;
|
|
96
|
+
if (ctx.input.paidThroughAccountId)
|
|
97
|
+
data.paid_through_account_id = ctx.input.paidThroughAccountId;
|
|
98
|
+
if (ctx.input.vendorId) data.vendor_id = ctx.input.vendorId;
|
|
99
|
+
if (ctx.input.description) data.description = ctx.input.description;
|
|
100
|
+
if (ctx.input.referenceNumber) data.reference_number = ctx.input.referenceNumber;
|
|
101
|
+
if (ctx.input.taxId) data.tax_id = ctx.input.taxId;
|
|
102
|
+
if (ctx.input.isBillable !== undefined) data.is_billable = ctx.input.isBillable;
|
|
103
|
+
if (ctx.input.customerId) data.customer_id = ctx.input.customerId;
|
|
104
|
+
if (ctx.input.projectId) data.project_id = ctx.input.projectId;
|
|
105
|
+
if (ctx.input.currencyId) data.currency_id = ctx.input.currencyId;
|
|
106
|
+
return data;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (ctx.input.action === 'create') {
|
|
110
|
+
if (!ctx.input.accountId) throw zohoServiceError('accountId is required for create');
|
|
111
|
+
if (!ctx.input.date) throw zohoServiceError('date is required for create');
|
|
112
|
+
if (ctx.input.amount === undefined)
|
|
113
|
+
throw zohoServiceError('amount is required for create');
|
|
114
|
+
let result = await client.createExpense(buildData());
|
|
115
|
+
let expense = result?.expense;
|
|
116
|
+
return {
|
|
117
|
+
output: { expense, apiMessage: result?.message },
|
|
118
|
+
message: `Created expense of **${expense?.total || ctx.input.amount}** on **${expense?.date || ctx.input.date}**.`
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (ctx.input.action === 'update') {
|
|
123
|
+
if (!ctx.input.expenseId) throw zohoServiceError('expenseId is required for update');
|
|
124
|
+
let result = await client.updateExpense(ctx.input.expenseId, buildData());
|
|
125
|
+
return {
|
|
126
|
+
output: { expense: result?.expense, apiMessage: result?.message },
|
|
127
|
+
message: `Updated expense **${ctx.input.expenseId}**.`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (ctx.input.action === 'delete') {
|
|
132
|
+
if (!ctx.input.expenseId) throw zohoServiceError('expenseId is required for delete');
|
|
133
|
+
let result = await client.deleteExpense(ctx.input.expenseId);
|
|
134
|
+
return {
|
|
135
|
+
output: { deleted: true, apiMessage: result?.message },
|
|
136
|
+
message: `Deleted expense **${ctx.input.expenseId}**.`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw zohoServiceError('Invalid Books expense action.');
|
|
141
|
+
})
|
|
142
|
+
.build();
|