# Infinity Outgoing Call (Kazakhstan) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Let a Polaris operator on the Kazakhstan instance start an outgoing call to a client from the client card via the Infinity telephony API (`POST /rest/v1/uc/calls`), interpret the synchronous `result` field, and log every call.

**Architecture:** Mirror the existing Webitel click-to-call path on the Infinity stack. A new `InfinityCallService` posts the call (Bearer token, `Http` facade — same style as `InfinityWebhookService`), always writes an `InfinityCallLog` row, and returns a `{result, callId}` DTO whose `result` is an `InfinityCallResult` enum. The Livewire `MainActionsComponent::initInfinityCall` handler `match`es the result: `ok` opens the communications window (`setAction(1)`), every other value shows the prescribed operator message. A Kazakhstan-only button in `phones.blade.php` dispatches the Livewire event after a SweetAlert confirmation.

**Tech Stack:** PHP 8.3, Laravel 10, Livewire 3, `Illuminate\Support\Facades\Http`, PHPUnit 10. Runs inside the `polaris-service-new` Docker container.

**Spec:** `docs/superpowers/specs/2026-06-04-infinity-outgoing-call-design.md`

> **All commands run inside the container.** Prefix Artisan/PHPUnit/Pint with
> `docker exec -i polaris-service-new ...`. Run `vendor/bin/pint --dirty` before each commit.

---

## File Structure

**Create:**
- `app/Integrations/Infinity/Enums/InfinityCallResult.php` — result status enum + parsing/message-key helpers.
- `app/Integrations/Infinity/DTO/InfinityCallResultData.php` — readonly `{result, callId}` carrier.
- `app/Integrations/Infinity/Services/InfinityCallService.php` — posts the call, logs it, returns the DTO.
- `app/Models/Collection/InfinityCallLog.php` — Eloquent model on the `collection` connection.
- `database/migrations/XXXX_XX_XX_XXXXXX_create_infinity_call_logs_table.php` — log table.
- `tests/Unit/Integrations/Infinity/Enums/InfinityCallResultTest.php` — pure unit test.
- `tests/Feature/Integrations/Infinity/InfinityCallServiceTest.php` — `Http::fake()` feature test.

**Modify:**
- `app/Livewire/Client/MainActionsComponent.php` — add listener + `initInfinityCall` method + import.
- `resources/views/livewire/client/phones.blade.php` — Kazakhstan button + `doCallKazakhstan` JS.
- `resources/views/livewire/main-actions.blade.php` — add `initInfinityCall` to loader `wire:target`.
- `resources/lang/es/views.php`, `resources/lang/ru/views.php`, `resources/lang/en/views.php` — 5 result-message keys.

---

## Task 1: Result enum (pure logic, TDD)

**Files:**
- Create: `app/Integrations/Infinity/Enums/InfinityCallResult.php`
- Test: `tests/Unit/Integrations/Infinity/Enums/InfinityCallResultTest.php`

- [ ] **Step 1: Write the failing test**

Create `tests/Unit/Integrations/Infinity/Enums/InfinityCallResultTest.php`:

