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(); /** * 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) => { 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 > => { 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} Resolved member info */ export const resolveMember = async ( identifier: string, ): Promise => { 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, }; };