mirror of
https://github.com/Sea-Haven-Industries/seahaven-door-unlock-api.git
synced 2026-09-30 05:53:12 +00:00
193 lines
5.2 KiB
TypeScript
193 lines
5.2 KiB
TypeScript
|
|
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;
|
||
|
|
}
|