feat: sales activities, forecast products, catalog categories, member cache, procurement filters, and comprehensive tests
New features: - ActivityController and manager for CW sales activities (CRUD) - ForecastProductController for opportunity forecast/product lines - CW member cache with dual-layer (in-memory + Redis) resolution - Catalog category/subcategory/ecosystem taxonomy module - Quote statuses type definitions with CW mapping - User-defined fields (UDF) module with cache and event refresh - Company sites CW module with serialization - Procurement manager filters (category, ecosystem, manufacturer, price, stock) - Opportunity notes CRUD and product line management via CW API - Opportunity type definitions endpoint Updates: - OpportunityController: CW refresh, company hydration, activities, custom fields - UserController: cwIdentifier field for CW member linking - CatalogItemController: category/subcategory fields from CW - PermissionNodes: sales note/product CRUD nodes, subCategories, collectPermissions - API routes: procurement categories/filters, sales notes/products, opportunity types - Global events: UDF and member refresh intervals on startup Tests (414 passing): - ActivityController, ForecastProductController, OpportunityController unit tests - UserController cwIdentifier tests - catalogCategories, companySites, memberCache, procurement module tests - activityTypes, opportunityTypes, quoteStatuses type tests - permissionNodes subCategories and getAllPermissionNodes tests - Updated test setup with redis mock, API method mocks, and builder helpers
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import { Collection } from "@discordjs/collection";
|
||||
import { connectWiseApi } from "../../../constants";
|
||||
import {
|
||||
CWActivity,
|
||||
CWActivitySummary,
|
||||
CWCreateActivity,
|
||||
CWPatchOperation,
|
||||
} from "./activity.types";
|
||||
|
||||
export const activityCw = {
|
||||
/**
|
||||
* Count Activities
|
||||
*
|
||||
* Returns the total number of activities in ConnectWise.
|
||||
* Optionally accepts CW conditions string for filtered counts.
|
||||
*/
|
||||
countItems: async (conditions?: string): Promise<number> => {
|
||||
const query = conditions
|
||||
? `/sales/activities/count?conditions=${encodeURIComponent(conditions)}`
|
||||
: "/sales/activities/count";
|
||||
const response = await connectWiseApi.get(query);
|
||||
return response.data.count;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch All Activity Summaries
|
||||
*
|
||||
* Lightweight fetch returning only id and _info (for lastUpdated comparison).
|
||||
* Paginates through all activities.
|
||||
*/
|
||||
fetchAllSummaries: async (): Promise<
|
||||
Collection<number, CWActivitySummary>
|
||||
> => {
|
||||
const allItems = new Collection<number, CWActivitySummary>();
|
||||
const pageSize = 1000;
|
||||
|
||||
const count = await activityCw.countItems();
|
||||
const totalPages = Math.ceil(count / pageSize);
|
||||
|
||||
for (let page = 0; page < totalPages; page++) {
|
||||
const response = await connectWiseApi.get(
|
||||
`/sales/activities?page=${page + 1}&pageSize=${pageSize}&fields=id,_info`,
|
||||
);
|
||||
const items: CWActivitySummary[] = response.data;
|
||||
|
||||
for (const item of items) {
|
||||
allItems.set(item.id, item);
|
||||
}
|
||||
}
|
||||
|
||||
return allItems;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch All Activities (Full)
|
||||
*
|
||||
* Fetches all activities with complete data. Paginates through
|
||||
* the full list. Optionally accepts CW conditions string for filtering.
|
||||
*/
|
||||
fetchAll: async (
|
||||
conditions?: string,
|
||||
): Promise<Collection<number, CWActivity>> => {
|
||||
const allItems = new Collection<number, CWActivity>();
|
||||
const pageSize = 1000;
|
||||
|
||||
const count = await activityCw.countItems(conditions);
|
||||
const totalPages = Math.ceil(count / pageSize);
|
||||
|
||||
for (let page = 0; page < totalPages; page++) {
|
||||
const conditionsParam = conditions
|
||||
? `&conditions=${encodeURIComponent(conditions)}`
|
||||
: "";
|
||||
const response = await connectWiseApi.get(
|
||||
`/sales/activities?page=${page + 1}&pageSize=${pageSize}${conditionsParam}`,
|
||||
);
|
||||
const items: CWActivity[] = response.data;
|
||||
|
||||
for (const item of items) {
|
||||
allItems.set(item.id, item);
|
||||
}
|
||||
}
|
||||
|
||||
return allItems;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Single Activity
|
||||
*
|
||||
* Fetches a single activity by its ConnectWise ID.
|
||||
*/
|
||||
fetch: async (id: number): Promise<CWActivity> => {
|
||||
const response = await connectWiseApi.get(`/sales/activities/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Activities by Company
|
||||
*
|
||||
* Fetches all activities associated with a specific ConnectWise company ID.
|
||||
*/
|
||||
fetchByCompany: async (
|
||||
cwCompanyId: number,
|
||||
): Promise<Collection<number, CWActivity>> => {
|
||||
return activityCw.fetchAll(`company/id=${cwCompanyId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Activities by Opportunity
|
||||
*
|
||||
* Fetches all activities associated with a specific opportunity ID.
|
||||
*/
|
||||
fetchByOpportunity: async (
|
||||
opportunityId: number,
|
||||
): Promise<Collection<number, CWActivity>> => {
|
||||
return activityCw.fetchAll(`opportunity/id=${opportunityId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create Activity
|
||||
*
|
||||
* Creates a new activity in ConnectWise.
|
||||
*/
|
||||
create: async (activity: CWCreateActivity): Promise<CWActivity> => {
|
||||
const response = await connectWiseApi.post("/sales/activities", activity);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update Activity (PATCH)
|
||||
*
|
||||
* Updates an existing activity using JSON Patch operations.
|
||||
*/
|
||||
update: async (
|
||||
id: number,
|
||||
operations: CWPatchOperation[],
|
||||
): Promise<CWActivity> => {
|
||||
const response = await connectWiseApi.patch(
|
||||
`/sales/activities/${id}`,
|
||||
operations,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace Activity (PUT)
|
||||
*
|
||||
* Replaces an entire activity record in ConnectWise.
|
||||
*/
|
||||
replace: async (
|
||||
id: number,
|
||||
activity: CWCreateActivity,
|
||||
): Promise<CWActivity> => {
|
||||
const response = await connectWiseApi.put(
|
||||
`/sales/activities/${id}`,
|
||||
activity,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete Activity
|
||||
*
|
||||
* Deletes an activity by its ConnectWise ID.
|
||||
*/
|
||||
delete: async (id: number): Promise<void> => {
|
||||
await connectWiseApi.delete(`/sales/activities/${id}`);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user