import { Call, CallDoc, CallQueryOptions, CallUpdateParams, getDb, } from "../index"; import type { CountOpts, DateRange, ToolExecution, CallStatus } from "./calls.types"; import { TOOL_EXECUTIONS_DEFAULT_LIMIT, TOOL_EXECUTIONS_DEFAULT_SKIP, } from "./calls.constants"; import { Filter, ObjectId } from "mongodb"; import * as process from "node:process"; import { applyQueryOptions } from "../utils/query.utils"; import { DashboardHeatmapMetric } from "./dashboard/calls.dashboard.types"; export const getCallsCollection = () => { return getDb().collection("calls"); }; export const getCallByCallSid = (callSid: string) => { return getCallsCollection().findOne({ callSid }); }; export const getCallsByPhoneNumber = (phoneNumber: string) => { return getCallsCollection() .find({ customerPhoneNumber: phoneNumber }) .toArray(); }; export const getCallsByClient = (clientId: string) => { return getCallsCollection().find({ clientId }).toArray(); }; export const getCallsByFlow = (flowId: ObjectId) => { return getCallsCollection().find({ flowId }).toArray(); }; export const createCallDoc = ( call: Omit, ) => { return getCallsCollection().insertOne({ ...call, createdAt: new Date(), updatedAt: new Date(), env: process.env.ENV ?? "unknown", }); }; export const updateCallByCallSid = async ( callSid: string, updates: CallUpdateParams, ): Promise => { return await getCallsCollection().findOneAndUpdate( { callSid: callSid }, { $set: { ...updates, updatedAt: new Date(), }, }, { returnDocument: "after" }, ); }; export const getToolExecutionsByCallSid = async ( callSid: string, opts?: Pick & { clientId?: string }, ): Promise => { const skip = Math.max(0, opts?.skip ?? TOOL_EXECUTIONS_DEFAULT_SKIP); const limit = Math.max(1, opts?.limit ?? TOOL_EXECUTIONS_DEFAULT_LIMIT); const filter: Filter = { callSid }; if (opts?.clientId != null) filter.clientId = opts.clientId; const call = await getCallsCollection().findOne( filter, { projection: { toolExecutions: { $slice: [skip, limit] } } }, ); return call?.toolExecutions ?? []; }; export const updateCallStatusByCallSid = async ( callSid: string, status: CallStatus, ): Promise => getCallsCollection().findOneAndUpdate( { callSid }, { $set: { status, updatedAt: new Date() } }, { returnDocument: "after" }, ); export const pushToolExecution = async ( callSid: string, execution: ToolExecution, ): Promise => { await getCallsCollection().updateOne( { callSid }, { $push: { toolExecutions: execution } }, ); }; // get calls by client and date range //client here is the user id export const getCallsByClientAndDateRange = ( clientId: string, startDate: Date, endDate: Date, ) => { return getCallsCollection() .find({ clientId, createdAt: { $gte: startDate, $lte: endDate, }, }) .toArray(); }; export const findCallsByQuery = async ( query: Filter, options?: CallQueryOptions, ): Promise => { const cursor = getCallsCollection().find(query); return await applyQueryOptions(cursor, options).toArray(); }; export const countCalls = async (query: Filter): Promise => { return getCallsCollection().countDocuments(query); }; export async function countCallsByPhoneInRange( customerPhoneNumber: string, opts: CountOpts = {}, range: DateRange = {}, ): Promise { const filter: Partial & { customerPhoneNumber: string } = { customerPhoneNumber, }; if (opts.isOutgoingCall !== undefined) { filter.isOutgoingCall = opts.isOutgoingCall; } if (opts.isIncomingCall !== undefined) { filter.isIncomingCall = opts.isIncomingCall; } if (range.since || range.until) { (filter as Filter).createdAt = { ...(range.since ? { $gte: range.since } : {}), ...(range.until ? { $lt: range.until } : {}), }; } return getCallsCollection().countDocuments(filter as Filter); } export const range = { between: (since: Date, until: Date): DateRange => ({ since, until }), todayToNow: (): DateRange => ({ since: startOfDay(), until: new Date() }), yesterday: (): DateRange => { const s = startOfDay(addDays(new Date(), -1)); const e = addDays(s, 1); return { since: s, until: e }; }, todayFull: (): DateRange => { const s = startOfDay(); const e = addDays(s, 1); return { since: s, until: e }; }, startOfDayToNow: (d: Date): DateRange => ({ since: startOfDay(d), until: new Date(), }), }; export function startOfDay(d: Date = new Date()): Date { const x = new Date(d); x.setUTCHours(0, 0, 0, 0); return x; } export function addDays(d: Date, days: number): Date { const x = new Date(d); x.setDate(x.getDate() + days); return x; } /** * Aggregate calls stats for a date range (createdAt in [startStr, endStr] in timezone). * completed = agentHungUp=true or status='completed'. */ export async function getCallsStatsForDateRange( clientId: string, startStr: string, endStr: string, timezone: string, ): Promise<{ count: number; totalLen: number; completed: number }> { const coll = getCallsCollection(); const out = await coll .aggregate<{ _id: null; count: number; totalLen: number; completed: number; }>([ // 1. Restrict to the given client { $match: { clientId } }, // 2. Derive dateLocal (createdAt as YYYY-MM-DD in timezone) and isCompleted (agentHungUp or status='completed') { $addFields: { dateLocal: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt", timezone }, }, isCompleted: { $or: [ { $eq: ["$agentHungUp", true] }, { $eq: ["$status", "completed"] }, ], }, }, }, // 3. Keep only documents with dateLocal in [startStr, endStr] (inclusive) { $match: { dateLocal: { $gte: startStr, $lte: endStr } } }, // 4. Single group: count, totalLen, completed { $group: { _id: null, count: { $sum: 1 }, totalLen: { $sum: "$callLength" }, completed: { $sum: { $cond: ["$isCompleted", 1, 0] } }, }, }, ]) .next(); return { count: out?.count ?? 0, totalLen: out?.totalLen ?? 0, completed: out?.completed ?? 0, }; } /** * Aggregate calls by hour for a given date (createdAt converted to given timezone). Hour format "HH:mm". */ export async function getCallsHourlyAggregation( clientId: string, dateStr: string, timezone: string, ): Promise { const coll = getCallsCollection(); const rows = await coll .aggregate<{ _id: number; calls: number }>([ // 1. Restrict to the given client { $match: { clientId } }, { $addFields: { dateLocal: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt", timezone }, }, hour: { $hour: { date: "$createdAt", timezone } }, }, }, // 3. Keep only the requested date { $match: { dateLocal: dateStr } }, // 4. Group by hour, sum calls per hour { $group: { _id: "$hour", calls: { $sum: 1 } } }, // 5. Order by hour ascending (0..23) { $sort: { _id: 1 } }, ]) .toArray(); return rows.map((r: any) => ({ hour: r._id, calls: r.calls, })); }