import { Collection } from "@discordjs/collection"; import { connectWiseApi } from "../../../constants"; import { runCollector } from "../../collector-client/runCollector"; export interface CWMember { id: number; identifier: string; firstName: string; lastName: string; officeEmail: string; inactiveFlag: boolean; _info: Record; } interface CollectorMemberRecord { memberRecId: number; memberId: string; firstName: string | null; lastName: string | null; emailAddress: string | null; deleteFlag: boolean; lastUpdateUtc?: string | null; lastUpdate?: string | null; _info?: Record; } const isCollectorMemberRecord = ( value: unknown, ): value is CollectorMemberRecord => { if (!value || typeof value !== "object") { return false; } const candidate = value as Partial; return ( typeof candidate.memberRecId === "number" && typeof candidate.memberId === "string" ); }; const normalizeCollectorMember = ( member: CollectorMemberRecord, ): CWMember => { const updatedAt = member.lastUpdateUtc ?? member.lastUpdate ?? ""; return { id: member.memberRecId, identifier: member.memberId, firstName: member.firstName ?? "", lastName: member.lastName ?? "", officeEmail: member.emailAddress ?? "", inactiveFlag: Boolean(member.deleteFlag), _info: member._info ?? { lastUpdated: updatedAt }, }; }; /** * Fetch All CW Members * * Fetches every member from ConnectWise using pagination and returns them * in a Collection keyed by their identifier (e.g. "jroberts"). * * @param opts.conditions - Optional CW conditions string to filter members * @returns {Promise>} Collection of CW members keyed by identifier */ export const fetchAllCwMembers = async (opts?: { conditions?: string; }): Promise> => { if (!opts?.conditions) { try { const collectorMembers = await runCollector("fetchMembers"); if (!Array.isArray(collectorMembers)) { throw new Error("Collector payload was not an array"); } const members = new Collection(); for (const member of collectorMembers) { if (!isCollectorMemberRecord(member)) { continue; } const normalized = normalizeCollectorMember(member); members.set(normalized.identifier, normalized); } if (members.size > 0) { console.log( `[fetchAllCwMembers] Using collector data from fetchMembers (${members.size} members)`, ); return members; } throw new Error("Collector payload did not contain valid member records"); } catch (err) { console.warn( `[fetchAllCwMembers] Collector fetchMembers failed, falling back to CW API: ${err instanceof Error ? err.message : String(err)}`, ); } } const members = new Collection(); const pageSize = 1000; const conditionsParam = opts?.conditions ? `&conditions=${encodeURIComponent(opts.conditions)}` : ""; const { data: countData } = await connectWiseApi.get( `/system/members/count${conditionsParam ? `?${conditionsParam.slice(1)}` : ""}`, ); const totalPages = Math.ceil(countData.count / pageSize); for (let page = 0; page < totalPages; page++) { const { data } = await connectWiseApi.get( `/system/members?page=${page + 1}&pageSize=${pageSize}${conditionsParam}`, ); for (const member of data) { members.set(member.identifier, member); } } return members; }; /** * Find CW Member Identifier by Email * * Looks up a ConnectWise member whose `officeEmail` matches the provided * email address (case-insensitive) and returns their `identifier` string * (e.g. "jroberts"). Returns `null` if no match is found. * * @param email - The email address to search for * @param members - Optional pre-fetched member collection to search against (avoids extra API call) * @returns {Promise} The CW identifier or null */ export const findCwIdentifierByEmail = async ( email: string, members?: Collection, ): Promise => { const allMembers = members ?? (await fetchAllCwMembers()); const normalised = email.toLowerCase(); const match = allMembers.find( (m) => m.officeEmail?.toLowerCase() === normalised, ); return match?.identifier ?? null; };