```php
<?php

namespace Tests\Unit\Integrations\Infinity\Enums;

use App\Integrations\Infinity\Enums\InfinityCallResult;
use Tests\TestCase;

class InfinityCallResultTest extends TestCase
{
    public function test_from_api_maps_known_values(): void
    {
        $this->assertSame(InfinityCallResult::Ok, InfinityCallResult::fromApi('ok'));
        $this->assertSame(InfinityCallResult::Rejected, InfinityCallResult::fromApi('rejected'));
        $this->assertSame(InfinityCallResult::Timeout, InfinityCallResult::fromApi('timeout'));
        $this->assertSame(InfinityCallResult::Error, InfinityCallResult::fromApi('error'));
        $this->assertSame(InfinityCallResult::Redirected, InfinityCallResult::fromApi('redirected'));
        $this->assertSame(InfinityCallResult::Undefined, InfinityCallResult::fromApi('undefined'));
    }

    public function test_from_api_maps_null_and_garbage_to_undefined(): void
    {
        $this->assertSame(InfinityCallResult::Undefined, InfinityCallResult::fromApi(null));
        $this->assertSame(InfinityCallResult::Undefined, InfinityCallResult::fromApi(''));
        $this->assertSame(InfinityCallResult::Undefined, InfinityCallResult::fromApi('something-else'));
    }

    public function test_is_ok_only_true_for_ok(): void
    {
        $this->assertTrue(InfinityCallResult::Ok->isOk());
        $this->assertFalse(InfinityCallResult::Rejected->isOk());
    }

    public function test_message_key_null_for_ok_and_set_for_failures(): void
    {
        $this->assertNull(InfinityCallResult::Ok->messageKey());
        $this->assertSame('views.client.phones.call_rejected', InfinityCallResult::Rejected->messageKey());
        $this->assertSame('views.client.phones.call_timeout', InfinityCallResult::Timeout->messageKey());
        $this->assertSame('views.client.phones.call_error', InfinityCallResult::Error->messageKey());
        $this->assertSame('views.client.phones.call_redirected', InfinityCallResult::Redirected->messageKey());
        $this->assertSame('views.client.phones.call_undefined', InfinityCallResult::Undefined->messageKey());
    }

    public function test_should_log_true_for_error_redirected_undefined(): void
    {
        $this->assertTrue(InfinityCallResult::Error->shouldLog());
        $this->assertTrue(InfinityCallResult::Redirected->shouldLog());
        $this->assertTrue(InfinityCallResult::Undefined->shouldLog());
        $this->assertFalse(InfinityCallResult::Ok->shouldLog());
        $this->assertFalse(InfinityCallResult::Rejected->shouldLog());
        $this->assertFalse(InfinityCallResult::Timeout->shouldLog());
    }
}
```

- [ ] **Step 2: Run the test to verify it fails**

Run: `docker exec -i polaris-service-new php artisan test --compact --filter=InfinityCallResultTest`
Expected: FAIL — `Class "App\Integrations\Infinity\Enums\InfinityCallResult" not found`.

- [ ] **Step 3: Write the enum**

Create `app/Integrations/Infinity/Enums/InfinityCallResult.php`:

```php
<?php

namespace App\Integrations\Infinity\Enums;

enum InfinityCallResult: string
{
    case Ok = 'ok';
    case Rejected = 'rejected';
    case Timeout = 'timeout';
    case Error = 'error';
    case Redirected = 'redirected';
    case Undefined = 'undefined';

    public static function fromApi(?string $value): self
    {
        return self::tryFrom((string) $value) ?? self::Undefined;
    }

    public function isOk(): bool
    {
        return $this === self::Ok;
    }

    public function shouldLog(): bool
    {
        return in_array($this, [self::Error, self::Redirected, self::Undefined], true);
    }

    public function messageKey(): ?string
    {
        return match ($this) {
            self::Ok => null,
            self::Rejected => 'views.client.phones.call_rejected',
            self::Timeout => 'views.client.phones.call_timeout',
            self::Error => 'views.client.phones.call_error',
            self::Redirected => 'views.client.phones.call_redirected',
            self::Undefined => 'views.client.phones.call_undefined',
        };
    }
}
```

- [ ] **Step 4: Run the test to verify it passes**

Run: `docker exec -i polaris-service-new php artisan test --compact --filter=InfinityCallResultTest`
Expected: PASS (5 tests).

- [ ] **Step 5: Format and commit**

```bash
docker exec -i polaris-service-new vendor/bin/pint --dirty
git add app/Integrations/Infinity/Enums/InfinityCallResult.php tests/Unit/Integrations/Infinity/Enums/InfinityCallResultTest.php
git commit -m "feat(infinity): add InfinityCallResult enum"
```

---

## Task 2: Result DTO

**Files:**
- Create: `app/Integrations/Infinity/DTO/InfinityCallResultData.php`

- [ ] **Step 1: Write the DTO**

Create `app/Integrations/Infinity/DTO/InfinityCallResultData.php`:

```php
<?php

namespace App\Integrations\Infinity\DTO;

use App\Integrations\Infinity\Enums\InfinityCallResult;

class InfinityCallResultData
{
    public function __construct(
        public readonly InfinityCallResult $result,
        public readonly ?string $callId,
    ) {
    }
}
```

