import { ClientConfigDoc, getDb, findClientByPhoneNumber, Products, KNOWN_PRODUCT_KEYS, KnownProductKey, } from "../index"; import { Collection, UpdateFilter } from "mongodb"; import { resetClientQuotaSettings } from "../clients/clients.quota.getters"; import { MAX_ACTIVE_SCANS } from "./clientsConfig.constants"; export const getClientsConfigCollection = (): Collection => { return getDb().collection("clientsConfig"); }; export const getClientConfig = (clientId: string) => { return getClientsConfigCollection().findOne({ clientId }); }; export const getClientDisplayName = async ( clientId: string, ): Promise => { const doc = await getClientsConfigCollection().findOne( { clientId }, { projection: { "quota.clientDisplayName": 1, _id: 0 } }, ); const name = doc?.quota?.clientDisplayName?.trim(); return name || "-"; }; export const getClientConfigByPhone = async (phone: string) => { const client = await findClientByPhoneNumber(phone); if (!client) return null; return getClientConfig(client.clientId); }; export const createClientConfigDoc = async ( clientConfig: ClientConfigDoc, ): Promise => { await getClientsConfigCollection().insertOne(clientConfig); }; export async function updateProductConfig( clientId: string, productKey: K, updates: Partial, ): Promise { if (!KNOWN_PRODUCT_KEYS.includes(productKey as KnownProductKey)) { throw new Error( `Invalid product key: "${productKey}". Must be one of: ${KNOWN_PRODUCT_KEYS.join(", ")}`, ); } const setOperations: Record = {}; for (const [key, value] of Object.entries(updates)) { setOperations[`products.${productKey}.${key}`] = value; } const result = await getClientsConfigCollection().updateOne( { clientId }, { $set: setOperations }, ); if (result.matchedCount === 0) { throw new Error(`No client config found for clientId: ${clientId}`); } } export const getActiveScanId = async ( clientId: string, ): Promise => { const cfg = await getClientConfig(clientId); return cfg?.products?.websiteTalk?.activeScanId; }; /** * Returns the client's active scan ids as a list. * Falls back to wrapping the legacy singular `activeScanId` field when * `activeScanIds` is absent — lets clients on the old field keep working * without a data migration. */ export const getActiveScanIds = async (clientId: string): Promise => { const cfg = await getClientConfig(clientId); const list = cfg?.products?.websiteTalk?.activeScanIds; if (list && list.length > 0) return list; const legacy = cfg?.products?.websiteTalk?.activeScanId; return legacy ? [legacy] : []; }; /** * @deprecated Use `addActiveScanId` instead — supports multiple simultaneously * active scans. This function overwrites the entire single-value field. */ export const updateActiveScanId = async ( clientId: string, scanId: string, ): Promise => { await updateProductConfig(clientId, "websiteTalk", { activeScanId: scanId }); }; /** * Adds a scan id to the client's active list. Writes the full list back * (rather than a bare array append) so a client still on the legacy * singular field gets migrated instead of silently losing it. */ export const addActiveScanId = async ( clientId: string, scanId: string, ): Promise => { const current = await getActiveScanIds(clientId); if (!current.includes(scanId) && current.length >= MAX_ACTIVE_SCANS) { throw new Error( `Client ${clientId} already has the maximum of ${MAX_ACTIVE_SCANS} active scans`, ); } const next = current.includes(scanId) ? current : [...current, scanId]; const result = await getClientsConfigCollection().updateOne( { clientId }, { $set: { "products.websiteTalk.activeScanIds": next }, $unset: { "products.websiteTalk.activeScanId": "" }, }, ); if (result.matchedCount === 0) { throw new Error(`No client config found for clientId: ${clientId}`); } }; /** * @deprecated Use `removeActiveScanId` instead — removes a single scan from * the active list without clearing the others. */ export const clearActiveScanId = async (clientId: string): Promise => { const result = await getClientsConfigCollection().updateOne( { clientId }, { $unset: { "products.websiteTalk.activeScanId": "" } }, ); if (result.matchedCount === 0) { throw new Error(`No client config found for clientId: ${clientId}`); } }; /** * Removes a single scan id from the active list. Also clears the legacy * singular field when it matches, so a not-yet-migrated client's removal * actually takes effect instead of silently no-op'ing. */ export const removeActiveScanId = async ( clientId: string, scanId: string, ): Promise => { const cfg = await getClientConfig(clientId); if (!cfg) { throw new Error(`No client config found for clientId: ${clientId}`); } const updateOps: UpdateFilter = { $pull: { "products.websiteTalk.activeScanIds": scanId }, }; if (cfg.products?.websiteTalk?.activeScanId === scanId) { updateOps.$unset = { "products.websiteTalk.activeScanId": "" }; } await getClientsConfigCollection().updateOne({ clientId }, updateOps); }; /** Returns the scan whose content seeds the phone call's opening description, if one is set. */ export const getPrimaryScanId = async ( clientId: string, ): Promise => { const cfg = await getClientConfig(clientId); return cfg?.products?.websiteTalk?.primaryScanId; }; /** * Sets the primary scan, replacing whatever was set before — exactly one * primary scan at a time. Throws if the client doesn't exist. */ export const setPrimaryScanId = async ( clientId: string, scanId: string, ): Promise => { const result = await getClientsConfigCollection().updateOne( { clientId }, { $set: { "products.websiteTalk.primaryScanId": scanId } }, ); if (result.matchedCount === 0) { throw new Error(`No client config found for clientId: ${clientId}`); } }; /** * Clears the primary scan. Callers that need the opening description fall * back to the first active scan. Throws if the client doesn't exist. */ export const clearPrimaryScanId = async (clientId: string): Promise => { const result = await getClientsConfigCollection().updateOne( { clientId }, { $unset: { "products.websiteTalk.primaryScanId": "" } }, ); if (result.matchedCount === 0) { throw new Error(`No client config found for clientId: ${clientId}`); } }; export async function listClientIdsWithConfiguredQuota(): Promise { const docs = await getClientsConfigCollection() .find({ "quota.totalQuota": { $exists: true, $ne: null } }) .project({ clientId: 1, _id: 0 }) .toArray(); return docs.map(({ clientId }) => clientId); } export async function incrementClientQuotaUsage( clientId: string, amount: number = 1, ): Promise { const result = await getClientsConfigCollection().updateOne( { clientId, "quota.totalQuota": { $exists: true } }, { $inc: { "quota.usedQuota": amount } }, ); if (result.matchedCount === 0) { return; } } export async function resetClientQuotaUsage(clientId: string): Promise { const result = await getClientsConfigCollection().updateOne( { clientId }, { $set: { "quota.usedQuota": 0 } }, ); if (result.matchedCount === 0) { throw new Error(`No client config found for clientId: ${clientId}`); } await resetClientQuotaSettings(clientId); } export { getClientQuotaState, updateQuotaAlertsConfig, markQuotaAlertSent, markQuotaExpiredSent, resetClientQuotaSettings, } from "../clients/clients.quota.getters";