6d935e7180
- Add Redis-backed opportunity cache with background refresh (30s interval) - Fix concurrency bug: use lazy thunks instead of eager promises for batching - Add withCwRetry utility with exponential backoff for transient CW errors - Add adaptive TTL algorithms (primary, sub-resource, products) based on opportunity activity - Add include query param on GET /sales/opportunities/:id (notes,contacts,products) - Add opt-in CW API logger (LOG_CW_API env var) with timestamped files in cw-api-logs/ - Add debug-scripts/analyze-cw-calls.py for API call analysis - Add computeSubResourceCacheTTL and computeProductsCacheTTL algorithms with tests - Increase CW API timeout from 15s to 30s - Unblock cache refresh from startup chain (remove await) - Prioritize recently updated opportunities in refresh cycle - Add CACHING.md documentation - Update API_ROUTES.md with caching details and include param - Update copilot instructions to require CACHING.md sync - Add dev:log script for CW API call logging during development
187 lines
5.0 KiB
TypeScript
187 lines
5.0 KiB
TypeScript
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}`);
|
|
},
|
|
|
|
/**
|
|
* Fetch Activities by Opportunity (Direct)
|
|
*
|
|
* Lightweight single-call variant that skips the count request.
|
|
* Fetches up to 1000 activities in a single GET — sufficient for
|
|
* virtually all opportunities. Used by the background cache refresh
|
|
* to avoid doubling CW API calls.
|
|
*/
|
|
fetchByOpportunityDirect: async (
|
|
opportunityId: number,
|
|
): Promise<CWActivity[]> => {
|
|
const conditions = encodeURIComponent(`opportunity/id=${opportunityId}`);
|
|
const response = await connectWiseApi.get(
|
|
`/sales/activities?pageSize=1000&conditions=${conditions}`,
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
/**
|
|
* 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}`);
|
|
},
|
|
};
|