- [ ] **Step 2: Format and commit**

```bash
docker exec -i polaris-service-new vendor/bin/pint --dirty
git add app/Integrations/Infinity/DTO/InfinityCallResultData.php
git commit -m "feat(infinity): add InfinityCallResultData DTO"
```

---

## Task 3: `infinity_call_logs` migration + model

**Files:**
- Create: `database/migrations/XXXX_XX_XX_XXXXXX_create_infinity_call_logs_table.php`
- Create: `app/Models/Collection/InfinityCallLog.php`

Mirrors `webitel_logs` (`database/migrations/2025_09_05_112036_added_webitel_logs_table.php` + `app/Models/Collection/WebitelLog.php`), with an extra `result` column.

- [ ] **Step 1: Generate the migration file**

Run: `docker exec -i polaris-service-new php artisan make:migration create_infinity_call_logs_table --no-interaction`

- [ ] **Step 2: Replace the migration body**

Open the generated file and replace its contents with:

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function getConnection(): string
    {
        return 'collection';
    }

    public function up(): void
    {
        Schema::create('infinity_call_logs', function (Blueprint $table) {
            $table->id();
            $table->string('document')->nullable();
            $table->integer('extension')->nullable();
            $table->unsignedBigInteger('agent_id')->nullable();
            $table->unsignedBigInteger('client_id')->nullable();
            $table->text('request')->nullable();
            $table->text('response')->nullable();
            $table->string('result')->nullable();
            $table->dateTime('call_date')->nullable();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('infinity_call_logs');
    }
};
```

- [ ] **Step 3: Create the model**

Run: `docker exec -i polaris-service-new php artisan make:model Collection/InfinityCallLog --no-interaction`

Then replace `app/Models/Collection/InfinityCallLog.php` contents with:

```php
<?php

namespace App\Models\Collection;

use Illuminate\Database\Eloquent\Model;

class InfinityCallLog extends Model
{
    protected $connection = 'collection';

    public $timestamps = false;

    protected $casts = [
        'call_date' => 'datetime',
    ];
}
```

- [ ] **Step 4: Run the migration**

Run: `docker exec -i polaris-service-new php artisan migrate --no-interaction`
Expected: `infinity_call_logs` table created (migration line shows `DONE`).

- [ ] **Step 5: Format and commit**

```bash
docker exec -i polaris-service-new vendor/bin/pint --dirty
git add database/migrations/*_create_infinity_call_logs_table.php app/Models/Collection/InfinityCallLog.php
git commit -m "feat(infinity): add infinity_call_logs table and model"
```

---

## Task 4: `InfinityCallService` (TDD with `Http::fake()`)

**Files:**
- Create: `app/Integrations/Infinity/Services/InfinityCallService.php`
- Test: `tests/Feature/Integrations/Infinity/InfinityCallServiceTest.php`

The test wraps each case in a transaction on the `collection` connection (via `DatabaseTransactions` + `$connectionsToTransact`) so logged rows roll back. It does not migrate or touch any real data.

- [ ] **Step 1: Write the failing test**

Create `tests/Feature/Integrations/Infinity/InfinityCallServiceTest.php`:

```php
<?php

namespace Tests\Feature\Integrations\Infinity;

use App\Integrations\Infinity\Enums\InfinityCallResult;
use App\Integrations\Infinity\Services\InfinityCallService;
use App\Models\Collection\Client\Client;
use App\Models\Collection\InfinityCallLog;
use App\Models\User\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

class InfinityCallServiceTest extends TestCase
{
    use DatabaseTransactions;

    protected $connectionsToTransact = ['collection'];

    private function client(): Client
    {
        $client = new Client();
        $client->documento = '222';
        $client->client_id = 'abc-123';

        return $client;
    }

    private function user(): User
    {
        $user = new User();
        $user->id = 7;
        $user->webitel_id = 5005;

        return $user;
    }

    public function test_posts_expected_payload_and_returns_ok_with_call_id(): void
    {
        Http::fake([
            '*/rest/v1/uc/calls' => Http::response(['result' => 'ok', 'id' => 'call-99'], 200),
        ]);

        $data = resolve(InfinityCallService::class)->call($this->client(), $this->user(), '77001234567');

        $this->assertSame(InfinityCallResult::Ok, $data->result);
        $this->assertSame('call-99', $data->callId);

        Http::assertSent(function (Request $request): bool {
            return str_ends_with($request->url(), '/rest/v1/uc/calls')
                && $request['from'] === '5005'
                && $request['to'] === '77001234567'
                && $request['mode'] === true
                && $request->hasHeader('Authorization', 'Bearer '.config('integrations.infinity.token'));
        });
    }

    public function test_logs_request_response_and_result(): void
    {
        Http::fake([
            '*/rest/v1/uc/calls' => Http::response(['result' => 'rejected'], 200),
        ]);

        resolve(InfinityCallService::class)->call($this->client(), $this->user(), '77001234567');

        $log = InfinityCallLog::query()->latest('id')->first();

        $this->assertNotNull($log);
        $this->assertSame('rejected', $log->result);
        $this->assertSame('222', $log->document);
        $this->assertSame(7, $log->agent_id);
        $this->assertStringContainsString('77001234567', $log->request);
        $this->assertStringContainsString('rejected', $log->response);
    }

    public function test_unknown_result_value_maps_to_undefined(): void
    {
        Http::fake([
            '*/rest/v1/uc/calls' => Http::response(['result' => 'weird'], 200),
        ]);

        $data = resolve(InfinityCallService::class)->call($this->client(), $this->user(), '77001234567');

        $this->assertSame(InfinityCallResult::Undefined, $data->result);
    }

    public function test_http_failure_logs_error_and_rethrows(): void
    {
        Http::fake([
            '*/rest/v1/uc/calls' => Http::response('boom', 500),
        ]);

        try {
            resolve(InfinityCallService::class)->call($this->client(), $this->user(), '77001234567');
            $this->fail('Expected RequestException was not thrown');
        } catch (\Illuminate\Http\Client\RequestException $exception) {
            $log = InfinityCallLog::query()->latest('id')->first();
            $this->assertSame(InfinityCallResult::Error->value, $log->result);
        }
    }
}
```

- [ ] **Step 2: Run the test to verify it fails**

Run: `docker exec -i polaris-service-new php artisan test --compact --filter=InfinityCallServiceTest`
Expected: FAIL — `Class "App\Integrations\Infinity\Services\InfinityCallService" not found`.

- [ ] **Step 3: Write the service**

Create `app/Integrations/Infinity/Services/InfinityCallService.php`:

```php
<?php

