import { SecretsManagerClient, GetSecretValueCommand, } from "@aws-sdk/client-secrets-manager"; import { ALWAYS_INCLUDE_EXTENSIONS, addAlwaysIncludedColleagues, groupUsersByDepartment, isEligibleColleague, primaryDepartmentName, splitPeerName, type ColleagueRef, type SyncUser, } from "./department"; import { mergeDepartmentBlfs } from "./merge"; import { resolveTemplateId, SLOT_CONTRACT } from "./slot-contract"; import { ThreeCxClient } from "./three-cx-client"; const secrets = new SecretsManagerClient({}); export interface BlfSyncEvent { dryRun?: boolean; smokeExtension?: string; } function parseSecretString(raw: string | undefined): string { if (!raw) { throw new Error("empty secret"); } try { const parsed = JSON.parse(raw) as unknown; return typeof parsed === "string" ? parsed : raw; } catch { return raw; } } async function readSecret(name: string): Promise { const res = await secrets.send(new GetSecretValueCommand({ SecretId: name })); return parseSecretString(res.SecretString); } function resolveTemplate(user: SyncUser) { const phones = user.Phones ?? []; for (const phone of phones) { const id = resolveTemplateId(phone.TemplateName, phone.Name); if (id) { return id; } } return undefined; } export async function handler(event: BlfSyncEvent = {}) { const dryRun = event.dryRun ?? process.env.DRY_RUN !== "false"; const smokeExtension = (event.smokeExtension ?? process.env.SMOKE_EXTENSION ?? "").trim(); const domain = await readSecret(process.env.THREE_CX_DOMAIN_SECRET!); const clientId = await readSecret(process.env.THREE_CX_CLIENT_ID_SECRET!); const clientSecret = await readSecret(process.env.THREE_CX_CLIENT_SECRET_SECRET!); const client = new ThreeCxClient({ domain, clientId, clientSecret }); const users = await client.listUsers(); const byDept = groupUsersByDepartment(users); const extraColleagues: ColleagueRef[] = []; for (const number of ALWAYS_INCLUDE_EXTENSIONS) { const already = users.find((u) => u.Number === number); if (already && isEligibleColleague(already)) { extraColleagues.push({ id: already.Id, number: already.Number, firstName: already.FirstName ?? "", lastName: already.LastName ?? "", }); continue; } const peer = await client.getPeerByNumber(number); if (!peer) { console.log(JSON.stringify({ action: "blf_sync_skip_extra", reason: "peer_not_found", extension: number })); continue; } const names = splitPeerName(peer.Name); extraColleagues.push({ id: peer.Id, number: peer.Number, firstName: names.firstName, lastName: names.lastName, }); } let patched = 0; let unchanged = 0; let skipped = 0; let failed = 0; for (const user of users) { const templateId = resolveTemplate(user); if (!templateId) { skipped += 1; console.log(JSON.stringify({ action: "blf_sync_skip", reason: "unknown_template", extension: user.Number, })); continue; } const dept = primaryDepartmentName(user); if (!dept) { skipped += 1; console.log(JSON.stringify({ action: "blf_sync_skip", reason: "no_department", extension: user.Number, })); continue; } const colleagues = addAlwaysIncludedColleagues( (byDept.get(dept) ?? []) .filter(isEligibleColleague) .map((u) => ({ id: u.Id, number: u.Number, firstName: u.FirstName ?? "", lastName: u.LastName ?? "", })), extraColleagues ); const result = mergeDepartmentBlfs({ currentXml: user.Blfs, selfExtension: user.Number, colleagues, contract: SLOT_CONTRACT[templateId], }); const logBase = { extension: user.Number, department: dept, templateId, placed: result.placed, overflow: result.overflow, changed: result.changed, }; if (!result.changed) { unchanged += 1; console.log(JSON.stringify({ action: "blf_sync_unchanged", ...logBase })); continue; } if (dryRun || (smokeExtension && user.Number !== smokeExtension)) { console.log(JSON.stringify({ action: dryRun ? "blf_sync_dry_run" : "blf_sync_skipped_not_smoke", ...logBase, proposedBlfs: result.xml, })); continue; } const write = await client.patchUserBlfs(user.Id, result.xml); if (write.status >= 200 && write.status < 300) { patched += 1; console.log(JSON.stringify({ action: "blf_sync_patched", ...logBase, status: write.status })); } else { failed += 1; console.error(JSON.stringify({ action: "blf_sync_patch_failed", ...logBase, status: write.status, })); } } const summary = { action: "blf_sync_complete", dryRun, smokeExtension: smokeExtension || undefined, visibleUsers: users.length, departments: [...byDept.keys()], patched, unchanged, skipped, failed, }; console.log(JSON.stringify(summary)); if (failed > 0) { throw new Error(`blf sync patch failed for ${failed} user(s)`); } return summary; }