seahaven-door-unlock-api/lambda/blf-sync/blf-sync-handler.ts

193 lines
5.2 KiB
TypeScript
Raw Permalink Normal View History

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<string> {
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<SyncUser>();
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;
}