Skip to content
Closed
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
5 changes: 1 addition & 4 deletions src/client/AutotaskClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,7 @@ export class AutotaskClient {
Secret: config.secret,
},
transformRequest: [
(data, headers) => {
if (defaultPerformanceConfig.enableCompression && data) {
headers!['Content-Encoding'] = 'gzip';
}
(data, _headers) => {
return JSON.stringify(data);
},
],
Expand Down
154 changes: 135 additions & 19 deletions src/client/sub-clients/CoreClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ import {
/**
* CoreClient handles the primary business entities:
* - Companies (Organizations)
* - Contacts (Individuals within companies)
* - Contacts (Individuals within companies)
* - Tickets (Service tickets and support requests)
* - Projects (Client projects and work orders)
* - Tasks (Project tasks and work items)
* - Opportunities (Sales opportunities and pipeline)
* - Resources (Human resources and staff members)
*
*
* Plus all related attachments, notes, and extended functionality.
*/
export class CoreClient extends BaseSubClient {
Expand Down Expand Up @@ -148,35 +148,72 @@ export class CoreClient extends BaseSubClient {
this.projectAttachments = new ProjectAttachments(this.axios, this.logger);
this.taskAttachments = new TaskAttachments(this.axios, this.logger);
this.resourceAttachments = new ResourceAttachments(this.axios, this.logger);
this.opportunityAttachments = new OpportunityAttachments(this.axios, this.logger);
this.opportunityAttachments = new OpportunityAttachments(
this.axios,
this.logger
);

// Core notes
this.companyNotes = new CompanyNotes(this.axios, this.logger);
this.ticketNotes = new TicketNotes(this.axios, this.logger);
this.projectNotes = new ProjectNotes(this.axios, this.logger);
this.taskNotes = new TaskNotes(this.axios, this.logger);
this.companyNoteAttachments = new CompanyNoteAttachments(this.axios, this.logger);
this.ticketNoteAttachments = new TicketNoteAttachments(this.axios, this.logger);
this.projectNoteAttachments = new ProjectNoteAttachments(this.axios, this.logger);
this.companyNoteAttachments = new CompanyNoteAttachments(
this.axios,
this.logger
);
this.ticketNoteAttachments = new TicketNoteAttachments(
this.axios,
this.logger
);
this.projectNoteAttachments = new ProjectNoteAttachments(
this.axios,
this.logger
);
this.taskNoteAttachments = new TaskNoteAttachments(this.axios, this.logger);

// Extended ticket entities
this.ticketCategories = new TicketCategories(this.axios, this.logger);
this.ticketCategoryFieldDefaults = new TicketCategoryFieldDefaults(this.axios, this.logger);
this.ticketAdditionalConfigurationItems = new TicketAdditionalConfigurationItems(this.axios, this.logger);
this.ticketAdditionalContacts = new TicketAdditionalContacts(this.axios, this.logger);
this.ticketChangeRequestApprovals = new TicketChangeRequestApprovals(this.axios, this.logger);
this.ticketCategoryFieldDefaults = new TicketCategoryFieldDefaults(
this.axios,
this.logger
);
this.ticketAdditionalConfigurationItems =
new TicketAdditionalConfigurationItems(this.axios, this.logger);
this.ticketAdditionalContacts = new TicketAdditionalContacts(
this.axios,
this.logger
);
this.ticketChangeRequestApprovals = new TicketChangeRequestApprovals(
this.axios,
this.logger
);
this.ticketCharges = new TicketCharges(this.axios, this.logger);
this.ticketChecklistItems = new TicketChecklistItems(this.axios, this.logger);
this.ticketChecklistLibraries = new TicketChecklistLibraries(this.axios, this.logger);
this.ticketChecklistItems = new TicketChecklistItems(
this.axios,
this.logger
);
this.ticketChecklistLibraries = new TicketChecklistLibraries(
this.axios,
this.logger
);
this.ticketHistory = new TicketHistory(this.axios, this.logger);
this.ticketRmaCredits = new TicketRmaCredits(this.axios, this.logger);
this.ticketSecondaryResources = new TicketSecondaryResources(this.axios, this.logger);
this.ticketTagAssociations = new TicketTagAssociations(this.axios, this.logger);
this.ticketSecondaryResources = new TicketSecondaryResources(
this.axios,
this.logger
);
this.ticketTagAssociations = new TicketTagAssociations(
this.axios,
this.logger
);

// Extended task entities
this.taskPredecessors = new TaskPredecessors(this.axios, this.logger);
this.taskSecondaryResources = new TaskSecondaryResources(this.axios, this.logger);
this.taskSecondaryResources = new TaskSecondaryResources(
this.axios,
this.logger
);

// Extended project entities
this.projectCharges = new ProjectCharges(this.axios, this.logger);
Expand All @@ -185,14 +222,21 @@ export class CoreClient extends BaseSubClient {
this.companyAlerts = new CompanyAlerts(this.axios, this.logger);
this.companyCategories = new CompanyCategories(this.axios, this.logger);
this.companyLocations = new CompanyLocations(this.axios, this.logger);
this.companySiteConfigurations = new CompanySiteConfigurations(this.axios, this.logger);
this.companySiteConfigurations = new CompanySiteConfigurations(
this.axios,
this.logger
);
this.companyTeams = new CompanyTeams(this.axios, this.logger);
this.companyToDos = new CompanyToDos(this.axios, this.logger);

// Extended contact entities
this.contactBillingProductAssociations = new ContactBillingProductAssociations(this.axios, this.logger);
this.contactBillingProductAssociations =
new ContactBillingProductAssociations(this.axios, this.logger);
this.contactGroups = new ContactGroups(this.axios, this.logger);
this.contactGroupContacts = new ContactGroupContacts(this.axios, this.logger);
this.contactGroupContacts = new ContactGroupContacts(
this.axios,
this.logger
);
}

getName(): string {
Expand Down Expand Up @@ -540,4 +584,76 @@ export class CoreClient extends BaseSubClient {
sort: 'projectName asc',
});
}
}

/**
* Search resources (users/technicians) by name or email
* @param query - Search query string
* @param searchFields - Fields to search in (default: ['firstName', 'lastName', 'email'])
* @param pageSize - Number of records to return (default: 100)
* @returns Promise with matching resources
*/
async searchResources(
query: string,
searchFields: string[] = ['firstName', 'lastName', 'email'],
pageSize: number = 100
) {
const filters = searchFields.map(field => ({
op: 'contains',
field,
value: query,
}));

return this.resources.list({
filter: filters.length === 1 ? filters : [{ op: 'or', items: filters }],
pageSize,
sort: 'lastName asc',
});
}

/**
* Resolve a resource by full name (e.g., "Will Spence").
* Splits the name into first/last parts and searches accordingly.
* @param name - Full name of the resource (e.g., "Will Spence")
* @returns The matched resource, or null if not found
* @throws Error if multiple resources match (ambiguous)
*/
async resolveResourceByName(name: string): Promise<{
id: number;
firstName: string;
lastName: string;
email: string;
[key: string]: any;
} | null> {
const nameParts = name.trim().split(/\s+/);
let resources: any[];

if (nameParts.length >= 2) {
// Search by last name (more unique), then filter by first name
const firstName = nameParts[0];
const lastName = nameParts.slice(1).join(' ');
const result = await this.searchResources(lastName, ['lastName']);
resources = ((result.data as any[]) || []).filter((r: any) =>
r.firstName?.toLowerCase().includes(firstName.toLowerCase())
);
// Fall back to full string search if no match
if (resources.length === 0) {
const fallback = await this.searchResources(name);
resources = (fallback.data as any[]) || [];
}
} else {
const result = await this.searchResources(name);
resources = (result.data as any[]) || [];
}

if (resources.length === 0) return null;
if (resources.length > 1) {
const names = resources
.map((r: any) => `${r.firstName} ${r.lastName} (ID: ${r.id})`)
.join(', ');
throw new Error(
`Multiple resources found matching "${name}": ${names}. Please be more specific.`
);
}
return resources[0];
}
}
109 changes: 98 additions & 11 deletions src/client/sub-clients/FinancialClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,18 @@ export class FinancialClient extends BaseSubClient {
// Billing entities
this.billingCodes = new BillingCodes(this.axios, this.logger);
this.billingItems = new BillingItems(this.axios, this.logger);
this.billingItemApprovalLevels = new BillingItemApprovalLevels(this.axios, this.logger);
this.billingItemApprovalLevels = new BillingItemApprovalLevels(
this.axios,
this.logger
);

// Invoice entities
this.invoices = new Invoices(this.axios, this.logger);
this.invoiceTemplates = new InvoiceTemplates(this.axios, this.logger);
this.additionalInvoiceFieldValues = new AdditionalInvoiceFieldValues(this.axios, this.logger);
this.additionalInvoiceFieldValues = new AdditionalInvoiceFieldValues(
this.axios,
this.logger
);

// Quote entities
this.quotes = new Quotes(this.axios, this.logger);
Expand All @@ -132,18 +138,30 @@ export class FinancialClient extends BaseSubClient {
// Purchase entities
this.purchaseOrders = new PurchaseOrders(this.axios, this.logger);
this.purchaseOrderItems = new PurchaseOrderItems(this.axios, this.logger);
this.purchaseOrderItemReceiving = new PurchaseOrderItemReceiving(this.axios, this.logger);
this.purchaseOrderItemReceiving = new PurchaseOrderItemReceiving(
this.axios,
this.logger
);
this.purchaseApprovals = new PurchaseApprovals(this.axios, this.logger);

// Sales entities
this.salesOrders = new SalesOrders(this.axios, this.logger);
this.salesOrderAttachments = new SalesOrderAttachments(this.axios, this.logger);
this.salesOrderAttachments = new SalesOrderAttachments(
this.axios,
this.logger
);

// Expense entities
this.expenseItems = new ExpenseItems(this.axios, this.logger);
this.expenseReports = new ExpenseReports(this.axios, this.logger);
this.expenseItemAttachments = new ExpenseItemAttachments(this.axios, this.logger);
this.expenseReportAttachments = new ExpenseReportAttachments(this.axios, this.logger);
this.expenseItemAttachments = new ExpenseItemAttachments(
this.axios,
this.logger
);
this.expenseReportAttachments = new ExpenseReportAttachments(
this.axios,
this.logger
);

// Order and change entities
this.changeOrderCharges = new ChangeOrderCharges(this.axios, this.logger);
Expand All @@ -158,13 +176,25 @@ export class FinancialClient extends BaseSubClient {
this.paymentTerms = new PaymentTerms(this.axios, this.logger);

// Pricing entities
this.priceListMaterialCodes = new PriceListMaterialCodes(this.axios, this.logger);
this.priceListMaterialCodes = new PriceListMaterialCodes(
this.axios,
this.logger
);
this.priceListProducts = new PriceListProducts(this.axios, this.logger);
this.priceListProductTiers = new PriceListProductTiers(this.axios, this.logger);
this.priceListProductTiers = new PriceListProductTiers(
this.axios,
this.logger
);
this.priceListRoles = new PriceListRoles(this.axios, this.logger);
this.priceListServices = new PriceListServices(this.axios, this.logger);
this.priceListServiceBundles = new PriceListServiceBundles(this.axios, this.logger);
this.priceListWorkTypeModifiers = new PriceListWorkTypeModifiers(this.axios, this.logger);
this.priceListServiceBundles = new PriceListServiceBundles(
this.axios,
this.logger
);
this.priceListWorkTypeModifiers = new PriceListWorkTypeModifiers(
this.axios,
this.logger
);
}

getName(): string {
Expand Down Expand Up @@ -443,4 +473,61 @@ export class FinancialClient extends BaseSubClient {
pageSize,
});
}
}

/**
* Get internal billing codes (useType=3) used for Regular Time entries.
* These represent categories like Internal Meeting, Training, PTO, etc.
* @param pageSize - Number of records to return (default: 500)
* @returns Promise with internal billing codes
*/
async getInternalBillingCodes(pageSize: number = 500) {
return this.billingCodes.list({
filter: [
{ op: 'eq', field: 'isActive', value: true },
{ op: 'eq', field: 'useType', value: 3 },
],
pageSize,
});
}

/**
* Resolve an internal billing code by name for Regular Time entries.
* Searches BillingCodes with useType=3 (internal allocation codes).
* @param name - Category name (e.g., "Internal Meeting", "Training", "PTO")
* @returns The matched billing code, or null if not found
* @throws Error if multiple billing codes match (ambiguous)
*/
async resolveInternalBillingCodeByName(name: string): Promise<{
id: number;
name: string;
[key: string]: any;
} | null> {
const result = await this.getInternalBillingCodes();
const codes = (result.data as any[]) || [];
const searchName = name.toLowerCase();

// Try exact match first
let match = codes.find((bc: any) => bc.name?.toLowerCase() === searchName);

// Then try contains match
if (!match) {
const containsMatches = codes.filter(
(bc: any) =>
bc.name?.toLowerCase().includes(searchName) ||
searchName.includes(bc.name?.toLowerCase())
);
if (containsMatches.length === 1) {
match = containsMatches[0];
} else if (containsMatches.length > 1) {
const names = containsMatches
.map((bc: any) => `${bc.name} (ID: ${bc.id})`)
.join(', ');
throw new Error(
`Multiple billing codes match "${name}": ${names}. Please be more specific.`
);
}
}

return match || null;
}
}
Loading