namespace App\Integrations\Infinity\Services;

use App\Integrations\Infinity\DTO\InfinityCallResultData;
use App\Integrations\Infinity\Enums\InfinityCallResult;
use App\Models\Collection\Client\Client;
use App\Models\Collection\InfinityCallLog;
use App\Models\User\User;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class InfinityCallService
{
    private const string CALLS_PATH = '/rest/v1/uc/calls';

    /**
     * @throws RequestException
     */
    public function call(Client $client, User $user, string $phone): InfinityCallResultData
    {
        $payload = [
            'from' => (string) $user->webitel_id,
            'to' => $phone,
            'mode' => true,
        ];

        $log = new InfinityCallLog();
        $log->document = $client->documento ?? null;
        $log->client_id = $client->client_id ?? null;
        $log->extension = $user->webitel_id ?: null;
        $log->agent_id = $user->id ?? null;
        $log->request = json_encode($payload);
        $log->call_date = now();
        $log->save();

        try {
            $response = Http::withToken(config('integrations.infinity.token'))
                ->acceptJson()
                ->asJson()
                ->timeout(70)
                ->post($this->endpoint(), $payload)
                ->throw();
        } catch (RequestException $exception) {
            $log->response = $exception->response?->body();
            $log->result = InfinityCallResult::Error->value;
            $log->save();

            Log::channel('stderr')->error('Infinity call request failed', [
                'message' => $exception->getMessage(),
                'payload' => $payload,
            ]);

            throw $exception;
        }

        $body = $response->json();
        $result = InfinityCallResult::fromApi($body['result'] ?? null);
        $callId = isset($body['id']) ? (string) $body['id'] : null;

        $log->response = $response->body();
        $log->result = $result->value;
        $log->save();

        if ($result->shouldLog()) {
            Log::channel('stderr')->warning('Infinity call returned non-ok result', [
                'result' => $result->value,
                'payload' => $payload,
                'response' => $body,
            ]);
        }

        return new InfinityCallResultData($result, $callId);
    }

    private function endpoint(): string
    {
        return rtrim((string) config('integrations.infinity.base_uri'), '/').self::CALLS_PATH;
    }
}
```

- [ ] **Step 4: Run the test to verify it passes**

Run: `docker exec -i polaris-service-new php artisan test --compact --filter=InfinityCallServiceTest`
Expected: PASS (4 tests).

- [ ] **Step 5: Format and commit**

```bash
docker exec -i polaris-service-new vendor/bin/pint --dirty
git add app/Integrations/Infinity/Services/InfinityCallService.php tests/Feature/Integrations/Infinity/InfinityCallServiceTest.php
git commit -m "feat(infinity): add InfinityCallService with call logging"
```

---

## Task 5: Translation keys (es / ru / en)

**Files:**
- Modify: `resources/lang/es/views.php`
- Modify: `resources/lang/ru/views.php`
- Modify: `resources/lang/en/views.php`

Each file has a `views.client.phones` block ending with `'saved_successfully' => ...`. Add the five keys immediately after that line, inside the same `'phones' => [ ... ]` array.

- [ ] **Step 1: Add Spanish keys**

In `resources/lang/es/views.php`, find within the `'phones' => [` block:

```php
            'saved_successfully' => 'Guardado con éxito',
        ],
