d7b374f8ab
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
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
import { Collection } from "@discordjs/collection";
|
|
import { prisma } from "../../../constants";
|
|
import { redis } from "../../../constants";
|
|
import { CWMember } from "./fetchAllMembers";
|
|
|
|
const REDIS_KEY = "cw:members";
|
|
|
|
export interface ResolvedMember {
|
|
/** Local database user ID (null if no matching local user) */
|
|
id: string | null;
|
|
/** CW member identifier (e.g. "jroberts") */
|
|
identifier: string;
|
|
/** Full name resolved from CW member cache, or raw identifier as fallback */
|
|
name: string;
|
|
/** ConnectWise member ID */
|
|
cwMemberId: number | null;
|
|
}
|
|
|
|
/**
|
|
* CW Member Cache
|
|
*
|
|
* Dual-layer cache (in-memory + Redis) of ConnectWise members keyed by
|
|
* their identifier (e.g. "jroberts"). Populated by `refreshCwIdentifiers`
|
|
* on startup and every 30 minutes thereafter.
|
|
*/
|
|
let memberCache = new Collection<string, CWMember>();
|
|
|
|
/**
|
|
* Set the member cache contents.
|
|
*
|
|
* Replaces both the in-memory Collection and the Redis snapshot.
|
|
*
|
|
* @param members - Collection of CW members keyed by identifier
|
|
*/
|
|
export const setMemberCache = async (members: Collection<string, CWMember>) => {
|
|
memberCache = members;
|
|
await redis.set(REDIS_KEY, JSON.stringify([...members.values()]));
|
|
};
|
|
|
|
/**
|
|
* Get the current member cache.
|
|
*
|
|
* Returns the in-memory Collection. If empty, attempts to hydrate from Redis
|
|
* first. Returns whatever is available (may be empty if Redis is also cold).
|
|
*/
|
|
export const getMemberCache = async (): Promise<
|
|
Collection<string, CWMember>
|
|
> => {
|
|
if (memberCache.size > 0) return memberCache;
|
|
|
|
const stored = await redis.get(REDIS_KEY);
|
|
if (stored) {
|
|
const parsed: CWMember[] = JSON.parse(stored);
|
|
memberCache = new Collection(parsed.map((m) => [m.identifier, m]));
|
|
}
|
|
|
|
return memberCache;
|
|
};
|
|
|
|
/**
|
|
* Resolve CW Identifier to Full Name
|
|
*
|
|
* Looks up a ConnectWise member by their identifier in the in-memory cache
|
|
* and returns their full name. Falls back to the raw identifier if not found.
|
|
*
|
|
* @param identifier - The CW member identifier (e.g. "jroberts")
|
|
* @returns The member's full name (e.g. "John Roberts") or the raw identifier
|
|
*/
|
|
export const resolveMemberName = (identifier: string): string => {
|
|
const member = memberCache.get(identifier);
|
|
if (!member) return identifier;
|
|
return `${member.firstName} ${member.lastName}`.trim() || identifier;
|
|
};
|
|
|
|
/**
|
|
* Resolve CW Identifier to Full Member Info
|
|
*
|
|
* Looks up a ConnectWise member by their identifier in the in-memory cache
|
|
* and cross-references with the local database to return a complete member
|
|
* reference including local user ID, CW identifier, full name, and CW member ID.
|
|
*
|
|
* @param identifier - The CW member identifier (e.g. "jroberts")
|
|
* @returns {Promise<ResolvedMember>} Resolved member info
|
|
*/
|
|
export const resolveMember = async (
|
|
identifier: string,
|
|
): Promise<ResolvedMember> => {
|
|
const cwMember = memberCache.get(identifier);
|
|
const name = cwMember
|
|
? `${cwMember.firstName} ${cwMember.lastName}`.trim() || identifier
|
|
: identifier;
|
|
|
|
const localUser = await prisma.user.findFirst({
|
|
where: { cwIdentifier: identifier },
|
|
select: { id: true },
|
|
});
|
|
|
|
return {
|
|
id: localUser?.id ?? null,
|
|
identifier,
|
|
name,
|
|
cwMemberId: cwMember?.id ?? null,
|
|
};
|
|
};
|