# Communication Module

Unified, queue-driven communication system supporting SMS and Voice channels with tenant-safe wallet management.

## Architecture

```
Producer (BuildSms / StoreSmsSendOutJob)
    │
    ▼
sms_send_out (outbox table, channel-agnostic)
    │
    ▼
ProcessCommunicationJob
    ├── claim record (row lock, pending → processing)
    ├── calculate units + pricing via ChannelInterface
    ├── deduct wallet via WalletService (row lock, balance + credit)
    ├── send via SmsChannel or VoiceChannel
    └── update status, log result, refund on failure
```

### Safety net

`sms:send` runs every minute via scheduler. It picks up any `pending` records that weren't dispatched (e.g. if `StoreSmsSendOutJob` failed before dispatching `ProcessCommunicationJob`) and enqueues them.

## Key services

| Service | Path | Purpose |
|---------|------|---------|
| `CommunicationManager` | `Services/Communication/CommunicationManager.php` | Resolves channel by name |
| `SmsChannel` | `Services/Communication/SmsChannel.php` | SMS gateway resolution, HTTP send, unit calculation |
| `VoiceChannel` | `Services/Communication/VoiceChannel.php` | Voice gateway resolution, campaign-based send |
| `SmsUnitCalculator` | `Services/Communication/SmsUnitCalculator.php` | GSM-7 vs Unicode detection, segment counting |
| `WalletService` | `Services/Wallet/WalletService.php` | Transactional balance operations with row locking |
| `ChannelInterface` | `Services/Communication/ChannelInterface.php` | Contract for all channels |
| `ChannelResult` | `Services/Communication/ChannelResult.php` | Normalized send result DTO |

## Database schema

### `sms_send_out` (outbox / ledger)

Original SMS fields (`message`, `sms_from`, `sms_to`, `sms_type`, `sms_count`, `sms_gateway_id`, `send_at`, `expire_at`, `status`, `is_error`, `comment`) are preserved.

Added fields:

| Column | Type | Purpose |
|--------|------|---------|
| `channel` | enum(sms,voice) | Which channel to use |
| `unit_price` | decimal(10,4) | Price per unit at time of send |
| `total_units` | int | Calculated units (segments for SMS, calls for voice) |
| `total_price` | decimal(10,4) | unit_price × total_units |
| `provider_status` | enum(pending,sent,failed) | Provider-level outcome |
| `provider_response` | text | Raw provider response |
| `failure_reason` | string | Human-readable failure cause |
| `sent_at` | timestamp | When provider confirmed send |
| `meta` | json | Arbitrary data: campaign_name, balance snapshots, cost breakdown |
| `attempts` | int | Number of processing attempts |
| `locked_at` | timestamp | When a worker claimed this record |
| `processed_at` | timestamp | When processing completed |

### `wallets`

All channels share a unified balance:

| Column | Type | Purpose |
|--------|------|---------|
| `balance` | int (×100) | Primary balance for all channels |
| `credit_balance` | int (×100) | Credit line for all channels |

All balances stored as integers (/100 scaling via model accessors).

### `voice_gateways`

Mirrors `sms_gateways` structure: `company_id`, `reseller_id`, `name`, `voice_rates` (×100 int), `voice_api`, `balance_api`, `status`, `note`.

## Status flow

```
pending → processing → completed
                    └→ failed
```

- **pending**: created by producer, waiting for `send_at`
- **processing**: claimed by `ProcessCommunicationJob` (locked_at set)
- **completed**: provider confirmed send
- **failed**: insufficient balance, provider error, or job exception

Records are never deleted. The outbox doubles as an audit log.

## Retry rules

- `ProcessCommunicationJob` has `tries = 3` with backoff `[30, 120, 300]` seconds.
- Retries apply to transient errors (HTTP timeouts, network issues).
- **Insufficient balance**: job is deleted immediately (no retry).
- **Provider rejection**: stored as failed, no retry.
- On permanent failure, the `failed()` method updates the record status.

## Wallet deduction logic

`WalletService::checkAndDeduct()`:

1. Lock wallet row with `FOR UPDATE` inside a transaction.
2. Check `balance + credit_balance >= amount`.
3. Deduct from primary balance first, spill remainder into credit.
4. Return balance snapshots for traceability.

On send failure, `WalletService::refund()` restores the amount to primary balance.

All channels use the unified `balance` and `credit_balance` fields.

## Tenant scoping

- Gateway resolution uses `(company_id, reseller_id)` with fallback to `company_id = 1`.
- `SmsSendOut` uses `BelongsToTenants` + `UseResellerScope`.
- In CLI/queue context, `ResellerScope` is bypassed via `config('app.skip_reseller_scope')` or `withoutGlobalScope()`.

## Adding a new channel

1. Create `NewChannel implements ChannelInterface` in `Services/Communication/`.
2. Register in `CommunicationManager::__construct()`.
3. Add gateway table migration if needed.
4. Add `'newchannel'` to the `channel` enum on `communication_queues`.
5. All channels share the unified `balance` + `credit_balance` wallet fields.

## Logging

| Channel | Path | Content |
|---------|------|---------|
| `sms` | `logs/sms/sms.log` | Successful SMS sends with cost/balance info |
| `smserror` | `logs/smserror/sms-error.log` | SMS failures, build errors |
| `voice` | `logs/voice/voice.log` | Successful voice sends |
| `voiceerror` | `logs/voiceerror/voice-error.log` | Voice failures |

All log entries include the outbox `uuid` for correlation.