```

Replace with:

```php
            'saved_successfully' => 'Guardado con éxito',
            'call_rejected' => 'Llamada rechazada. Intente de nuevo',
            'call_timeout' => 'Tiempo agotado. Intente de nuevo',
            'call_error' => 'Error técnico. Consulte al administrador.',
            'call_redirected' => 'Llamada redirigida.',
            'call_undefined' => 'Estado desconocido. Revise el registro',
        ],
```

- [ ] **Step 2: Add Russian keys**

In `resources/lang/ru/views.php`, find within the `'phones' => [` block:

```php
            'saved_successfully' => 'Успешно сохранено',
        ],
```

Replace with:

```php
            'saved_successfully' => 'Успешно сохранено',
            'call_rejected' => 'Звонок отклонён. Попробуйте снова',
            'call_timeout' => 'Время ожидания истекло. Попробуйте снова',
            'call_error' => 'Техническая ошибка. Обратитесь к администратору.',
            'call_redirected' => 'Звонок переадресован.',
            'call_undefined' => 'Статус неизвестен. Проверьте журнал',
        ],
```

- [ ] **Step 3: Add English keys**

In `resources/lang/en/views.php`, find within the first `'phones' => [` block (around line 107) the `'saved_successfully'` entry that closes the block:

```php
            'saved_successfully' => 'Saved successfully',
        ],
```

Replace with:

```php
            'saved_successfully' => 'Saved successfully',
            'call_rejected' => 'Call rejected. Try again',
            'call_timeout' => 'Timed out. Try again',
            'call_error' => 'Technical error. Contact the administrator.',
            'call_redirected' => 'Call redirected.',
            'call_undefined' => 'Unknown status. Check the log',
        ],
