/
mikopbx
/
ModuleBeelinePbx
Обзор
Документация
Войти
/
mikopbx
/
ModuleBeelinePbx
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
master
tests/BeelineCdrHelperTest.php
321 строка
15 KB
boffart
Инициализация модуля ModuleBeelinePbx
08 июл 2026, 17:11
08 июл 2026, 17:11
1cc4e1a
Код
Авторство
О чём код?
<?php /* * MikoPBX - free phone system for small business * Copyright © 2017-2024 Alexey Portnov and Nikolay Beketov * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. */ namespace Modules\ModuleBeelinePbx\Tests; use Modules\ModuleBeelinePbx\Lib\BeelineCdrHelper; use PHPUnit\Framework\TestCase; /** * Тесты преобразования записи статистики Beeline в строку CDR. * * Данные основаны на реальных ответах /v2/statistics и /abonents. */ class BeelineCdrHelperTest extends TestCase { /** * Отвеченный входящий с externalTrackingId: сторона А — внешний номер, сторона Б — добавочный, * did — многоканальный номер, право на запись — pending. */ public function testBuildInboundAnsweredWithTracking(): void { $row = [ 'startDate' => 1781706578000, 'abonent' => ['userId' => '9037690474@msk.ims', 'extension' => '21270'], 'direction' => 'INBOUND', 'phone_from' => '+79775801935', 'phone_to' => '+74957986353', 'duration' => 9340, 'status' => 'RECIEVED', 'externalTrackingId' => 'abc-123', ]; $cdr = BeelineCdrHelper::buildCdrRow($row, 0); $this->assertNotNull($cdr); $this->assertSame('fs-beeline-abc-123', $cdr['UNIQUEID']); $this->assertSame('fs-beeline-abc-123', $cdr['linkedid']); $this->assertSame('79775801935', $cdr['src_num']); $this->assertSame('21270', $cdr['dst_num']); $this->assertSame('74957986353', $cdr['did']); $this->assertSame('ANSWERED', $cdr['disposition']); $this->assertSame(9, $cdr['duration']); $this->assertSame(9, $cdr['billsec']); $this->assertSame('none', $cdr['bee_rec_status']); $this->assertSame('abc-123', $cdr['bee_ext_tracking_id']); $this->assertSame('9037690474@msk.ims', $cdr['bee_user_id']); $this->assertSame('PJSIP/beeline-abc-123', $cdr['src_chan']); $this->assertSame('PJSIP/beeline-21270-abc-123', $cdr['dst_chan']); $this->assertNotSame('', $cdr['answer']); } /** * Отвеченный исходящий без externalTrackingId: сторона А — добавочный, сторона Б — внешний; * UNIQUEID синтезируется хэшем, запись недоступна (нет tracking) → none. */ public function testBuildOutboundAnsweredWithoutTracking(): void { $row = [ 'startDate' => 1781706578000, 'abonent' => ['userId' => '9647085730@msk.ims', 'extension' => '21250'], 'direction' => 'OUTBOUND', 'phone_to' => '+79652041568', 'duration' => 39920, 'status' => 'PLACED', ]; $cdr = BeelineCdrHelper::buildCdrRow($row, 0); $this->assertNotNull($cdr); $this->assertStringStartsWith('fs-beeline-h', $cdr['UNIQUEID']); $this->assertSame('21250', $cdr['src_num']); $this->assertSame('79652041568', $cdr['dst_num']); $this->assertSame('', $cdr['did']); $this->assertSame('ANSWERED', $cdr['disposition']); $this->assertSame(40, $cdr['duration']); $this->assertSame('none', $cdr['bee_rec_status']); } /** * Пропущенный звонок: NOANSWER, billsec 0, пустой answer, запись недоступна даже с tracking. */ public function testBuildMissedCall(): void { $row = [ 'startDate' => 1781705547000, 'abonent' => ['userId' => '9771275486@msk.ims', 'extension' => '21420'], 'direction' => 'OUTBOUND', 'phone_to' => '+79203412169', 'duration' => 0, 'status' => 'MISSED', 'externalTrackingId' => 'm-1', ]; $cdr = BeelineCdrHelper::buildCdrRow($row, 0); $this->assertNotNull($cdr); $this->assertSame('NOANSWER', $cdr['disposition']); $this->assertSame(0, $cdr['billsec']); $this->assertSame('', $cdr['answer']); $this->assertSame('none', $cdr['bee_rec_status']); } /** * Строка без номера сотрудника (нет ни extension, ни мобильного в userId) не пригодна для CDR. */ public function testBuildReturnsNullWithoutEmployeeNumber(): void { $row = [ 'startDate' => 1781705547000, 'abonent' => ['userId' => 'SIP00XXX@ip.beeline.ru'], // нет extension, userId не msisdn 'direction' => 'OUTBOUND', 'phone_to' => '+79203412169', 'duration' => 1000, 'status' => 'PLACED', ]; $this->assertNull(BeelineCdrHelper::buildCdrRow($row, 0)); } /** * gap сдвигает время начала на заданное число часов. */ public function testGapShiftsStartTime(): void { $row = [ 'startDate' => 1781706578000, 'abonent' => ['userId' => 'u', 'extension' => '21000'], 'direction' => 'OUTBOUND', 'phone_to' => '+79652041568', 'duration' => 10000, 'status' => 'PLACED', ]; $cdr0 = BeelineCdrHelper::buildCdrRow($row, 0); $cdr3 = BeelineCdrHelper::buildCdrRow($row, 3); $diff = strtotime($cdr3['start']) - strtotime($cdr0['start']); $this->assertSame(3 * 3600, $diff); } /** * callUid: externalTrackingId имеет приоритет; без него — детерминированный хэш. */ public function testCallUidPrefersTracking(): void { $withTracking = ['externalTrackingId' => 'xyz', 'abonent' => ['userId' => 'u'], 'startDate' => 1, 'direction' => 'INBOUND']; $this->assertSame('xyz', BeelineCdrHelper::callUid($withTracking, '79990001122')); $noTracking = ['abonent' => ['userId' => 'u'], 'startDate' => 1, 'direction' => 'INBOUND']; $uidA = BeelineCdrHelper::callUid($noTracking, '79990001122'); $uidB = BeelineCdrHelper::callUid($noTracking, '79990001122'); $this->assertSame($uidA, $uidB); $this->assertStringStartsWith('h', $uidA); $this->assertNotSame($uidA, BeelineCdrHelper::callUid($noTracking, '70000000000')); } /** * isTrunkAbonent распознаёт SIP-транки Beeline (userId …@ip.beeline.ru). */ public function testIsTrunkAbonent(): void { $trunk = ['abonent' => ['userId' => 'SIP00KKBU00BP9@ip.beeline.ru']]; $employee = ['abonent' => ['userId' => '9037690474@msk.ims.mnc099.mcc250.3gppnetwork.org']]; $this->assertTrue(BeelineCdrHelper::isTrunkAbonent($trunk)); $this->assertFalse(BeelineCdrHelper::isTrunkAbonent($employee)); } /** * parseExcludedNumbers: короткие номера — точно, длинные — по последним 10 цифрам; * разные разделители и произвольный формат. */ public function testParseExcludedNumbers(): void { $set = BeelineCdrHelper::parseExcludedNumbers("2500\n+7 916 215-35-49, 8(495)1234567 ; \n"); $this->assertArrayHasKey('2500', $set); // короткий — как есть $this->assertArrayHasKey('9162153549', $set); // 11 цифр → последние 10 $this->assertArrayHasKey('4951234567', $set); // 10 цифр $this->assertCount(3, $set); } /** * isNumberExcluded: короткий добавочный точно, внешний по последним 10 цифрам. */ public function testIsNumberExcluded(): void { $set = BeelineCdrHelper::parseExcludedNumbers("2500\n79162153549"); $this->assertTrue(BeelineCdrHelper::isNumberExcluded('2500', $set)); $this->assertTrue(BeelineCdrHelper::isNumberExcluded('+79162153549', $set)); $this->assertTrue(BeelineCdrHelper::isNumberExcluded('89162153549', $set)); $this->assertFalse(BeelineCdrHelper::isNumberExcluded('21500', $set)); $this->assertFalse(BeelineCdrHelper::isNumberExcluded('79990001122', $set)); $this->assertFalse(BeelineCdrHelper::isNumberExcluded('2500', [])); } /** * isRowExcluded: строка SIP-транка (ext 2500) отсекается по добавочному номеру. */ public function testIsRowExcludedByExtension(): void { $excluded = BeelineCdrHelper::parseExcludedNumbers('2500'); $trunkRow = [ 'abonent' => ['userId' => 'SIP00KKBU00BP9@ip.beeline.ru', 'extension' => '2500'], 'direction' => 'INBOUND', 'phone_from' => '+78125072778', ]; $employeeRow = [ 'abonent' => ['userId' => '9037690474@msk.ims', 'extension' => '21270'], 'direction' => 'INBOUND', 'phone_from' => '+78125072778', ]; $this->assertTrue(BeelineCdrHelper::isRowExcluded($trunkRow, $excluded)); $this->assertFalse(BeelineCdrHelper::isRowExcluded($employeeRow, $excluded)); } /** * employeeNumber: режим extension отдаёт добавочный, mobile — 11-значный мобильный (10→7…). */ public function testEmployeeNumberModes(): void { // Мобильный сотрудника берётся из userId (msisdn до «@»), не из abonent.phone. $abonent = ['userId' => '9037690474@msk.ims', 'extension' => '21270']; $this->assertSame('21270', BeelineCdrHelper::employeeNumber($abonent, BeelineCdrHelper::EMPLOYEE_FIELD_EXTENSION)); $this->assertSame('79037690474', BeelineCdrHelper::employeeNumber($abonent, BeelineCdrHelper::EMPLOYEE_FIELD_MOBILE)); // SIP-транк: мобильного нет → фоллбэк на extension. $trunk = ['userId' => 'SIP00KKBU00BP9@ip.beeline.ru', 'extension' => '2500']; $this->assertSame('2500', BeelineCdrHelper::employeeNumber($trunk, BeelineCdrHelper::EMPLOYEE_FIELD_MOBILE)); // Фоллбэк: нет extension → mobile даже в режиме extension. $this->assertSame('79037690474', BeelineCdrHelper::employeeNumber(['userId' => '9037690474@x'], BeelineCdrHelper::EMPLOYEE_FIELD_EXTENSION)); } /** * employeeMobileFromUserId: msisdn из userId (10→11), '' для SIP/нечисловых. */ public function testEmployeeMobileFromUserId(): void { $this->assertSame('79037690474', BeelineCdrHelper::employeeMobileFromUserId('9037690474@msk.ims.mnc099.mcc250.3gppnetwork.org')); $this->assertSame('', BeelineCdrHelper::employeeMobileFromUserId('SIP00KKBU00BP9@ip.beeline.ru')); $this->assertSame('', BeelineCdrHelper::employeeMobileFromUserId('')); } /** * buildCdrRow в режиме mobile: сторона сотрудника — 11-значный мобильный (из userId). */ public function testBuildCdrRowMobileMode(): void { $row = [ 'startDate' => 1781706578000, 'abonent' => ['userId' => '9037690474@msk.ims', 'extension' => '21270'], 'direction' => 'INBOUND', 'phone_from' => '+79775801935', 'duration' => 9340, 'status' => 'RECIEVED', ]; $cdr = BeelineCdrHelper::buildCdrRow($row, 0, BeelineCdrHelper::EMPLOYEE_FIELD_MOBILE); $this->assertSame('79775801935', $cdr['src_num']); $this->assertSame('79037690474', $cdr['dst_num']); // сотрудник = мобильный 11 цифр $this->assertStringContainsString('PJSIP/beeline-79037690474-', $cdr['dst_chan']); } /** * isRowExcluded: отсечение по внешнему номеру (phone_from/phone_to). */ public function testIsRowExcludedByExternalNumber(): void { $excluded = BeelineCdrHelper::parseExcludedNumbers('79162153549'); $row = [ 'abonent' => ['userId' => 'u', 'extension' => '21030'], 'direction' => 'INBOUND', 'phone_from' => '+79162153549', ]; $this->assertTrue(BeelineCdrHelper::isRowExcluded($row, $excluded)); $this->assertFalse(BeelineCdrHelper::isRowExcluded($row, [])); } /** * employeeName: имя сотрудника берётся из firstName; lastName (у транков) — как запасной вариант. * Данные реальны: /v2/statistics отдаёт firstName='m-Наталия', lastName=null у сотрудников * и firstName=null, lastName='Sip-Art' у SIP-транков. */ public function testEmployeeName(): void { $this->assertSame('m-Наталия', BeelineCdrHelper::employeeName(['firstName' => 'm-Наталия'])); $this->assertSame('m-Елизавета Пуглеева', BeelineCdrHelper::employeeName(['firstName' => 'm-Елизавета Пуглеева', 'lastName' => null])); $this->assertSame('Sip-Art', BeelineCdrHelper::employeeName(['firstName' => null, 'lastName' => 'Sip-Art'])); $this->assertSame('', BeelineCdrHelper::employeeName(['userId' => 'u'])); } /** * buildCdrRow: имя сотрудника ставится на его сторону — dst_name при входящем, src_name при * исходящем; сторона внешнего абонента остаётся без имени. */ public function testBuildCdrRowFillsEmployeeName(): void { $inbound = [ 'externalTrackingId' => 'abc-123', 'abonent' => ['userId' => '9779348842@msk.ims', 'extension' => '21280', 'firstName' => 'm-Наталия'], 'direction' => 'INBOUND', 'phone_from' => '+79775801935', 'phone_to' => '+74957986353', 'duration' => 9340, 'status' => 'RECIEVED', ]; $cdr = BeelineCdrHelper::buildCdrRow($inbound, 0); $this->assertSame('', $cdr['src_name']); $this->assertSame('m-Наталия', $cdr['dst_name']); $outbound = [ 'abonent' => ['userId' => '9645071444@msk.ims', 'extension' => '21230', 'firstName' => 'm-Елизавета Пуглеева'], 'direction' => 'OUTBOUND', 'phone_to' => '+74951208801', 'duration' => 40000, 'status' => 'PLACED', 'startDate' => 1781706578000, ]; $cdr = BeelineCdrHelper::buildCdrRow($outbound, 0); $this->assertSame('m-Елизавета Пуглеева', $cdr['src_name']); $this->assertSame('', $cdr['dst_name']); } }