null; try { $date = $date ?? Carbon::yesterday()->format('Y-m-d'); $startTime = microtime(true); if (CompanySettings::get(SettingsHelper::S_COMPANY_CONSOLE_DEBUG_LOGS)) { Log::info("Starting outbound calls report for date {$date}"); } $calls = $this->fetchOutboundCalls($date, $emit); $fetched = count($calls); if (CompanySettings::get(SettingsHelper::S_COMPANY_CONSOLE_DEBUG_LOGS)) { Log::info("Outbound calls fetched: {$fetched}"); } $count = $this->processOutboundCalls($calls, $date, $emit); $elapsed = round(microtime(true) - $startTime, 1); $emit("Done — {$count} records ({$elapsed}s)"); if (CompanySettings::get(SettingsHelper::S_COMPANY_CONSOLE_DEBUG_LOGS)) { Log::info("Successfully generated {$count} outbound calls for date {$date}"); } return $count; } catch (\Exception $e) { if (CompanySettings::get(SettingsHelper::S_COMPANY_CONSOLE_DEBUG_LOGS)) { Log::error("Error generating outbound calls report: " . $e->getMessage()); } return 0; } } // ── Private helpers ─────────────────────────────────────────────────────── private function deleteExistingOutboundCalls(string $date): int { return OutboundReport::where('report_date', $date)->delete(); } private function fetchOutboundCalls(string $date, callable $emit = null): array { $emit ??= static fn() => null; $startOfDay = Carbon::parse($date)->startOfDay()->format('Y-m-d H:i:s'); $endOfDay = Carbon::parse($date)->endOfDay()->format('Y-m-d H:i:s'); // ── Step 1: Company users (IDs only — cdr.cnum = user ID) ───────────── $emit('Step 1/3 — Fetching company users...'); $userIds = DB::table('users') ->whereNotNull('company_id') ->pluck('id') ->all(); if (empty($userIds)) { $emit('No users found — nothing to process.'); return []; } $emit('Step 1/3 — Found ' . count($userIds) . ' users'); // ── Step 2: Outbound CDR rows for the target date ───────────────────── // cdr.cnum = user ID (the agent who placed the call) // dcontext = 'from-internal' identifies agent-initiated calls $emit('Step 2/3 — Fetching outbound CDR records...'); $cdrRows = DB::table('asteriskcdrdb.cdr') ->select([ 'uniqueid', 'linkedid', 'calldate', 'cnum', 'cnam', 'dst', 'src', 'duration', 'billsec', DB::raw('(duration - billsec) AS ringtime'), 'disposition', 'recordingfile', ]) ->whereIn('disposition', ['ANSWERED', 'BUSY', 'NO ANSWER', 'FAILED']) ->where('dcontext', 'from-internal') ->where('lastapp', 'Dial') ->whereColumn('uniqueid', 'linkedid') ->whereBetween('calldate', [$startOfDay, $endOfDay]) ->whereIn('cnum', $userIds) ->orderBy('calldate', 'asc') ->get(); if ($cdrRows->isEmpty()) { $emit('No outbound CDR records found for this date.'); return []; } $emit('Step 2/3 — ' . $cdrRows->count() . ' CDR records found'); // ── Step 3: Surveys for ANSWERED calls ──────────────────────────────── $emit('Step 3/3 — Fetching surveys...'); $answeredLinkedids = $cdrRows ->where('disposition', 'ANSWERED') ->pluck('linkedid') ->unique() ->values() ->all(); $surveys = !empty($answeredLinkedids) ? DB::table('surveys') ->select(['linkedid', 'customer_choice']) ->whereIn('linkedid', $answeredLinkedids) ->get() ->keyBy('linkedid') : collect(); $emit('Step 3/3 — ' . $surveys->count() . ' surveys found'); // ── Single-pass merge ───────────────────────────────────────────────── $emit('Merging results...'); $results = []; foreach ($cdrRows as $row) { $results[] = (object) [ 'callid' => $row->uniqueid, 'linkedid' => $row->linkedid, 'call_time' => $row->calldate, 'agent_ext' => $row->cnum, 'agent_name' => $row->cnam, 'called_number' => $row->dst, 'src' => $row->src, 'duration' => (int) ($row->duration ?? 0), 'talktime' => (int) ($row->billsec ?? 0), 'ringtime' => (int) ($row->ringtime ?? 0), 'disposition' => $row->disposition, 'recordingfile' => $row->recordingfile, 'survey' => $surveys[$row->linkedid]->customer_choice ?? null, ]; } $emit('Merge complete — ' . count($results) . ' records ready'); return $results; } private function processOutboundCalls(array $data, string $date, callable $output = null): int { $emit = $output ?? static fn() => null; $now = now(); $rows = []; foreach ($data as $item) { $rows[] = [ 'linkedid' => $item->linkedid, 'callid' => $item->callid, 'call_time' => $item->call_time, 'agent_ext' => $item->agent_ext, 'agent_name' => $item->agent_name, 'called_number' => $item->called_number, 'src' => $item->src, 'duration' => $item->duration, 'talktime' => $item->talktime, 'ringtime' => $item->ringtime, 'disposition' => $item->disposition, 'recordingfile' => $item->recordingfile, 'done' => 0, 'survey' => $item->survey, 'report_date' => $date, 'created_at' => $now, 'updated_at' => $now, ]; } $emit('Deleting existing records for ' . $date . '...'); $this->deleteExistingOutboundCalls($date); $batches = array_chunk($rows, 1000); $totalBatches = count($batches); foreach ($batches as $i => $batch) { $emit(sprintf('Inserting batch %d/%d (%s records)...', $i + 1, $totalBatches, number_format(count($batch)))); try { DB::table('outbound_reports')->insert($batch); } catch (\Exception $e) { if (CompanySettings::get(SettingsHelper::S_COMPANY_CONSOLE_DEBUG_LOGS)) { Log::error("Failed to insert outbound calls batch: " . $e->getMessage()); } } } return count($rows); } // ========================================================================= // Real-time single-call processing (invoked by ProcessOutboundCallJob) // ========================================================================= /** * Process a single outbound call identified by its linkedid. * * Uses a lean 2-query pipeline: * Query 1 — CDR rows for the linkedid (picks the root leg; cnam = agent name) * Query 2 — Survey (ANSWERED calls only) */ public function processSingleOutboundCall(string $linkedid): OutboundReport|false { try { $linkedid = trim($linkedid); // ── Query 1: CDR rows for this linkedid ─────────────────────────── $cdrRows = DB::table('asteriskcdrdb.cdr') ->select([ 'uniqueid', 'linkedid', 'calldate', 'cnum', 'cnam', 'dst', 'src', 'duration', 'billsec', DB::raw('(duration - billsec) AS ringtime'), 'disposition', 'recordingfile', ]) ->where('linkedid', $linkedid) ->where('dcontext', 'from-internal') ->where('lastapp', 'Dial') ->orderBy('calldate', 'asc') ->get(); if ($cdrRows->isEmpty()) { if (CompanySettings::get(SettingsHelper::S_COMPANY_CONSOLE_DEBUG_LOGS)) { Log::warning("[OutboundCallsService] No CDR rows for {$linkedid}"); } return false; } // Root leg (uniqueid = linkedid) carries the canonical call data. // Fall back to the first row if the root leg is not present. $root = $cdrRows->firstWhere('uniqueid', $linkedid) ?? $cdrRows->first(); // ── Query 2: Survey (ANSWERED calls only) ───────────────────────── $survey = null; if ($root->disposition === 'ANSWERED') { $survey = DB::table('surveys') ->where('linkedid', $linkedid) ->value('customer_choice'); } $callTime = $root->calldate; $reportDate = Carbon::parse($callTime)->format('Y-m-d'); // ── Write ────────────────────────────────────────────────────────── OutboundReport::where('linkedid', $linkedid)->delete(); $report = OutboundReport::create([ 'linkedid' => $linkedid, 'callid' => $root->uniqueid, 'call_time' => $callTime, 'agent_ext' => $root->cnum, 'agent_name' => $root->cnam, 'called_number' => $root->dst, 'src' => $root->src, 'duration' => (int) ($root->duration ?? 0), 'talktime' => (int) ($root->billsec ?? 0), 'ringtime' => (int) ($root->ringtime ?? 0), 'disposition' => $root->disposition, 'recordingfile' => $root->recordingfile, 'done' => 0, 'survey' => $survey, 'report_date' => $reportDate, ]); $calledNumber = $root->dst; if ($calledNumber !== null && str_starts_with($calledNumber, '+2')) { $calledNumber = substr($calledNumber, 2); } $outboundUserId = null; if (!empty($root->cnum) && preg_match('/\d+/', $root->cnum, $m)) { $outboundUserId = (int) $m[0]; } CallLogSyncService::sync($linkedid, 'outbound', [ 'phone_number' => $calledNumber, 'call_date' => $callTime, 'duration' => (int) ($root->duration ?? 0), 'talktime' => (int) ($root->billsec ?? 0), 'ringtime' => (int) ($root->ringtime ?? 0), 'user_id' => $outboundUserId, ]); return $report; } catch (\Exception $e) { if (CompanySettings::get(SettingsHelper::S_COMPANY_CONSOLE_DEBUG_LOGS)) { Log::error("[OutboundCallsService] Error processing outbound call [{$linkedid}]: " . $e->getMessage()); } return false; } } }