diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index 9eb056b..af66d9a 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -45,9 +45,21 @@ class Account extends Authenticatable public static $dtmfProtocols = ['sipinfo' => 'SIPInfo', 'rfc2833' => 'RFC2833', 'sipmessage' => 'SIP Message']; - /** - * Scopes - */ + public static function boot() + { + parent::boot(); + + static::deleted(function ($item) { + StatisticsMessage::where('from_username', $item->username) + ->where('from_domain', $item->domain) + ->delete(); + + StatisticsCall::where('from_username', $item->username) + ->where('from_domain', $item->domain) + ->delete(); + }); + } + protected static function booted() { static::addGlobalScope('domain', function (Builder $builder) { @@ -179,8 +191,8 @@ class Account extends Authenticatable public function getFullIdentifierAttribute() { $displayName = $this->attributes['display_name'] - ? '"' . $this->attributes['display_name'] . '" ' - : ''; + ? '"' . $this->attributes['display_name'] . '" ' + : ''; return $displayName . 'getIdentifierAttribute() . '>'; } diff --git a/flexiapi/app/Http/Controllers/Admin/StatisticsController.php b/flexiapi/app/Http/Controllers/Admin/StatisticsController.php index 925f711..d86ec6c 100644 --- a/flexiapi/app/Http/Controllers/Admin/StatisticsController.php +++ b/flexiapi/app/Http/Controllers/Admin/StatisticsController.php @@ -3,10 +3,11 @@ namespace App\Http\Controllers\Admin; use App\Account; +use App\StatisticsMessage; +use App\StatisticsCall; use App\Http\Controllers\Controller; use Illuminate\Http\Request; -use App\StatisticsMessage; use Carbon\Carbon; use Carbon\CarbonInterval; use Carbon\CarbonPeriod; @@ -50,6 +51,12 @@ class StatisticsController extends Controller $data = StatisticsMessage::orderBy($dateColumn, 'asc'); break; + case 'calls': + $dateColumn = 'initiated_at'; + $label = 'Calls'; + $data = StatisticsCall::orderBy($dateColumn, 'asc'); + break; + case 'accounts': $label = 'Accounts'; $data = Account::orderBy($dateColumn, 'asc'); diff --git a/flexiapi/app/Http/Controllers/Api/StatisticsCallController.php b/flexiapi/app/Http/Controllers/Api/StatisticsCallController.php new file mode 100644 index 0000000..2161c8a --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/StatisticsCallController.php @@ -0,0 +1,92 @@ +. +*/ + +namespace App\Http\Controllers\Api; + +use App\Http\Controllers\Controller; +use App\StatisticsCall; +use App\StatisticsCallDevice; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; + +class StatisticsCallController extends Controller +{ + public function store(Request $request) + { + $request->validate([ + 'id' => 'required|string|max:64', + 'from' => 'required|string|max:256', + 'to' => 'required|string|max:256', + 'initiated_at' => 'required|iso_date', + 'ended_at' => 'iso_date', + 'conference_id' => 'string|nullable', + ]); + + $statisticsCall = new StatisticsCall; + $statisticsCall->id = $request->get('id'); + list($statisticsCall->from_username, $statisticsCall->from_domain) = explode('@', $request->get('from')); + list($statisticsCall->to_username, $statisticsCall->to_domain) = explode('@', $request->get('to')); + $statisticsCall->initiated_at = $request->get('initiated_at'); + $statisticsCall->ended_at = $request->get('ended_at'); + //$statisticsCall->conference_id = $request->get('conference_id'); + + try { + return $statisticsCall->saveOrFail(); + } catch (\Exception $e) { + Log::channel('database_errors')->error($e->getMessage()); + abort(400, 'Database error'); + } + } + + public function storeDevice(Request $request, string $callId, string $deviceId) + { + $request->validate([ + 'rang_at' => 'iso_date', + 'invite_terminated.at' => 'required_with:invite_terminated.state,iso_date', + 'invite_terminated.state' => 'required_with:invite_terminated.at,string', + ]); + + try { + return StatisticsCallDevice::updateOrCreate( + ['call_id' => $callId, 'device_id' => $deviceId], + [ + 'rang_at' => $request->get('rang_at'), + 'invite_terminated_at' => $request->get('invite_terminated.at'), + 'invite_terminated_state' => $request->get('invite_terminated.state') + ] + ); + } catch (\Exception $e) { + Log::channel('database_errors')->error($e->getMessage()); + abort(400, 'Database error'); + } + } + + public function update(Request $request, string $callId) + { + $request->validate([ + 'ended_at' => 'required|iso_date', + ]); + + $statisticsCall = StatisticsCall::where('id', $callId)->firstOrFail(); + $statisticsCall->ended_at = $request->get('ended_at'); + $statisticsCall->save(); + + return $statisticsCall; + } +} diff --git a/flexiapi/app/Http/Controllers/Api/StatisticsMessageController.php b/flexiapi/app/Http/Controllers/Api/StatisticsMessageController.php index aaeba00..53b00e0 100644 --- a/flexiapi/app/Http/Controllers/Api/StatisticsMessageController.php +++ b/flexiapi/app/Http/Controllers/Api/StatisticsMessageController.php @@ -1,4 +1,21 @@ . +*/ namespace App\Http\Controllers\Api; @@ -22,7 +39,7 @@ class StatisticsMessageController extends Controller $statisticsMessage = new StatisticsMessage; $statisticsMessage->id = $request->get('id'); - $statisticsMessage->from = $request->get('from'); + list($statisticsMessage->from_username, $statisticsMessage->from_domain) = explode('@', $request->get('from')); $statisticsMessage->sent_at = $request->get('sent_at'); $statisticsMessage->encrypted = $request->get('encrypted'); //$statisticsMessage->conference_id = $request->get('conference_id'); @@ -43,9 +60,11 @@ class StatisticsMessageController extends Controller 'received_at' => 'required|iso_date' ]); + list($toUsername, $toDomain) = explode('@', $to); + try { return StatisticsMessageDevice::updateOrCreate( - ['message_id' => $messageId, 'to' => $to, 'device_id' => $deviceId], + ['message_id' => $messageId, 'to_username' => $toUsername, 'to_domain' => $toDomain, 'device_id' => $deviceId], ['last_status' => $request->get('last_status'), 'received_at' => $request->get('received_at')] ); } catch (\Exception $e) { diff --git a/flexiapi/app/StatisticsCall.php b/flexiapi/app/StatisticsCall.php new file mode 100644 index 0000000..0d9591d --- /dev/null +++ b/flexiapi/app/StatisticsCall.php @@ -0,0 +1,32 @@ +. +*/ + +namespace App; + +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; + +class StatisticsCall extends Model +{ + use HasFactory; + + public $incrementing = false; + protected $casts = ['initiated_at' => 'datetime', 'ended_at' => 'datetime']; + protected $keyType = 'string'; +} diff --git a/flexiapi/app/StatisticsCallDevice.php b/flexiapi/app/StatisticsCallDevice.php new file mode 100644 index 0000000..6717df2 --- /dev/null +++ b/flexiapi/app/StatisticsCallDevice.php @@ -0,0 +1,36 @@ +. +*/ + +namespace App; + +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; + +class StatisticsCallDevice extends Model +{ + use HasFactory; + + protected $fillable = ['call_id', 'device_id', 'rang_at', 'invite_terminated_at', 'invite_terminated_state', 'call_id']; + protected $casts = ['rang_at' => 'datetime']; + + public function call() + { + return $this->hasOne(StatisticsCall::class, 'id', 'call_id'); + } +} diff --git a/flexiapi/app/StatisticsMessageDevice.php b/flexiapi/app/StatisticsMessageDevice.php index dbf9fff..719a21e 100644 --- a/flexiapi/app/StatisticsMessageDevice.php +++ b/flexiapi/app/StatisticsMessageDevice.php @@ -26,7 +26,7 @@ class StatisticsMessageDevice extends Model { use HasFactory; - protected $fillable = ['message_id', 'to', 'device_id', 'last_status', 'received_at']; + protected $fillable = ['message_id', 'to_username', 'to_domain', 'device_id', 'last_status', 'received_at']; protected $casts = ['received_at' => 'datetime']; public function message() diff --git a/flexiapi/database/factories/StatisticsCallFactory.php b/flexiapi/database/factories/StatisticsCallFactory.php new file mode 100644 index 0000000..de40a2a --- /dev/null +++ b/flexiapi/database/factories/StatisticsCallFactory.php @@ -0,0 +1,21 @@ + $this->faker->uuid(), + 'from_username' => $this->faker->userName(), + 'from_domain' => $this->faker->domainName(), + 'to_username' => $this->faker->userName(), + 'to_domain' => $this->faker->domainName(), + 'initiated_at' => $this->faker->dateTimeBetween('-1 year'), + 'ended_at' => $this->faker->dateTimeBetween('-1 year'), + ]; + } +} diff --git a/flexiapi/database/factories/StatisticsMessageFactory.php b/flexiapi/database/factories/StatisticsMessageFactory.php index 3252a96..4c70d9e 100644 --- a/flexiapi/database/factories/StatisticsMessageFactory.php +++ b/flexiapi/database/factories/StatisticsMessageFactory.php @@ -10,7 +10,8 @@ class StatisticsMessageFactory extends Factory { return [ 'id' => $this->faker->uuid(), - 'from' => $this->faker->email(), + 'from_username' => $this->faker->userName(), + 'from_domain' => $this->faker->domainName(), 'sent_at' => $this->faker->dateTimeBetween('-1 year'), 'encrypted' => false ]; diff --git a/flexiapi/database/migrations/2023_09_14_132627_create_statistics_calls_table.php b/flexiapi/database/migrations/2023_09_14_132627_create_statistics_calls_table.php new file mode 100644 index 0000000..2dc7954 --- /dev/null +++ b/flexiapi/database/migrations/2023_09_14_132627_create_statistics_calls_table.php @@ -0,0 +1,105 @@ +string('id', 64)->unique(); + $table->string('from_username', 256); + $table->string('from_domain', 256); + $table->string('to_username', 256); + $table->string('to_domain', 256); + $table->dateTime('initiated_at'); + $table->dateTime('ended_at')->nullable(); + $table->string('conference_id')->nullable(); + $table->timestamps(); + + $table->index(['from_username', 'from_domain']); + $table->index('initiated_at'); + }); + + Schema::create('statistics_call_devices', function (Blueprint $table) { + $table->id(); + $table->string('call_id', 64); + $table->string('device_id', 64); + $table->dateTime('rang_at')->nullable(); + $table->dateTime('invite_terminated_at')->nullable(); + $table->string('invite_terminated_state')->nullable(); + $table->timestamps(); + + $table->foreign('call_id')->references('id')->on('statistics_calls')->onDelete('cascade'); + $table->unique(['call_id', 'device_id']); + }); + + Schema::disableForeignKeyConstraints(); + Schema::drop('statistics_message_devices'); + Schema::drop('statistics_messages'); + + Schema::create('statistics_messages', function (Blueprint $table) { + $table->string('id', 64)->unique(); + $table->string('from_username', 256); + $table->string('from_domain', 256); + $table->dateTime('sent_at'); + $table->boolean('encrypted')->default(false); + $table->string('conference_id')->nullable(); + $table->timestamps(); + + $table->index(['from_username', 'from_domain']); + $table->index('sent_at'); + }); + + Schema::create('statistics_message_devices', function (Blueprint $table) { + $table->id(); + $table->string('message_id', 64); + $table->string('to_username', 256); + $table->string('to_domain', 256); + $table->string('device_id', 64); + $table->integer('last_status'); + $table->dateTime('received_at'); + $table->timestamps(); + + $table->foreign('message_id')->references('id')->on('statistics_messages')->onDelete('cascade'); + $table->unique(['message_id', 'to_username', 'to_domain', 'device_id'], 'statistics_message_devices_message_id_to_u_to_d_device_id_unique'); + }); + + Schema::enableForeignKeyConstraints(); + } + + public function down() + { + Schema::disableForeignKeyConstraints(); + + Schema::dropIfExists('statistics_calls'); + Schema::dropIfExists('statistics_call_devices'); + + StatisticsMessageDevice::truncate(); + StatisticsMessage::truncate(); + + Schema::table('statistics_messages', function(Blueprint $table) { + $table->dropIndex('statistics_messages_from_username_from_domain_index'); + $table->dropColumn('from_username'); + $table->dropColumn('from_domain'); + + $table->string('from', 256)->index(); + }); + + Schema::table('statistics_message_devices', function(Blueprint $table) { + $table->dropUnique('statistics_message_devices_message_id_to_u_to_d_device_id_unique'); + $table->dropColumn('to_username'); + $table->dropColumn('to_domain'); + + $table->string('to', 256); + + $table->unique(['message_id', 'to', 'device_id']); + }); + + Schema::enableForeignKeyConstraints(); + } +}; diff --git a/flexiapi/database/seeds/StatisticsMessagesSeeder.php b/flexiapi/database/seeds/StatisticsSeeder.php similarity index 83% rename from flexiapi/database/seeds/StatisticsMessagesSeeder.php rename to flexiapi/database/seeds/StatisticsSeeder.php index 617ce34..e8fd62e 100644 --- a/flexiapi/database/seeds/StatisticsMessagesSeeder.php +++ b/flexiapi/database/seeds/StatisticsSeeder.php @@ -19,23 +19,32 @@ namespace Database\Seeders; -use Illuminate\Database\Seeder; - +use App\StatisticsCall; +use App\StatisticsCallDevice; use App\StatisticsMessage; use App\StatisticsMessageDevice; + +use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Schema; -class StatisticsMessagesSeeder extends Seeder +class StatisticsSeeder extends Seeder { public function run() { Schema::disableForeignKeyConstraints(); StatisticsMessageDevice::truncate(); StatisticsMessage::truncate(); + + StatisticsCallDevice::truncate(); + StatisticsCall::truncate(); Schema::enableForeignKeyConstraints(); StatisticsMessage::factory() ->count(10000) ->create(); + + StatisticsCall::factory() + ->count(10000) + ->create(); } } diff --git a/flexiapi/resources/views/admin/statistics/show.blade.php b/flexiapi/resources/views/admin/statistics/show.blade.php index 3ff1381..e46d83a 100644 --- a/flexiapi/resources/views/admin/statistics/show.blade.php +++ b/flexiapi/resources/views/admin/statistics/show.blade.php @@ -13,6 +13,7 @@ 'items' => [ route('admin.statistics.show', ['type' => 'messages']) => 'Messages', route('admin.statistics.show', ['type' => 'accounts']) => 'Accounts', + route('admin.statistics.show', ['type' => 'calls']) => 'Calls', ], ]) diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index aa2d0a2..7557852 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -657,7 +657,7 @@ JSON parameters: * `id` required, string * `from` required, string the sender of the message -* `sent_at` required, string, format ISO8601, when the mesage was actually sent +* `sent_at` required, string, format ISO8601, when the message was actually sent * `encrypted` required, boolean * `conference_id` string @@ -672,6 +672,44 @@ JSON parameters: * `last_status` required, an integer containing the last status code * `received_at` required, format ISO8601, when the message was received +### `POST /statistics/calls` + +Admin + +Announce the beginning of a call. + +JSON parameters: + +* `id` required, string +* `from` required, string the initier of the call +* `to` required, string the destination of the call +* `initiated_at` required, string, format ISO8601, when the call was started +* `ended_at` string, format ISO8601, when the call finished +* `conference_id` string + +### `PATCH /statistics/calls/{call_id}/devices/{device_id}` + +Admin + +Complete a call status. + +JSON parameters: + +* `rang_at` format ISO8601, when the device rang +* `invite_terminated` + * `at` format ISO8601, when the invitation ended + * `state` the termination state + +### `PATCH /statistics/calls/{call_id}` + +Admin + +Update a call when ending. + +JSON parameters: + +* `ended_at` required, string, format ISO8601, when the call finished + # Non-API Endpoints The following URLs are **not API endpoints** they are not returning `JSON` content and they are not located under `/api` but directly under the root path. diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index ebeabd5..025e5b8 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -23,6 +23,7 @@ use App\Http\Controllers\Api\Admin\AccountController as AdminAccountController; use App\Http\Controllers\Api\Admin\AccountTypeController; use App\Http\Controllers\Api\Admin\ContactsListController; use App\Http\Controllers\Api\StatisticsMessageController; +use App\Http\Controllers\Api\StatisticsCallController; use Illuminate\Http\Request; Route::get('/', 'Api\ApiController@documentation')->name('api'); @@ -126,5 +127,11 @@ Route::group(['middleware' => ['auth.digest_or_key']], function () { Route::post('/', 'store'); Route::patch('{message_id}/to/{to}/devices/{device_id}', 'storeDevice'); }); + + Route::prefix('statistics/calls')->controller(StatisticsCallController::class)->group(function () { + Route::post('/', 'store'); + Route::patch('{call_id}', 'update'); + Route::patch('{call_id}/devices/{device_id}', 'storeDevice'); + }); }); }); diff --git a/flexiapi/tests/Feature/ApiStatisticsMessagesTest.php b/flexiapi/tests/Feature/ApiStatisticsMessagesTest.php deleted file mode 100644 index 7dd4294..0000000 --- a/flexiapi/tests/Feature/ApiStatisticsMessagesTest.php +++ /dev/null @@ -1,112 +0,0 @@ -. -*/ - -namespace Tests\Feature; - -use App\Admin; -use App\StatisticsMessageDevice; -use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Foundation\Testing\WithFaker; -use Tests\TestCase; - -class ApiStatisticsMessagesTest extends TestCase -{ - use WithFaker, RefreshDatabase; - - protected $route = '/api/statistics/messages'; - - public function testMessages() - { - $admin = Admin::factory()->create(); - $admin->account->generateApiKey(); - - $id = '1234'; - - $this->keyAuthenticated($admin->account) - ->json('POST', $this->route, [ - 'id' => $id, - 'from' => $this->faker->email(), - 'sent_at' => $this->faker->iso8601(), - 'encrypted' => false - ]) - ->assertStatus(200); - - $this->assertDatabaseHas('statistics_messages', [ - 'id' => $id - ]); - - $this->keyAuthenticated($admin->account) - ->json('POST', $this->route, [ - 'id' => $id, - 'from' => $this->faker->email(), - 'sent_at' => $this->faker->iso8601(), - 'encrypted' => false - ]) - ->assertStatus(400); - - $this->keyAuthenticated($admin->account) - ->json('POST', $this->route, [ - 'id' => $id, - 'from' => $this->faker->email(), - 'sent_at' => 'bad_date', - 'encrypted' => false - ]) - ->assertJsonValidationErrors(['sent_at']); - - // Patch previous message with devices - - $to = $this->faker->email(); - $device = $this->faker->uuid(); - - $receivedAt = $this->faker->iso8601(); - $lastStatus = 200; - - $newReceivedAt = $this->faker->iso8601(); - $newLastStatus = 201; - - $this->keyAuthenticated($admin->account) - ->json('PATCH', $this->route . '/' . $id . '/to/' . $to . ' /devices/' . $device, [ - 'last_status' => $lastStatus, - 'received_at' => $receivedAt - ]) - ->assertStatus(201); - - $this->keyAuthenticated($admin->account) - ->json('PATCH', $this->route . '/' . $id . '/to/' . $to . ' /devices/' . $device, [ - 'last_status' => $newLastStatus, - 'received_at' => $newReceivedAt - ]) - ->assertStatus(200); - - $this->assertSame(1, StatisticsMessageDevice::count()); - $this->assertDatabaseHas('statistics_message_devices', [ - 'message_id' => $id, - 'last_status' => $newLastStatus - ]); - - $this->keyAuthenticated($admin->account) - ->json('PATCH', $this->route . '/' . $id . '/to/' . $this->faker->email() . ' /devices/' . $this->faker->uuid(), [ - 'last_status' => $newLastStatus, - 'received_at' => $newReceivedAt - ]) - ->assertStatus(201); - - $this->assertSame(2, StatisticsMessageDevice::count()); - } -} diff --git a/flexiapi/tests/Feature/ApiStatisticsTest.php b/flexiapi/tests/Feature/ApiStatisticsTest.php new file mode 100644 index 0000000..9fcea97 --- /dev/null +++ b/flexiapi/tests/Feature/ApiStatisticsTest.php @@ -0,0 +1,229 @@ +. +*/ + +namespace Tests\Feature; + +use App\Account; +use App\Admin; +use App\StatisticsCallDevice; +use App\StatisticsMessageDevice; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Foundation\Testing\WithFaker; +use Tests\TestCase; + +class ApiStatisticsTest extends TestCase +{ + use WithFaker, RefreshDatabase; + + protected $routeMessages = '/api/statistics/messages'; + protected $routeCalls = '/api/statistics/calls'; + + public function testMessages() + { + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $id = '1234'; + $fromUsername = 'username'; + $fromDomain = 'domain.com'; + + $account = Account::factory()->create([ + 'username' => $fromUsername, + 'domain' => $fromDomain, + ]); + + $this->keyAuthenticated($admin->account) + ->json('POST', $this->routeMessages, [ + 'id' => $id, + 'from' => $fromUsername . '@' . $fromDomain, + 'sent_at' => $this->faker->iso8601(), + 'encrypted' => false + ]) + ->assertStatus(200); + + $this->assertDatabaseHas('statistics_messages', [ + 'id' => $id + ]); + + $this->keyAuthenticated($admin->account) + ->json('POST', $this->routeMessages, [ + 'id' => $id, + 'from' => $this->faker->email(), + 'sent_at' => $this->faker->iso8601(), + 'encrypted' => false + ]) + ->assertStatus(400); + + $this->keyAuthenticated($admin->account) + ->json('POST', $this->routeMessages, [ + 'id' => $id, + 'from' => $this->faker->email(), + 'sent_at' => 'bad_date', + 'encrypted' => false + ]) + ->assertJsonValidationErrors(['sent_at']); + + // Patch previous message with devices + + $to = $this->faker->email(); + $device = $this->faker->uuid(); + + $receivedAt = $this->faker->iso8601(); + $lastStatus = 200; + + $newReceivedAt = $this->faker->iso8601(); + $newLastStatus = 201; + + $this->keyAuthenticated($admin->account) + ->json('PATCH', $this->routeMessages . '/' . $id . '/to/' . $to . ' /devices/' . $device, [ + 'last_status' => $lastStatus, + 'received_at' => $receivedAt + ]) + ->assertStatus(201); + + $this->keyAuthenticated($admin->account) + ->json('PATCH', $this->routeMessages . '/' . $id . '/to/' . $to . ' /devices/' . $device, [ + 'last_status' => $newLastStatus, + 'received_at' => $newReceivedAt + ]) + ->assertStatus(200); + + $this->assertSame(1, StatisticsMessageDevice::count()); + $this->assertDatabaseHas('statistics_message_devices', [ + 'message_id' => $id, + 'last_status' => $newLastStatus + ]); + + $this->keyAuthenticated($admin->account) + ->json('PATCH', $this->routeMessages . '/' . $id . '/to/' . $this->faker->email() . ' /devices/' . $this->faker->uuid(), [ + 'last_status' => $newLastStatus, + 'received_at' => $newReceivedAt + ]) + ->assertStatus(201); + + $this->assertSame(2, StatisticsMessageDevice::count()); + + // Deletion event test + + $account->delete(); + $this->assertDatabaseMissing('statistics_messages', [ + 'id' => $id + ]); + } + + public function testCalls() + { + $admin = Admin::factory()->create(); + $admin->account->generateApiKey(); + + $id = '1234'; + $fromUsername = 'username'; + $fromDomain = 'domain.com'; + $toUsername = 'usernameto'; + $toDomain = 'domainto.com'; + + $account = Account::factory()->create([ + 'username' => $fromUsername, + 'domain' => $fromDomain, + ]); + + $this->keyAuthenticated($admin->account) + ->json('POST', $this->routeCalls, [ + 'id' => $id, + 'from' => $fromUsername . '@' . $fromDomain, + 'to' => $toUsername . '@' . $toDomain, + 'initiated_at' => $this->faker->iso8601(), + ]) + ->assertStatus(200); + + $this->assertDatabaseHas('statistics_calls', [ + 'id' => $id + ]); + + $this->keyAuthenticated($admin->account) + ->json('POST', $this->routeCalls, [ + 'id' => $id, + 'from' => $fromUsername . '@' . $fromDomain, + 'to' => $toUsername . '@' . $toDomain, + 'initiated_at' => $this->faker->iso8601(), + ]) + ->assertStatus(400); + + // Patch previous call with devices* + + $to = $this->faker->email(); + $device = $this->faker->uuid(); + + $rangAt = $this->faker->iso8601(); + $newRangAt = $this->faker->iso8601(); + + $this->keyAuthenticated($admin->account) + ->json('PATCH', $this->routeCalls . '/' . $id . '/devices/' . $device, [ + 'rang_at' => $rangAt, + 'invite_terminated' => [ + 'at' => $this->faker->iso8601(), + 'state' => 'declined' + ] + ]) + ->assertStatus(201); + + $this->keyAuthenticated($admin->account) + ->json('PATCH', $this->routeCalls . '/' . $id . '/devices/' . $device, [ + 'rang_at' => $newRangAt, + 'invite_terminated' => [ + 'at' => $this->faker->iso8601(), + 'state' => 'declined' + ] + ]) + ->assertStatus(200); + + $this->keyAuthenticated($admin->account) + ->json('PATCH', $this->routeCalls . '/' . $id . '/devices/' . $device, [ + 'invite_terminated' => [ + 'state' => 'declined' + ] + ]) + ->assertStatus(422); + + $this->keyAuthenticated($admin->account) + ->json('PATCH', $this->routeCalls . '/' . $id . '/devices/' . $device, [ + 'rang_at' => $this->faker->iso8601() + ]) + ->assertStatus(200); + + $this->assertSame(1, StatisticsCallDevice::count()); + + // Update + + $endedAt = $this->faker->iso8601(); + + $this->keyAuthenticated($admin->account) + ->json('PATCH', $this->routeCalls . '/' . $id, [ + 'ended_at' => $endedAt + ]) + ->assertStatus(200); + + // Deletion event test + + $account->delete(); + $this->assertDatabaseMissing('statistics_calls', [ + 'id' => $id + ]); + } +}