```

> If the exact `saved_successfully` source string differs in `en`, keep its current
> value and only append the five `call_*` lines after it, inside the same `phones` array.

- [ ] **Step 4: Verify keys resolve**

Run:
```bash
docker exec -i polaris-service-new php artisan tinker --execute="echo __('views.client.phones.call_rejected', [], 'es'), PHP_EOL, __('views.client.phones.call_error', [], 'ru');"
```
Expected: prints `Llamada rechazada. Intente de nuevo` then the Russian technical-error string (no `views.client.phones...` passthrough).

- [ ] **Step 5: Commit**

```bash
git add resources/lang/es/views.php resources/lang/ru/views.php resources/lang/en/views.php
git commit -m "feat(infinity): add outgoing call result messages (es/ru/en)"
```

---

## Task 6: Livewire `initInfinityCall` handler

**Files:**
- Modify: `app/Livewire/Client/MainActionsComponent.php`

- [ ] **Step 1: Add the import**

In `app/Livewire/Client/MainActionsComponent.php`, after the line:

```php
use App\Integrations\Webitel\Services\WebitelCallService;
```

add:

```php
use App\Integrations\Infinity\Services\InfinityCallService;
```

- [ ] **Step 2: Register the listener**

In `getListeners()` (around line 200-208), change:

```php
            'togglePhoneStatus' => 'togglePhoneStatus',
            'initWebitelCall'   => 'initWebitelCall',
        ];
```

to:

```php
            'togglePhoneStatus' => 'togglePhoneStatus',
            'initWebitelCall'   => 'initWebitelCall',
            'initInfinityCall'  => 'initInfinityCall',
        ];
```

- [ ] **Step 3: Add the handler method**

Immediately after the `initWebitelCall()` method (ends around line 391 with its closing `}`), add:

```php
    public function initInfinityCall(int $phoneId): void
    {
        /** @var Phone $phone */
        $phone = $this->phones->firstWhere('id', $phoneId);

        /** @var InfinityCallService $infinityCallService */
        $infinityCallService = resolve(InfinityCallService::class);

        try {
            $callResult = $infinityCallService->call(
                $this->client,
                User::findCached($this->userId),
                $phone->telefono
            );
        } catch (\Throwable $exception) {
            $this->dispatch('showError', message: __('views.client.phones.call_error'));
            Log::channel('sentry')->error($exception);

            return;
        }

        $this->externalCommunicationId = $callResult->callId;

        if ($callResult->result->isOk()) {
            $this->setAction(1, $phoneId);

            return;
        }

        $this->dispatch('showError', message: __($callResult->result->messageKey()));
    }
```

- [ ] **Step 4: Sanity-check the class still loads**

Run: `docker exec -i polaris-service-new php artisan test --compact --filter=InfinityCallServiceTest`
Expected: PASS (autoloading/parse check — confirms no syntax error introduced in the component).

- [ ] **Step 5: Format and commit**

```bash
docker exec -i polaris-service-new vendor/bin/pint --dirty
git add app/Livewire/Client/MainActionsComponent.php
git commit -m "feat(infinity): add initInfinityCall Livewire handler"
```

---

## Task 7: Kazakhstan call button + JS + loader

**Files:**
- Modify: `resources/views/livewire/client/phones.blade.php`
- Modify: `resources/views/livewire/main-actions.blade.php`

- [ ] **Step 1: Add the Kazakhstan button**

In `resources/views/livewire/client/phones.blade.php`, find the `@if(isPeru())` block:

```php
                        @if(isPeru())
                            <div onclick="doCallPeru({{$phone->id}})" data-bs-toggle="tooltip"
                                 data-bs-placement="top" data-bs-original-title="{{ __('views.client.gestiones.phones.call') }}" class="float-end text-muted d-inline-flex me-1 cursor-pointer">
                                <i class="las la-phone-volume fs-26"></i>
                            </div>
                        @endif
```

Immediately after that `@endif`, add:

```php
                        @if(isKazakhstan())
                            <div onclick="doCallKazakhstan({{$phone->id}})" data-bs-toggle="tooltip"
                                 data-bs-placement="top" data-bs-original-title="{{ __('views.client.gestiones.phones.call') }}" class="float-end text-muted d-inline-flex me-1 cursor-pointer">
                                <i class="las la-phone-volume fs-26"></i>
                            </div>
                        @endif
```

- [ ] **Step 2: Add the `doCallKazakhstan` JS function**

In the same file, inside the `<script>` block, after the `doCallColombia` function's closing `}` (before any following function), add:

```js
    function doCallKazakhstan(phoneId) {
        Swal.fire({
            text: "{{ __('views.client.phones.confirm_call') }}",
            icon: "question",
            confirmButtonText: "{{ __('views.client.phones.yes') }}",
            cancelButtonText: "{{ __('views.client.phones.no') }}",
            showCloseButton: true,
            showCancelButton: true,
            preConfirm: () => {
                Livewire.dispatch('initInfinityCall', {phoneId});
            },
        });
    }
