/**
* client_update Tool
*
* Update an existing client's information.
*/
import { z } from 'zod';
import { ClientUpdateInputSchema, ClientSingleOutputSchema } from './schemas.js';
import { FreshBooksClientWrapper } from '../../client/index.js';
import { ErrorHandler } from '../../errors/error-handler.js';
import { ToolContext } from '../../errors/types.js';
import { logger } from '../../utils/logger.js';
/**
* Tool definition for client_update
*/
export const clientUpdateTool = {
name: 'client_update',
description: `Update an existing client's information in FreshBooks.
WHEN TO USE:
- User wants to modify client details
- User says "update client", "change client info", "edit customer"
- Correcting client contact information
- Updating billing address or preferences
- Changing client status (archive/activate)
REQUIRED:
- accountId: FreshBooks account ID (get from auth_status if not specified)
- clientId: The client ID to update (numeric)
- At least one field to update
UPDATABLE FIELDS:
Contact: fName, lName, organization, email
Phones: busPhone, homePhone, mobPhone, fax
Primary address: pStreet, pStreet2, pCity, pProvince, pCode, pCountry
Secondary address: sStreet, sStreet2, sCity, sProvince, sCode, sCountry
Financial: currencyCode, language, vatNumber, vatName
Preferences: allowLateFees, allowLateNotifications
Status: visState (0=active, 1=deleted, 2=archived)
Notes: note
PARTIAL UPDATES:
Only include fields you want to change. Omitted fields remain unchanged.
RETURNS:
Updated client record with all current information.
EXAMPLES:
- "Update client 123's email to newemail@example.com"
- "Change client 456's address to 789 Oak Street, Boston, MA"
- "Archive client 789 (set visState to 2)"
- "Enable late fees for client 234"`,
inputSchema: ClientUpdateInputSchema,
outputSchema: ClientSingleOutputSchema,
/**
* Execute the tool
*/
async execute(
input: z.infer<typeof ClientUpdateInputSchema>,
client: FreshBooksClientWrapper
): Promise<z.infer<typeof ClientSingleOutputSchema>> {
const handler = ErrorHandler.wrapHandler(
'client_update',
async (
input: z.infer<typeof ClientUpdateInputSchema>,
_context: ToolContext
) => {
const { accountId, clientId, ...updateData } = input;
logger.debug('Updating client', {
accountId,
clientId,
fields: Object.keys(updateData),
});
// Build update object for API (convert camelCase to snake_case)
const updates: Record<string, unknown> = {};
// Contact information
if (updateData.fName !== undefined) updates.fname = updateData.fName;
if (updateData.lName !== undefined) updates.lname = updateData.lName;
if (updateData.organization !== undefined) updates.organization = updateData.organization;
if (updateData.email !== undefined) updates.email = updateData.email;
// Phone numbers
if (updateData.busPhone !== undefined) updates.bus_phone = updateData.busPhone;
if (updateData.homePhone !== undefined) updates.home_phone = updateData.homePhone;
if (updateData.mobPhone !== undefined) updates.mob_phone = updateData.mobPhone;
if (updateData.fax !== undefined) updates.fax = updateData.fax;
// Note
if (updateData.note !== undefined) updates.note = updateData.note;
// Primary address
if (updateData.pStreet !== undefined) updates.p_street = updateData.pStreet;
if (updateData.pStreet2 !== undefined) updates.p_street2 = updateData.pStreet2;
if (updateData.pCity !== undefined) updates.p_city = updateData.pCity;
if (updateData.pProvince !== undefined) updates.p_province = updateData.pProvince;
if (updateData.pCode !== undefined) updates.p_code = updateData.pCode;
if (updateData.pCountry !== undefined) updates.p_country = updateData.pCountry;
// Secondary address
if (updateData.sStreet !== undefined) updates.s_street = updateData.sStreet;
if (updateData.sStreet2 !== undefined) updates.s_street2 = updateData.sStreet2;
if (updateData.sCity !== undefined) updates.s_city = updateData.sCity;
if (updateData.sProvince !== undefined) updates.s_province = updateData.sProvince;
if (updateData.sCode !== undefined) updates.s_code = updateData.sCode;
if (updateData.sCountry !== undefined) updates.s_country = updateData.sCountry;
// Financial settings
if (updateData.currencyCode !== undefined) updates.currency_code = updateData.currencyCode;
if (updateData.language !== undefined) updates.language = updateData.language;
if (updateData.vatNumber !== undefined) updates.vat_number = updateData.vatNumber;
if (updateData.vatName !== undefined) updates.vat_name = updateData.vatName;
// Billing preferences
if (updateData.allowLateFees !== undefined) updates.allow_late_fees = updateData.allowLateFees;
if (updateData.allowLateNotifications !== undefined) {
updates.allow_late_notifications = updateData.allowLateNotifications;
}
// Status
if (updateData.visState !== undefined) updates.vis_state = updateData.visState;
const result = await client.executeWithRetry(
'client_update',
async (fbClient) => {
const response = await fbClient.clients.update(updates, accountId, String(clientId));
if (!response.ok) {
throw response.error;
}
return response.data;
}
);
// FreshBooks returns: { client: { ... } }
const updatedClient = (result as { client?: unknown }).client ?? result;
logger.info('Client updated successfully', {
clientId,
});
return updatedClient as z.infer<typeof ClientSingleOutputSchema>;
}
);
return handler(input, { accountId: input.accountId });
},
};