```

- [ ] **Step 3: Add `initInfinityCall` to the loader target**

In `resources/views/livewire/main-actions.blade.php`, change:

```php
    <div wire:loading wire:target="initWebitelCall" class="loading-waiter">
```

to:

```php
    <div wire:loading wire:target="initWebitelCall,initInfinityCall" class="loading-waiter">
```

- [ ] **Step 4: Rebuild assets / clear views**

Run:
```bash
docker exec -i polaris-service-new php artisan view:clear
```
(Blade-only change — no Vite rebuild needed. If the user reports the button missing, ask them to run `npm run dev` / `npm run build`.)

- [ ] **Step 5: Commit**

```bash
git add resources/views/livewire/client/phones.blade.php resources/views/livewire/main-actions.blade.php
git commit -m "feat(infinity): add Kazakhstan outgoing call button"
```

---

## Task 8: Manual verification (Livewire + UI wiring)

No automated Livewire test — mounting `MainActionsComponent` needs a real `Client` and live repositories, which this repo's test suite deliberately avoids (see CLAUDE.md). Verify the end-to-end wiring manually.

- [ ] **Step 1: Confirm the full suite is green**

Run: `docker exec -i polaris-service-new php artisan test --compact`
Expected: all tests pass (includes `InfinityCallResultTest`, `InfinityCallServiceTest`).

- [ ] **Step 2: Confirm Kazakhstan-instance config**

Run:
```bash
docker exec -i polaris-service-new php artisan tinker --execute="echo config('polaris.instance'), PHP_EOL; var_dump(isKazakhstan()); echo config('integrations.infinity.base_uri');"
```
Expected: prints `kazakhstan`, `true`, and the Infinity base URI. If the container is not running the Kazakhstan instance, the button won't render — note this to the user and verify the Blade `@if(isKazakhstan())` branch by other means.

- [ ] **Step 3: UI smoke test (operator flow)**

In a Kazakhstan-instance browser session, open a client card → Phones tab. Confirm:
- The phone icon (`la-phone-volume`) appears next to each phone row.
- Clicking it shows the SweetAlert confirmation.
- Confirming triggers the loader ("creating call" spinner) and, on a real `ok`, opens the management window (`setAction(1)`); on a failure result, shows the matching localized toast.

- [ ] **Step 4: Confirm a log row was written**

After a test call, run:
```bash
docker exec -i polaris-service-new php artisan tinker --execute="echo App\Models\Collection\InfinityCallLog::query()->latest('id')->first()?->toJson();"
```
Expected: a row with `request`, `response`, `result`, `agent_id`, `document` populated.

- [ ] **Step 5: Reconcile `callId` field with the real API response**

Inspect the logged `response` JSON. If the call id is not under `id` (e.g. it's `call_id` or nested), update `InfinityCallService::call()` where `$callId` is derived, re-run `InfinityCallServiceTest`, and commit the fix. (Open question from the spec.)

---

## Self-Review Notes

- **Spec coverage:** button (Task 7) · `result` interpretation table (Task 1 enum + Task 6 match) · per-result operator messages (Task 5) · synchronous `result` from POST (Task 4 service) · `from = webitel_id` / `to = telefono` raw / `mode:true` (Task 4 payload + test) · logging per call like Webitel (Task 3 + Task 4) · all six `result` values incl. `undefined` fallback (Task 1) · HTTP-failure path (Task 4 + Task 6). ✅
- **Type consistency:** `InfinityCallResult` (enum), `InfinityCallResultData {result, callId}` (DTO), `InfinityCallService::call(Client, User, string): InfinityCallResultData`, `InfinityCallLog` columns `document/extension/agent_id/client_id/request/response/result/call_date` — used identically across Tasks 1-6. ✅
- **Open item (intentional):** real API call-id field name — bounded and handled in Task 8 Step 5; full response is logged so it's trivially correctable.
```
